Skip to content

Commit

Permalink
Supply 'templateNodes' to 'componentInfo' so that components can acce…
Browse files Browse the repository at this point in the history
…pt inline templates
  • Loading branch information
SteveSanderson committed Oct 7, 2014
1 parent a32060f commit 98c55ee
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 6 deletions.
8 changes: 7 additions & 1 deletion spec/components/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,18 @@ describe('Components: Component binding', function() {
expect(testNode.childNodes[0].childNodes[0]).not.toBe(testTemplate[0]);
});

it('Passes params and componentInfo (with prepopulated element) to the component\'s viewmodel factory', function() {
it('Passes params and componentInfo (with prepopulated element and templateNodes) to the component\'s viewmodel factory', function() {
var componentConfig = {
template: '<div data-bind="text: 123">I have been prepopulated and not bound yet</div>',
viewModel: {
createViewModel: function(params, componentInfo) {
expect(componentInfo.element).toContainText('I have been prepopulated and not bound yet');
expect(params).toBe(testComponentParams);
expect(componentInfo.templateNodes.length).toEqual(3);
expect(componentInfo.templateNodes[0]).toContainText('Here are some ');
expect(componentInfo.templateNodes[1]).toContainText('template');
expect(componentInfo.templateNodes[2]).toContainText(' nodes');
expect(componentInfo.templateNodes[1].tagName.toLowerCase()).toEqual('em');

//verify that createViewModel is the same function and was called with the component definition as the context
expect(this.createViewModel).toBe(componentConfig.viewModel.createViewModel);
Expand All @@ -89,6 +94,7 @@ describe('Components: Component binding', function() {
}
}
};
testNode.innerHTML = '<div data-bind="component: testComponentBindingValue">Here are some <em>template</em> nodes</div>';

ko.components.register(testComponentName, componentConfig);
ko.applyBindings(outerViewModel, testNode);
Expand Down
56 changes: 56 additions & 0 deletions spec/components/customElementBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,60 @@ describe('Components: Custom elements', function() {
throw ex;
}
});

it('Is possible to set up components that receive, inject, and bind templates supplied by the user of the component (sometimes called "templated components" or "transclusion")', function() {
// This spec repeats assertions made in other specs elsewhere, but is useful to prove the end-to-end technique

this.after(function() {
ko.components.unregister('special-list');
});

// First define a reusable 'special-list' component that produces a <ul> in which the <li>s have special CSS classes
// It also injects and binds a supplied template for each list item
ko.components.register('special-list', {
template: '<ul class="my-special-list" data-bind="foreach: specialListItems">'
+ '<li class="special-list-item" data-bind="template: { nodes: $parent.suppliedItemTemplate }">'
+ '</li>'
+ '</ul>',
viewModel: {
createViewModel: function(params, componentInfo) {
return {
specialListItems: params.items,
suppliedItemTemplate: componentInfo.templateNodes
};
}
}
});

// Now make some view markup that uses <special-list> and supplies a template to be used inside each list item
testNode.innerHTML = '<h1>Cheeses</h1>'
+ '<special-list params="items: cheeses">'
+ '<em data-bind="text: name"></em> has quality <em data-bind="text: quality"></em>'
+ '</special-list>';

// Finally, bind it all to some data
ko.applyBindings({
cheeses: [
{ name: 'brie', quality: 7 },
{ name: 'cheddar', quality: 9 },
{ name: 'roquefort', quality: 3 }
]
}, testNode);

jasmine.Clock.tick(1);
expect(testNode.childNodes[0]).toContainText('Cheeses');
expect(testNode.childNodes[1].childNodes[0].tagName.toLowerCase()).toEqual('ul');
expect(testNode.childNodes[1].childNodes[0].className).toEqual('my-special-list');
expect(testNode.childNodes[1].childNodes[0]).toContainHtml(
'<li class="special-list-item" data-bind="template: { nodes: $parent.supplieditemtemplate }">'
+ '<em data-bind="text: name">brie</em> has quality <em data-bind="text: quality">7</em>'
+ '</li>'
+ '<li class="special-list-item" data-bind="template: { nodes: $parent.supplieditemtemplate }">'
+ '<em data-bind="text: name">cheddar</em> has quality <em data-bind="text: quality">9</em>'
+ '</li>'
+ '<li class="special-list-item" data-bind="template: { nodes: $parent.supplieditemtemplate }">'
+ '<em data-bind="text: name">roquefort</em> has quality <em data-bind="text: quality">3</em>'
+ '</li>'
);
});
});
2 changes: 1 addition & 1 deletion spec/templatingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ describe('Templating', function() {

it('Should not allow "nodes: someObservableArray"', function() {
// See comment in implementation for reasoning
testNode.innerHTML = "<div data-bind='template: { nodes: myNodes, bypassDomNodeWrap: true }'>Should not use this inline template</div>";
testNode.innerHTML = "<div data-bind='template: { nodes: myNodes, bypassDomNodeWrap: true }'>Should not use this inline template</div>";
expect(function() {
ko.applyBindings({ myNodes: ko.observableArray() }, testNode);
}).toThrowContaining("The \"nodes\" option must be a plain, non-observable array");
Expand Down
9 changes: 5 additions & 4 deletions src/components/componentBinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
};
},
originalChildNodes = ko.utils.makeArray(ko.virtualElements.childNodes(element));

ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);

Expand Down Expand Up @@ -48,7 +49,7 @@
throw new Error('Unknown component \'' + componentName + '\'');
}
cloneTemplateIntoElement(componentName, componentDefinition, element);
var componentViewModel = createViewModel(componentDefinition, element, componentParams),
var componentViewModel = createViewModel(componentDefinition, element, originalChildNodes, componentParams),
childBindingContext = bindingContext['createChildContext'](componentViewModel);
currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element);
Expand All @@ -71,10 +72,10 @@
ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
}

function createViewModel(componentDefinition, element, componentParams) {
function createViewModel(componentDefinition, element, originalChildNodes, componentParams) {
var componentViewModelFactory = componentDefinition['createViewModel'];
return componentViewModelFactory
? componentViewModelFactory.call(componentDefinition, componentParams, { element: element })
? componentViewModelFactory.call(componentDefinition, componentParams, { element: element, templateNodes: originalChildNodes })
: componentParams; // Template-only component
}

Expand Down

0 comments on commit 98c55ee

Please sign in to comment.