From a32060fb1d2985dcdd26c6778a68a88c9bf755b3 Mon Sep 17 00:00:00 2001 From: Steven Sanderson Date: Tue, 7 Oct 2014 10:55:16 +0100 Subject: [PATCH 1/4] Enhance template binding to accept a "nodes" array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When injecting supplied nodes into a component output, it’s useful to be able to pass the node array directly from the component viewmodel and bind to those nodes in the view. If we didn’t have this feature, you’d have to dynamically create a named template or something, which would be awful. --- spec/templatingBehaviors.js | 72 ++++++++++++++++++++++++++++++++++++ src/templating/templating.js | 11 ++++++ 2 files changed, 83 insertions(+) diff --git a/spec/templatingBehaviors.js b/spec/templatingBehaviors.js index 431d4ea6c..2c75d272c 100644 --- a/spec/templatingBehaviors.js +++ b/spec/templatingBehaviors.js @@ -473,6 +473,78 @@ describe('Templating', function() { expect(testNode.childNodes[0]).toContainHtml("outer
inner via inline binding: 1inner via external binding: 2
"); }); + it('Should accept a "nodes" option that gives the template nodes', function() { + // This is an alternative to specifying a named template, and is useful in conjunction with components + ko.setTemplateEngine(new dummyTemplateEngine({ + innerTemplate: "the name is [js: name()]" // See that custom template engines are applied to the injected nodes + })); + + testNode.innerHTML = "
"; + var model = { + testNodes: [ + document.createTextNode("begin"), + document.createElement("div"), + document.createTextNode("end") + ], + testData: { name: ko.observable("alpha") } + }; + model.testNodes[1].setAttribute("data-bind", "template: 'innerTemplate'"); // See that bindings are applied to the injected nodes + + ko.applyBindings(model, testNode); + expect(testNode.childNodes[0]).toContainHtml("begin
the name is alpha
end"); + + // The injected bindings update to match model changes as usual + model.testData.name("beta"); + expect(testNode.childNodes[0]).toContainHtml("begin
the name is beta
end"); + }); + + it('Should accept a "nodes" option that gives the template nodes, and it can be used in conjunction with "foreach"', function() { + testNode.innerHTML = "
"; + + // This time we'll check that the nodes array doesn't have to be a real array - it can be the .childNodes + // property of a DOM element, which is subtly different. + var templateContainer = document.createElement("div"); + templateContainer.innerHTML = "[
]"; + var model = { + testNodes: templateContainer.childNodes, + testData: ko.observableArray([{ name: ko.observable("alpha") }, { name: "beta" }, { name: "gamma" }]) + }; + model.testNodes[1].setAttribute("data-bind", "text: name"); + + ko.applyBindings(model, testNode); + expect(testNode.childNodes[0]).toContainText("[alpha][beta][gamma]"); + + // The injected bindings update to match model changes as usual + model.testData.splice(1, 1); + expect(testNode.childNodes[0]).toContainText("[alpha][gamma]"); + + // Changing the nodes array does *not* affect subsequent output from the template. + // This behavior may be subject to change. I'm adding this assertion just to record what + // the current behavior is, even if we might want to alter it in the future. We don't need + // to document or make any guarantees about what happens if you do this - it's just not + // a supported thing to do. + templateContainer.innerHTML = "[Modified, but will not appear in template output because the nodes were already cloned]"; + model.testData.splice(1, 0, { name: "delta" }); + expect(testNode.childNodes[0]).toContainText("[alpha][delta][gamma]"); + }); + + it('Should interpret "nodes: anyFalseyValue" as being equivalent to supplying an empty node array', function() { + // This behavior helps to avoid inconsistency if you're programmatically supplying a node array + // but sometimes you might not have any nodes - you don't want the template binding to dynamically + // switch over to "inline template" mode just because your 'nodes' value is null, for example. + testNode.innerHTML = "
Should not use this inline template
"; + ko.applyBindings(null, testNode); + expect(testNode.childNodes[0]).toContainHtml(''); + }); + + it('Should not allow "nodes: someObservableArray"', function() { + // See comment in implementation for reasoning + testNode.innerHTML = "
Should not use this inline template
"; + expect(function() { + ko.applyBindings({ myNodes: ko.observableArray() }, testNode); + }).toThrowContaining("The \"nodes\" option must be a plain, non-observable array"); + }); + describe('Data binding \'foreach\' option', function() { it('Should remove existing content', function () { ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "template content" })); diff --git a/src/templating/templating.js b/src/templating/templating.js index b54229cb4..8c7183eb3 100644 --- a/src/templating/templating.js +++ b/src/templating/templating.js @@ -224,6 +224,17 @@ if (typeof bindingValue == "string" || bindingValue['name']) { // It's a named template - clear the element ko.virtualElements.emptyNode(element); + } else if ('nodes' in bindingValue) { + // We've been given an array of DOM nodes. Save them as the template source. + // There is no known use case for the node array being an observable array (if the output + // varies, put that behavior *into* your template - that's what templates are for), and + // the implementation would be a mess, so assert that it's not observable. + var nodes = bindingValue['nodes'] || []; + if (ko.isObservable(nodes)) { + throw new Error('The "nodes" option must be a plain, non-observable array.'); + } + var container = ko.utils.moveCleanedNodesToContainerElement(nodes); // This also removes the nodes from their current parent + new ko.templateSources.anonymousTemplate(element)['nodes'](container); } else { // It's an anonymous template - store the element contents, then clear the element var templateNodes = ko.virtualElements.childNodes(element), From 98c55eef00eee07943de2d8552aa1eb784df4643 Mon Sep 17 00:00:00 2001 From: Steven Sanderson Date: Tue, 7 Oct 2014 11:30:22 +0100 Subject: [PATCH 2/4] Supply 'templateNodes' to 'componentInfo' so that components can accept inline templates --- spec/components/componentBindingBehaviors.js | 8 ++- spec/components/customElementBehaviors.js | 56 ++++++++++++++++++++ spec/templatingBehaviors.js | 2 +- src/components/componentBinding.js | 9 ++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/spec/components/componentBindingBehaviors.js b/spec/components/componentBindingBehaviors.js index 2b5fbb366..e6d291611 100644 --- a/spec/components/componentBindingBehaviors.js +++ b/spec/components/componentBindingBehaviors.js @@ -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: '
I have been prepopulated and not bound yet
', 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); @@ -89,6 +94,7 @@ describe('Components: Component binding', function() { } } }; + testNode.innerHTML = '
Here are some template nodes
'; ko.components.register(testComponentName, componentConfig); ko.applyBindings(outerViewModel, testNode); diff --git a/spec/components/customElementBehaviors.js b/spec/components/customElementBehaviors.js index fede45b41..8890cf9d6 100644 --- a/spec/components/customElementBehaviors.js +++ b/spec/components/customElementBehaviors.js @@ -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