2 var dummyTemplateEngine = function (templates) {
3 var inMemoryTemplates = templates || {};
4 var inMemoryTemplateData = {};
6 function dummyTemplateSource(id) {
9 dummyTemplateSource.prototype = {
11 if (arguments.length >= 1)
12 inMemoryTemplates[this.id] = val;
13 return inMemoryTemplates[this.id];
15 data: function(key, val) {
16 if (arguments.length >= 2) {
17 inMemoryTemplateData[this.id] = inMemoryTemplateData[this.id] || {};
18 inMemoryTemplateData[this.id][key] = val;
20 return (inMemoryTemplateData[this.id] || {})[key];
24 this.makeTemplateSource = function(template) {
25 if (typeof template == "string")
26 return new dummyTemplateSource(template); // Named template comes from the in-memory collection
27 else if ((template.nodeType == 1) || (template.nodeType == 8))
28 return new ko.templateSources.anonymousTemplate(template); // Anonymous template
31 this.renderTemplateSource = function (templateSource, bindingContext, options) {
32 var data = bindingContext['$data'];
33 options = options || {};
34 var templateText = templateSource.text();
35 if (typeof templateText == "function")
36 templateText = templateText(data, options);
38 templateText = options.showParams ? templateText + ", data=" + data + ", options=" + options : templateText;
39 var templateOptions = options.templateOptions; // Have templateOptions in scope to support [js:templateOptions.foo] syntax
42 with (bindingContext) {
44 with (options.templateRenderingVariablesInScope || {}) {
45 // Dummy [renderTemplate:...] syntax
46 result = templateText.replace(/\[renderTemplate\:(.*?)\]/g, function (match, templateName) {
47 return ko.renderTemplate(templateName, data, options);
51 var evalHandler = function (match, script) {
53 var evalResult = eval(script);
54 return (evalResult === null) || (evalResult === undefined) ? "" : evalResult.toString();
56 throw new Error("Error evaluating script: [js: " + script + "]\n\nException: " + ex.toString());
60 // Dummy [[js:...]] syntax (in case you need to use square brackets inside the expression)
61 result = result.replace(/\[\[js\:([\s\S]*?)\]\]/g, evalHandler);
63 // Dummy [js:...] syntax
64 result = result.replace(/\[js\:([\s\S]*?)\]/g, evalHandler);
69 // Use same HTML parsing code as real template engine so as to trigger same combination of IE weirdnesses
70 // Also ensure resulting nodelist is an array to mimic what the default templating engine does, so we see the effects of not being able to remove dead memo comment nodes.
71 return ko.utils.arrayPushAll([], ko.utils.parseHtmlFragment(result));
74 this.rewriteTemplate = function (template, rewriterCallback) {
75 // Only rewrite if the template isn't a function (can't rewrite those)
76 var templateSource = this.makeTemplateSource(template);
77 if (typeof templateSource.text() != "function")
78 return ko.templateEngine.prototype.rewriteTemplate.call(this, template, rewriterCallback);
80 this.createJavaScriptEvaluatorBlock = function (script) { return "[js:" + script + "]"; };
82 dummyTemplateEngine.prototype = new ko.templateEngine();
84 describe('Templating', {
85 before_each: function () {
86 ko.setTemplateEngine(new ko.nativeTemplateEngine());
87 var existingNode = document.getElementById("testNode");
88 if (existingNode != null)
89 existingNode.parentNode.removeChild(existingNode);
90 testNode = document.createElement("div");
91 testNode.id = "testNode";
92 document.body.appendChild(testNode);
95 'Template engines can return an array of DOM nodes': function () {
96 ko.setTemplateEngine(new dummyTemplateEngine({ x: [document.createElement("div"), document.createElement("span")] }));
97 ko.renderTemplate("x", null);
100 'Should not be able to render a template until a template engine is provided': function () {
102 ko.setTemplateEngine(undefined);
103 try { ko.renderTemplate("someTemplate", {}) }
104 catch (ex) { threw = true }
105 value_of(threw).should_be(true);
108 'Should be able to render a template into a given DOM element': function () {
109 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" }));
110 ko.renderTemplate("someTemplate", null, null, testNode);
111 value_of(testNode.childNodes.length).should_be(1);
112 value_of(testNode.innerHTML).should_be("ABC");
115 'Should be able to access newly rendered/inserted elements in \'afterRender\' callaback': function () {
116 var passedElement, passedDataItem;
117 var myCallback = function(elementsArray, dataItem) {
118 value_of(elementsArray.length).should_be(1);
119 passedElement = elementsArray[0];
120 passedDataItem = dataItem;
123 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" }));
124 ko.renderTemplate("someTemplate", myModel, { afterRender: myCallback }, testNode);
125 value_of(passedElement.nodeValue).should_be("ABC");
126 value_of(passedDataItem).should_be(myModel);
129 'Should automatically rerender into DOM element when dependencies change': function () {
130 var dependency = new ko.observable("A");
131 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: function () {
132 return "Value = " + dependency();
136 ko.renderTemplate("someTemplate", null, null, testNode);
137 value_of(testNode.childNodes.length).should_be(1);
138 value_of(testNode.innerHTML).should_be("Value = A");
141 value_of(testNode.childNodes.length).should_be(1);
142 value_of(testNode.innerHTML).should_be("Value = B");
145 'Should not rerender DOM element if observable accessed in \'afterRender\' callaback is changed': function () {
146 var observable = new ko.observable("A"), count = 0;
147 var myCallback = function(elementsArray, dataItem) {
148 observable(); // access observable in callback
150 var myTemplate = function() {
151 return "Value = " + (++count);
153 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: myTemplate }));
154 ko.renderTemplate("someTemplate", {}, { afterRender: myCallback }, testNode);
155 value_of(testNode.childNodes.length).should_be(1);
156 value_of(testNode.innerHTML).should_be("Value = 1");
159 value_of(testNode.childNodes.length).should_be(1);
160 value_of(testNode.innerHTML).should_be("Value = 1");
163 'If the supplied data item is observable, evaluates it and has subscription on it': function () {
164 var observable = new ko.observable("A");
165 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: function (data) {
166 return "Value = " + data;
169 ko.renderTemplate("someTemplate", observable, null, testNode);
170 value_of(testNode.innerHTML).should_be("Value = A");
173 value_of(testNode.innerHTML).should_be("Value = B");
176 'Should stop updating DOM nodes when the dependency next changes if the DOM node has been removed from the document': function () {
177 var dependency = new ko.observable("A");
178 var template = { someTemplate: function () { return "Value = " + dependency() } };
179 ko.setTemplateEngine(new dummyTemplateEngine(template));
181 ko.renderTemplate("someTemplate", null, null, testNode);
182 value_of(testNode.childNodes.length).should_be(1);
183 value_of(testNode.innerHTML).should_be("Value = A");
185 testNode.parentNode.removeChild(testNode);
187 value_of(testNode.childNodes.length).should_be(1);
188 value_of(testNode.innerHTML).should_be("Value = A");
191 'Should be able to render a template using data-bind syntax': function () {
192 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "template output" }));
193 testNode.innerHTML = "<div data-bind='template:\"someTemplate\"'></div>";
194 ko.applyBindings(null, testNode);
195 value_of(testNode.childNodes[0].innerHTML).should_be("template output");
198 'Should be able to tell data-bind syntax which object to pass as data for the template (otherwise, uses viewModel)': function () {
199 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
200 testNode.innerHTML = "<div data-bind='template: { name: \"someTemplate\", data: someProp }'></div>";
201 ko.applyBindings({ someProp: { childProp: 123} }, testNode);
202 value_of(testNode.childNodes[0].innerHTML).should_be("result = 123");
205 'Should re-render a named template when its data item notifies about mutation': function () {
206 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
207 testNode.innerHTML = "<div data-bind='template: { name: \"someTemplate\", data: someProp }'></div>";
209 var myData = ko.observable({ childProp: 123 });
210 ko.applyBindings({ someProp: myData }, testNode);
211 value_of(testNode.childNodes[0].innerHTML).should_be("result = 123");
213 // Now mutate and notify
214 myData().childProp = 456;
215 myData.valueHasMutated();
216 value_of(testNode.childNodes[0].innerHTML).should_be("result = 456");
219 'Should stop tracking inner observables immediately when the container node is removed from the document': function() {
220 var innerObservable = ko.observable("some value");
221 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp()]" }));
222 testNode.innerHTML = "<div data-bind='template: { name: \"someTemplate\", data: someProp }'></div>";
223 ko.applyBindings({ someProp: { childProp: innerObservable} }, testNode);
225 value_of(innerObservable.getSubscriptionsCount()).should_be(1);
226 ko.removeNode(testNode.childNodes[0]);
227 value_of(innerObservable.getSubscriptionsCount()).should_be(0);
230 'Should be able to pick template via an observable model property': function () {
231 ko.setTemplateEngine(new dummyTemplateEngine({
232 firstTemplate: "First template output",
233 secondTemplate: "Second template output"
236 var chosenTemplate = ko.observable("firstTemplate");
237 testNode.innerHTML = "<div data-bind='template: chosenTemplate'></div>";
238 ko.applyBindings({ chosenTemplate: chosenTemplate }, testNode);
239 value_of(testNode.childNodes[0].innerHTML).should_be("First template output");
241 chosenTemplate("secondTemplate");
242 value_of(testNode.childNodes[0].innerHTML).should_be("Second template output");
245 'Should be able to pick template as a function of the data item using data-bind syntax, with the binding context available as a second parameter': function () {
246 var templatePicker = function(dataItem, bindingContext) {
247 // Having the entire binding context available means you can read sibling or parent level properties
248 value_of(bindingContext.$parent.anotherProperty).should_be(456);
249 return dataItem.myTemplate;
251 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
252 testNode.innerHTML = "<div data-bind='template: { name: templateSelectorFunction, data: someProp }'></div>";
253 ko.applyBindings({ someProp: { childProp: 123, myTemplate: "someTemplate" }, templateSelectorFunction: templatePicker, anotherProperty: 456 }, testNode);
254 value_of(testNode.childNodes[0].innerHTML).should_be("result = 123");
257 'Should be able to chain templates, rendering one from inside another': function () {
258 ko.setTemplateEngine(new dummyTemplateEngine({
259 outerTemplate: "outer template output, [renderTemplate:innerTemplate]", // [renderTemplate:...] is special syntax supported by dummy template engine
260 innerTemplate: "inner template output <span data-bind='text: 123'></span>"
262 testNode.innerHTML = "<div data-bind='template:\"outerTemplate\"'></div>";
263 ko.applyBindings(null, testNode);
264 value_of(testNode.childNodes[0]).should_contain_html("outer template output, inner template output <span>123</span>");
267 'Should rerender chained templates when their dependencies change, without rerendering parent templates': function () {
268 var observable = new ko.observable("ABC");
269 var timesRenderedOuter = 0, timesRenderedInner = 0;
270 ko.setTemplateEngine(new dummyTemplateEngine({
271 outerTemplate: function () { timesRenderedOuter++; return "outer template output, [renderTemplate:innerTemplate]" }, // [renderTemplate:...] is special syntax supported by dummy template engine
272 innerTemplate: function () { timesRenderedInner++; return observable() }
274 testNode.innerHTML = "<div data-bind='template:\"outerTemplate\"'></div>";
275 ko.applyBindings(null, testNode);
276 value_of(testNode.childNodes[0]).should_contain_html("outer template output, abc");
277 value_of(timesRenderedOuter).should_be(1);
278 value_of(timesRenderedInner).should_be(1);
281 value_of(testNode.childNodes[0]).should_contain_html("outer template output, def");
282 value_of(timesRenderedOuter).should_be(1);
283 value_of(timesRenderedInner).should_be(2);
286 'Should stop tracking inner observables referenced by a chained template as soon as the chained template output node is removed from the document': function() {
287 var innerObservable = ko.observable("some value");
288 ko.setTemplateEngine(new dummyTemplateEngine({
289 outerTemplate: "outer template output, <span id='innerTemplateOutput'>[renderTemplate:innerTemplate]</span>",
290 innerTemplate: "result = [js: childProp()]"
292 testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\", data: someProp }'></div>";
293 ko.applyBindings({ someProp: { childProp: innerObservable} }, testNode);
295 value_of(innerObservable.getSubscriptionsCount()).should_be(1);
296 ko.removeNode(document.getElementById('innerTemplateOutput'));
297 value_of(innerObservable.getSubscriptionsCount()).should_be(0);
300 'Should handle data-bind attributes from inside templates, regardless of element and attribute casing': function () {
301 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<INPUT Data-Bind='value:\"Hi\"' />" }));
302 ko.renderTemplate("someTemplate", null, null, testNode);
303 value_of(testNode.childNodes[0].value).should_be("Hi");
306 'Should handle data-bind attributes that include newlines from inside templates': function () {
307 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<input data-bind='value:\n\"Hi\"' />" }));
308 ko.renderTemplate("someTemplate", null, null, testNode);
309 value_of(testNode.childNodes[0].value).should_be("Hi");
312 'Data binding syntax should be able to reference variables put into scope by the template engine': function () {
313 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<input data-bind='value:message' />" }));
314 ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { message: "hello"} }, testNode);
315 value_of(testNode.childNodes[0].value).should_be("hello");
318 'Data binding syntax should be able to use $element in binding value': function() {
319 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<div data-bind='text: $element.tagName'></div>" }));
320 ko.renderTemplate("someTemplate", null, null, testNode);
321 value_of(testNode.childNodes[0]).should_contain_text("DIV");
324 'Data binding syntax should be able to use $context in binding value to refer to the context object': function() {
325 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<div data-bind='text: $context.$data === $data'></div>" }));
326 ko.renderTemplate("someTemplate", {}, null, testNode);
327 value_of(testNode.childNodes[0]).should_contain_text("true");
330 'Data binding syntax should defer evaluation of variables until the end of template rendering (so bindings can take independent subscriptions to them)': function () {
331 ko.setTemplateEngine(new dummyTemplateEngine({
332 someTemplate: "<input data-bind='value:message' />[js: message = 'goodbye'; undefined; ]"
334 ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { message: "hello"} }, testNode);
335 value_of(testNode.childNodes[0].value).should_be("goodbye");
338 'Data binding syntax should use the template\'s \'data\' object as the viewModel value (so \'this\' is set correctly when calling click handlers etc.)': function() {
339 ko.setTemplateEngine(new dummyTemplateEngine({
340 someTemplate: "<button data-bind='click: someFunctionOnModel'>click me</button>"
343 didCallMyFunction : false,
344 someFunctionOnModel : function() { this.didCallMyFunction = true }
346 ko.renderTemplate("someTemplate", viewModel, null, testNode);
347 var buttonNode = testNode.childNodes[0];
348 value_of(buttonNode.tagName).should_be("BUTTON"); // Be sure we're clicking the right thing
350 value_of(viewModel.didCallMyFunction).should_be(true);
353 'Data binding syntax should permit nested templates, and only bind inner templates once': function() {
354 // Will verify that bindings are applied only once for both inline (rewritten) bindings,
355 // and external (non-rewritten) ones
356 var originalBindingProvider = ko.bindingProvider.instance;
357 ko.bindingProvider.instance = {
358 nodeHasBindings: function(node, bindingContext) {
359 return (node.tagName == 'EM') || originalBindingProvider.nodeHasBindings(node, bindingContext);
361 getBindings: function(node, bindingContext) {
362 if (node.tagName == 'EM')
363 return { text: ++model.numBindings };
364 return originalBindingProvider.getBindings(node, bindingContext);
368 ko.setTemplateEngine(new dummyTemplateEngine({
369 outerTemplate: "Outer <div data-bind='template: { name: \"innerTemplate\", bypassDomNodeWrap: true }'></div>",
370 innerTemplate: "Inner via inline binding: <span data-bind='text: ++numBindings'></span>"
371 + "Inner via external binding: <em></em>"
373 var model = { numBindings: 0 };
374 testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\", bypassDomNodeWrap: true }'></div>";
375 ko.applyBindings(model, testNode);
376 value_of(model.numBindings).should_be(2);
377 value_of(testNode.childNodes[0]).should_contain_html("outer <div>inner via inline binding: <span>2</span>inner via external binding: <em>1</em></div>");
379 ko.bindingProvider.instance = originalBindingProvider;
382 'Data binding syntax should support \'foreach\' option, whereby it renders for each item in an array but doesn\'t rerender everything if you push or splice': function () {
383 var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
384 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>The item is [js: personName]</div>" }));
385 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
387 ko.applyBindings({ myCollection: myArray }, testNode);
388 value_of(testNode.childNodes[0]).should_contain_html("<div>the item is bob</div><div>the item is frank</div>");
389 var originalBobNode = testNode.childNodes[0].childNodes[0];
390 var originalFrankNode = testNode.childNodes[0].childNodes[1];
392 myArray.push({ personName: "Steve" });
393 value_of(testNode.childNodes[0]).should_contain_html("<div>the item is bob</div><div>the item is frank</div><div>the item is steve</div>");
394 value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode);
395 value_of(testNode.childNodes[0].childNodes[1]).should_be(originalFrankNode);
398 'Data binding \'foreach\' option should apply bindings within the context of each item in the array': function () {
399 var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
400 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is <span data-bind='text: personName'></span>" }));
401 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
403 ko.applyBindings({ myCollection: myArray }, testNode);
404 value_of(testNode.childNodes[0]).should_contain_html("the item is <span>bob</span>the item is <span>frank</span>");
407 'Data binding \'foreach\' options should only bind each group of output nodes once': function() {
409 ko.bindingHandlers.countInits = { init: function() { initCalls++ } };
410 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<span data-bind='countInits: true'></span>" }));
411 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
413 ko.applyBindings({ myCollection: [1,2,3] }, testNode);
414 value_of(initCalls).should_be(3); // 3 because there were 3 items in myCollection
417 'Data binding \'foreach\' should handle templates in which the very first node has a binding': function() {
418 // Represents https://github.com/SteveSanderson/knockout/pull/440
419 // Previously, the rewriting (which introduces a comment node before the bound node) was interfering
420 // with the array-to-DOM-node mapping state tracking
421 ko.setTemplateEngine(new dummyTemplateEngine({ mytemplate: "<div data-bind='text: $data'></div>" }));
422 testNode.innerHTML = "<div data-bind=\"template: { name: 'mytemplate', foreach: items }\"></div>";
424 // Bind against initial array containing one entry. UI just shows "original"
425 var myArray = ko.observableArray(["original"]);
426 ko.applyBindings({ items: myArray });
427 value_of(testNode.childNodes[0]).should_contain_html("<div>original</div>");
429 // Now replace the entire array contents with one different entry.
430 // UI just shows "new" (previously with bug, showed "original" AND "new")
432 value_of(testNode.childNodes[0]).should_contain_html("<div>new</div>");
435 'Data binding \'foreach\' should handle chained templates in which the very first node has a binding': function() {
436 // See https://github.com/SteveSanderson/knockout/pull/440 and https://github.com/SteveSanderson/knockout/pull/144
437 ko.setTemplateEngine(new dummyTemplateEngine({
438 outerTemplate: "<div data-bind='text: $data'></div>[renderTemplate:innerTemplate]x", // [renderTemplate:...] is special syntax supported by dummy template engine
439 innerTemplate: "inner <span data-bind='text: 123'></span>"
441 testNode.innerHTML = "<div data-bind=\"template: { name: 'outerTemplate', foreach: items }\"></div>";
443 // Bind against initial array containing one entry.
444 var myArray = ko.observableArray(["original"]);
445 ko.applyBindings({ items: myArray });
446 value_of(testNode.childNodes[0]).should_contain_html("<div>original</div>inner <span>123</span>x");
448 // Now replace the entire array contents with one different entry.
450 value_of(testNode.childNodes[0]).should_contain_html("<div>new</div>inner <span>123</span>x");
453 'Data binding \'foreach\' option should apply bindings with an $index in the context': function () {
454 var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
455 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item # is <span data-bind='text: $index'></span>" }));
456 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
458 ko.applyBindings({ myCollection: myArray }, testNode);
459 value_of(testNode.childNodes[0]).should_contain_html("the item # is <span>0</span>the item # is <span>1</span>");
462 'Data binding \'foreach\' option should update bindings that reference an $index if the list changes': function () {
463 var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
464 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item <span data-bind='text: personName'></span>is <span data-bind='text: $index'></span>" }));
465 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
467 ko.applyBindings({ myCollection: myArray }, testNode);
468 value_of(testNode.childNodes[0]).should_contain_html("the item <span>bob</span>is <span>0</span>the item <span>frank</span>is <span>1</span>");
470 var frank = myArray.pop(); // remove frank
471 value_of(testNode.childNodes[0]).should_contain_html("the item <span>bob</span>is <span>0</span>");
473 myArray.unshift(frank); // put frank in the front
474 value_of(testNode.childNodes[0]).should_contain_html("the item <span>frank</span>is <span>0</span>the item <span>bob</span>is <span>1</span>");
478 'Data binding \'foreach\' option should accept array with "undefined" and "null" items': function () {
479 var myArray = new ko.observableArray([undefined, null]);
480 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is <span data-bind='text: String($data)'></span>" }));
481 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
483 ko.applyBindings({ myCollection: myArray }, testNode);
484 value_of(testNode.childNodes[0]).should_contain_html("the item is <span>undefined</span>the item is <span>null</span>");
487 'Data binding \'foreach\' option should update DOM nodes when a dependency of their mapping function changes': function() {
488 var myObservable = new ko.observable("Steve");
489 var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: myObservable }, { personName: "Another" }]);
490 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>The item is [js: ko.utils.unwrapObservable(personName)]</div>" }));
491 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
493 ko.applyBindings({ myCollection: myArray }, testNode);
494 value_of(testNode.childNodes[0]).should_contain_html("<div>the item is bob</div><div>the item is steve</div><div>the item is another</div>");
495 var originalBobNode = testNode.childNodes[0].childNodes[0];
497 myObservable("Steve2");
498 value_of(testNode.childNodes[0]).should_contain_html("<div>the item is bob</div><div>the item is steve2</div><div>the item is another</div>");
499 value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode);
501 // Ensure we can still remove the corresponding nodes (even though they've changed), and that doing so causes the subscription to be disposed
502 value_of(myObservable.getSubscriptionsCount()).should_be(1);
503 myArray.splice(1, 1);
504 value_of(testNode.childNodes[0]).should_contain_html("<div>the item is bob</div><div>the item is another</div>");
505 myObservable("Something else"); // Re-evaluating the observable causes the orphaned subscriptions to be disposed
506 value_of(myObservable.getSubscriptionsCount()).should_be(0);
509 'Data binding \'foreach\' option should treat a null parameter as meaning \'no items\'': function() {
510 var myArray = new ko.observableArray(["A", "B"]);
511 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "hello" }));
512 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
514 ko.applyBindings({ myCollection: myArray }, testNode);
515 value_of(testNode.childNodes[0].childNodes.length).should_be(2);
517 // Now set the observable to null and check it's treated like an empty array
518 // (because how else should null be interpreted?)
520 value_of(testNode.childNodes[0].childNodes.length).should_be(0);
523 'Data binding \'foreach\' option should accept an \"as\" option to define an alias for the iteration variable': function() {
524 // Note: There are more detailed specs (e.g., covering nesting) associated with the "foreach" binding which
525 // uses this templating functionality internally.
526 var myArray = new ko.observableArray(["A", "B"]);
527 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "[js:myAliasedItem]" }));
528 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection, as: \"myAliasedItem\" }'></div>";
530 ko.applyBindings({ myCollection: myArray }, testNode);
531 value_of(testNode.childNodes[0]).should_contain_text("AB");
534 'Data binding \'foreach\' option should stop tracking inner observables when the container node is removed': function() {
535 var innerObservable = ko.observable("some value");
536 var myArray = new ko.observableArray([{obsVal:innerObservable}, {obsVal:innerObservable}]);
537 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is [js: ko.utils.unwrapObservable(obsVal)]" }));
538 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
540 ko.applyBindings({ myCollection: myArray }, testNode);
541 value_of(innerObservable.getSubscriptionsCount()).should_be(2);
543 ko.removeNode(testNode.childNodes[0]);
544 value_of(innerObservable.getSubscriptionsCount()).should_be(0);
547 'Data binding \'foreach\' option should stop tracking inner observables related to each array item when that array item is removed': function() {
548 var innerObservable = ko.observable("some value");
549 var myArray = new ko.observableArray([{obsVal:innerObservable}, {obsVal:innerObservable}]);
550 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is [js: ko.utils.unwrapObservable(obsVal)]" }));
551 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
553 ko.applyBindings({ myCollection: myArray }, testNode);
554 value_of(innerObservable.getSubscriptionsCount()).should_be(2);
556 myArray.splice(1, 1);
557 value_of(innerObservable.getSubscriptionsCount()).should_be(1);
559 value_of(innerObservable.getSubscriptionsCount()).should_be(0);
562 'Data binding syntax should omit any items whose \'_destroy\' flag is set (unwrapping the flag if it is observable)' : function() {
563 var myArray = new ko.observableArray([{ someProp: 1 }, { someProp: 2, _destroy: 'evals to true' }, { someProp : 3 }, { someProp: 4, _destroy: ko.observable(false) }]);
564 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>someProp=[js: someProp]</div>" }));
565 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
567 ko.applyBindings({ myCollection: myArray }, testNode);
568 value_of(testNode.childNodes[0]).should_contain_html("<div>someprop=1</div><div>someprop=3</div><div>someprop=4</div>");
571 'Data binding syntax should include any items whose \'_destroy\' flag is set if you use includeDestroyed' : function() {
572 var myArray = new ko.observableArray([{ someProp: 1 }, { someProp: 2, _destroy: 'evals to true' }, { someProp : 3 }]);
573 ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>someProp=[js: someProp]</div>" }));
574 testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection, includeDestroyed: true }'></div>";
576 ko.applyBindings({ myCollection: myArray }, testNode);
577 value_of(testNode.childNodes[0]).should_contain_html("<div>someprop=1</div><div>someprop=2</div><div>someprop=3</div>");
580 'Data binding syntax should support \"if\" condition' : function() {
581 ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Value: [js: myProp().childProp]" }));
582 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\", \"if\": myProp }'></div>";
584 var viewModel = { myProp: ko.observable({ childProp: 'abc' }) };
585 ko.applyBindings(viewModel, testNode);
587 // Initially there is a value
588 value_of(testNode.childNodes[0]).should_contain_text("Value: abc");
590 // Causing the condition to become false causes the output to be removed
591 viewModel.myProp(null);
592 value_of(testNode.childNodes[0]).should_contain_text("");
594 // Causing the condition to become true causes the output to reappear
595 viewModel.myProp({ childProp: 'def' });
596 value_of(testNode.childNodes[0]).should_contain_text("Value: def");
599 'Data binding syntax should support \"ifnot\" condition' : function() {
600 ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Hello" }));
601 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\", ifnot: shouldHide }'></div>";
603 var viewModel = { shouldHide: ko.observable(true) };
604 ko.applyBindings(viewModel, testNode);
606 // Initially there is no output (shouldHide=true)
607 value_of(testNode.childNodes[0]).should_contain_text("");
609 // Causing the condition to become false causes the output to be displayed
610 viewModel.shouldHide(false);
611 value_of(testNode.childNodes[0]).should_contain_text("Hello");
613 // Causing the condition to become true causes the output to disappear
614 viewModel.shouldHide(true);
615 value_of(testNode.childNodes[0]).should_contain_text("");
618 'Data binding syntax should support \"if\" condition in conjunction with foreach': function() {
619 ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Value: [js: myProp().childProp]" }));
620 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\", \"if\": myProp, foreach: [$data, $data, $data] }'></div>";
622 var viewModel = { myProp: ko.observable({ childProp: 'abc' }) };
623 ko.applyBindings(viewModel, testNode);
624 value_of(testNode.childNodes[0].childNodes[0].nodeValue).should_be("Value: abc");
625 value_of(testNode.childNodes[0].childNodes[1].nodeValue).should_be("Value: abc");
626 value_of(testNode.childNodes[0].childNodes[2].nodeValue).should_be("Value: abc");
628 // Causing the condition to become false causes the output to be removed
629 viewModel.myProp(null);
630 value_of(testNode.childNodes[0]).should_contain_text("");
632 // Causing the condition to become true causes the output to reappear
633 viewModel.myProp({ childProp: 'def' });
634 value_of(testNode.childNodes[0].childNodes[0].nodeValue).should_be("Value: def");
635 value_of(testNode.childNodes[0].childNodes[1].nodeValue).should_be("Value: def");
636 value_of(testNode.childNodes[0].childNodes[2].nodeValue).should_be("Value: def");
639 'Should be able to populate checkboxes from inside templates, despite IE6 limitations': function () {
640 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<input type='checkbox' data-bind='checked:isChecked' />" }));
641 ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { isChecked: true } }, testNode);
642 value_of(testNode.childNodes[0].checked).should_be(true);
645 'Should be able to populate radio buttons from inside templates, despite IE6 limitations': function () {
646 ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "<input type='radio' name='somename' value='abc' data-bind='checked:someValue' />" }));
647 ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { someValue: 'abc' } }, testNode);
648 value_of(testNode.childNodes[0].checked).should_be(true);
651 'Should be able to render a different template for each array entry by passing a function as template name, with the array entry\'s binding context available as a second parameter': function() {
652 var myArray = new ko.observableArray([
653 { preferredTemplate: 1, someProperty: 'firstItemValue' },
654 { preferredTemplate: 2, someProperty: 'secondItemValue' }
656 ko.setTemplateEngine(new dummyTemplateEngine({
657 firstTemplate: "<div>Template1Output, [js:someProperty]</div>",
658 secondTemplate: "<div>Template2Output, [js:someProperty]</div>"
660 testNode.innerHTML = "<div data-bind='template: {name: getTemplateModelProperty, foreach: myCollection}'></div>";
662 var getTemplate = function(dataItem, bindingContext) {
663 // Having the item's binding context available means you can read sibling or parent level properties
664 value_of(bindingContext.$parent.anotherProperty).should_be(123);
666 return dataItem.preferredTemplate == 1 ? 'firstTemplate' : 'secondTemplate';
668 ko.applyBindings({ myCollection: myArray, getTemplateModelProperty: getTemplate, anotherProperty: 123 }, testNode);
669 value_of(testNode.childNodes[0]).should_contain_html("<div>template1output, firstitemvalue</div><div>template2output, seconditemvalue</div>");
672 'Data binding \'templateOptions\' should be passed to template': function() {
674 someAdditionalData: { myAdditionalProp: "someAdditionalValue" },
675 people: new ko.observableArray([
680 ko.setTemplateEngine(new dummyTemplateEngine({myTemplate: "<div>Person [js:name] has additional property [js:templateOptions.myAdditionalProp]</div>"}));
681 testNode.innerHTML = "<div data-bind='template: {name: \"myTemplate\", foreach: people, templateOptions: someAdditionalData }'></div>";
683 ko.applyBindings(myModel, testNode);
684 value_of(testNode.childNodes[0]).should_contain_html("<div>person alpha has additional property someadditionalvalue</div><div>person beta has additional property someadditionalvalue</div>");
687 'If the template binding is updated, should dispose any template subscriptions previously associated with the element': function() {
688 var myObservable = ko.observable("some value"),
690 subModel: ko.observable({ myObservable: myObservable })
692 ko.setTemplateEngine(new dummyTemplateEngine({myTemplate: "<span>The value is [js:myObservable()]</span>"}));
693 testNode.innerHTML = "<div data-bind='template: {name: \"myTemplate\", data: subModel}'></div>";
694 ko.applyBindings(myModel, testNode);
696 // Right now the template references myObservable, so there should be exactly one subscription on it
697 value_of(testNode.childNodes[0]).should_contain_text("The value is some value");
698 value_of(myObservable.getSubscriptionsCount()).should_be(1);
699 var renderedNode1 = testNode.childNodes[0].childNodes[0];
701 // By changing the object for subModel, we force the data-bind value to be re-evaluated and the template to be re-rendered,
702 // setting up a new template subscription, so there have now existed two subscriptions on myObservable...
703 myModel.subModel({ myObservable: myObservable });
704 value_of(testNode.childNodes[0].childNodes[0]).should_not_be(renderedNode1);
706 // ...but, because the old subscription should have been disposed automatically, there should only be one left
707 value_of(myObservable.getSubscriptionsCount()).should_be(1);
710 'Should be able to specify a template engine instance using data-bind syntax': function() {
711 ko.setTemplateEngine(new dummyTemplateEngine({ theTemplate: "Default output" })); // Not going to use this one
712 var alternativeTemplateEngine = new dummyTemplateEngine({ theTemplate: "Alternative output" });
714 testNode.innerHTML = "<div data-bind='template: { name: \"theTemplate\", templateEngine: chosenEngine }'></div>";
715 ko.applyBindings({ chosenEngine: alternativeTemplateEngine }, testNode);
717 value_of(testNode.childNodes[0]).should_contain_text("Alternative output");
720 'Should be able to bind $data to an alias using \'as\'': function() {
721 ko.setTemplateEngine(new dummyTemplateEngine({
722 myTemplate: "ValueLiteral: [js:item.prop], ValueBound: <span data-bind='text: item.prop'></span>"
724 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\", data: someItem, as: \"item\" }'></div>";
725 ko.applyBindings({ someItem: { prop: 'Hello' } }, testNode);
726 value_of(testNode.childNodes[0]).should_contain_text("ValueLiteral: Hello, ValueBound: Hello");
729 'Data-bind syntax should expose parent binding context as $parent if binding with an explicit \"data\" value': function() {
730 ko.setTemplateEngine(new dummyTemplateEngine({
731 myTemplate: "ValueLiteral: [js:$parent.parentProp], ValueBound: <span data-bind='text: $parent.parentProp'></span>"
733 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\", data: someItem }'></div>";
734 ko.applyBindings({ someItem: {}, parentProp: 'Hello' }, testNode);
735 value_of(testNode.childNodes[0]).should_contain_text("ValueLiteral: Hello, ValueBound: Hello");
738 'Data-bind syntax should expose all ancestor binding contexts as $parents': function() {
739 ko.setTemplateEngine(new dummyTemplateEngine({
740 outerTemplate: "<div data-bind='template: { name:\"middleTemplate\", data: middleItem }'></div>",
741 middleTemplate: "<div data-bind='template: { name: \"innerTemplate\", data: innerItem }'></div>",
742 innerTemplate: "(Data:[js:$data.val], Parent:[[js:$parents[0].val]], Grandparent:[[js:$parents[1].val]], Root:[js:$root.val], Depth:[js:$parents.length])"
744 testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\", data: outerItem }'></div>";
752 innerItem: { val: "INNER" }
756 value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("(Data:INNER, Parent:MIDDLE, Grandparent:OUTER, Root:ROOT, Depth:3)");
759 'Should not be allowed to rewrite templates that embed anonymous templates': function() {
760 // The reason is that your template engine's native control flow and variable evaluation logic is going to run first, independently
761 // of any KO-native control flow, so variables would get evaluated in the wrong context. Example:
763 // <div data-bind="foreach: someArray">
764 // ${ somePropertyOfEachArrayItem } <-- This gets evaluated *before* the foreach binds, so it can't reference array entries
767 // It should be perfectly OK to fix this just by preventing anonymous templates within rewritten templates, because
768 // (1) The developer can always use their template engine's native control flow syntax instead of the KO-native ones - that will work
769 // (2) The developer can use KO's native templating instead, if they are keen on KO-native control flow or anonymous templates
771 ko.setTemplateEngine(new dummyTemplateEngine({
772 myTemplate: "<div data-bind='template: { data: someData }'>Childprop: [js: childProp]</div>"
774 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\" }'></div>";
776 var didThrow = false;
778 ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
781 value_of(ex.message).should_be("This template engine does not support anonymous templates nested within its templates");
783 value_of(didThrow).should_be(true);
786 'Should not be allowed to rewrite templates that embed control flow bindings': function() {
787 // Same reason as above
788 ko.utils.arrayForEach(['if', 'ifnot', 'with', 'foreach'], function(bindingName) {
789 ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "<div data-bind='" + bindingName + ": \"SomeValue\"'>Hello</div>" }));
790 testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\" }'></div>";
792 var didThrow = false;
793 try { ko.applyBindings({ someData: { childProp: 'abc' } }, testNode) }
796 value_of(ex.message).should_be("This template engine does not support the '" + bindingName + "' binding within its templates");
799 throw new Error("Did not prevent use of " + bindingName);
803 'Data binding syntax should permit nested templates using virtual containers (with arbitrary internal whitespace and newlines)': function() {
804 ko.setTemplateEngine(new dummyTemplateEngine({
805 outerTemplate: "Outer <!-- ko template: \n" +
806 "{ name: \"innerTemplate\" } \n" +
808 innerTemplate: "Inner via inline binding: <span data-bind='text: \"someText\"'></span>"
811 testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\" }'></div>";
812 ko.applyBindings(model, testNode);
813 value_of(testNode.childNodes[0]).should_contain_html("outer <!-- ko -->inner via inline binding: <span>sometext</span><!-- /ko -->");
816 'Should be able to render anonymous templates using virtual containers': function() {
817 ko.setTemplateEngine(new dummyTemplateEngine());
818 testNode.innerHTML = "Start <!-- ko template: { data: someData } -->Childprop: [js: childProp]<!-- /ko --> End";
819 ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
820 value_of(testNode).should_contain_html("start <!-- ko template: { data: somedata } -->childprop: abc<!-- /ko -->end");
823 'Should be able to use anonymous templates that contain first-child comment nodes': function() {
824 // This represents issue https://github.com/SteveSanderson/knockout/issues/188
825 // (IE < 9 strips out leading comment nodes when you use .innerHTML)
826 ko.setTemplateEngine(new dummyTemplateEngine({}));
827 testNode.innerHTML = "start <div data-bind='foreach: [1,2]'><span><!-- leading comment -->hello</span></div>";
828 ko.applyBindings(null, testNode);
829 value_of(testNode).should_contain_html('start <div data-bind="foreach: [1,2]"><span><!-- leading comment -->hello</span><span><!-- leading comment -->hello</span></div>');
832 'Should allow anonymous templates output to include top-level virtual elements, and will bind their virtual children only once': function() {
833 delete ko.bindingHandlers.nonexistentHandler;
835 ko.bindingHandlers.countInits = { init: function () { initCalls++ } };
836 testNode.innerHTML = "<div data-bind='template: {}'><!-- ko nonexistentHandler: true --><span data-bind='countInits: true'></span><!-- /ko --></div>";
837 ko.applyBindings(null, testNode);
838 value_of(initCalls).should_be(1);
841 'Should not throw errors if trying to apply text to a non-rendered node': function() {
842 // Represents https://github.com/SteveSanderson/knockout/issues/660
843 // A <span> can't go directly into a <tr>, so modern browsers will silently strip it. We need to verify this doesn't
844 // throw errors during unmemoization (when unmemoizing, it will try to apply the text to the following text node
845 // instead of the node you intended to bind to).
846 // Note that IE < 9 won't strip the <tr>; instead it has much stranger behaviors regarding unexpected DOM structures.
847 // It just happens not to give an error in this particular case, though it would throw errors in many other cases
848 // of malformed template DOM.
849 ko.setTemplateEngine(new dummyTemplateEngine({
850 myTemplate: "<tr><span data-bind=\"text: 'Some text'\"></span> </tr>" // The whitespace after the closing span is what triggers the strange HTML parsing
852 testNode.innerHTML = "<div data-bind='template: \"myTemplate\"'></div>";
853 ko.applyBindings(null, testNode);
854 // Since the actual template markup was invalid, we don't really care what the
855 // resulting DOM looks like. We are only verifying there were no exceptions.