migrated knockout asset to bower
[openemr.git] / public / assets / knockout-2-2-1 / spec / templatingBehaviors.js
blob37df535963ec68ffea6030d9f96b3e5cccd37145
2 var dummyTemplateEngine = function (templates) {
3     var inMemoryTemplates = templates || {};
4     var inMemoryTemplateData = {};
6     function dummyTemplateSource(id) {
7         this.id = id;
8     }
9     dummyTemplateSource.prototype = {
10         text: function(val) {
11             if (arguments.length >= 1)
12                 inMemoryTemplates[this.id] = val;
13             return inMemoryTemplates[this.id];
14         },
15         data: function(key, val) {
16             if (arguments.length >= 2) {
17                 inMemoryTemplateData[this.id] = inMemoryTemplateData[this.id] || {};
18                 inMemoryTemplateData[this.id][key] = val;
19             }
20             return (inMemoryTemplateData[this.id] || {})[key];
21         }
22     }
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
29     };
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
41         var result;
42         with (bindingContext) {
43             with (data || {}) {
44                 with (options.templateRenderingVariablesInScope || {}) {
45                     // Dummy [renderTemplate:...] syntax
46                     result = templateText.replace(/\[renderTemplate\:(.*?)\]/g, function (match, templateName) {
47                         return ko.renderTemplate(templateName, data, options);
48                     });
51                     var evalHandler = function (match, script) {
52                         try {
53                             var evalResult = eval(script);
54                             return (evalResult === null) || (evalResult === undefined) ? "" : evalResult.toString();
55                         } catch (ex) {
56                             throw new Error("Error evaluating script: [js: " + script + "]\n\nException: " + ex.toString());
57                         }
58                     }
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);
65                 }
66             }
67         }
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));
72     };
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);
79     };
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);
93     },
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);
98     },
100     'Should not be able to render a template until a template engine is provided': function () {
101         var threw = false;
102         ko.setTemplateEngine(undefined);
103         try { ko.renderTemplate("someTemplate", {}) }
104         catch (ex) { threw = true }
105         value_of(threw).should_be(true);
106     },
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");
113     },
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;
121         }
122         var myModel = {};
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);
127     },
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();
133         }
134         }));
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");
140         dependency("B");
141         value_of(testNode.childNodes.length).should_be(1);
142         value_of(testNode.innerHTML).should_be("Value = B");
143     },
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
149         };
150         var myTemplate = function() {
151             return "Value = " + (++count);
152         };
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");
158         observable("B");
159         value_of(testNode.childNodes.length).should_be(1);
160         value_of(testNode.innerHTML).should_be("Value = 1");
161     },
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;
167         }
168         }));
169         ko.renderTemplate("someTemplate", observable, null, testNode);
170         value_of(testNode.innerHTML).should_be("Value = A");
172         observable("B");
173         value_of(testNode.innerHTML).should_be("Value = B");
174     },
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);
186         dependency("B");
187         value_of(testNode.childNodes.length).should_be(1);
188         value_of(testNode.innerHTML).should_be("Value = A");
189     },
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");
196     },
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");
203     },
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");
217     },
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);
228     },
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"
234         }));
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");
243     },
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;
250         };
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");
255     },
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>"
261         }));
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>");
265     },
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() }
273         }));
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);
280         observable("DEF");
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);
284     },
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()]"
291         }));
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);
298     },
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");
304     },
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");
310     },
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");
316     },
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");
322     },
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");
328     },
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; ]"
333         }));
334         ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { message: "hello"} }, testNode);
335         value_of(testNode.childNodes[0].value).should_be("goodbye");
336     },
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>"
341         }));
342         var viewModel = {
343             didCallMyFunction : false,
344             someFunctionOnModel : function() { this.didCallMyFunction = true }
345         };
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
349         buttonNode.click();
350         value_of(viewModel.didCallMyFunction).should_be(true);
351     },
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);
360             },
361             getBindings: function(node, bindingContext) {
362                 if (node.tagName == 'EM')
363                     return { text: ++model.numBindings };
364                 return originalBindingProvider.getBindings(node, bindingContext);
365             }
366         };
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>"
372         }));
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;
380     },
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);
396     },
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>");
405     },
407     'Data binding \'foreach\' options should only bind each group of output nodes once': function() {
408         var initCalls = 0;
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
415     },
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")
431         myArray(["new"]);
432         value_of(testNode.childNodes[0]).should_contain_html("<div>new</div>");
433     },
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>"
440         }));
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.
449         myArray(["new"]);
450         value_of(testNode.childNodes[0]).should_contain_html("<div>new</div>inner <span>123</span>x");
451     },
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>");
460     },
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>");
476     },
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>");
485     },
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);
507     },
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?)
519         myArray(null);
520         value_of(testNode.childNodes[0].childNodes.length).should_be(0);
521     },
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");
532     },
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);
545     },
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);
558         myArray([]);
559         value_of(innerObservable.getSubscriptionsCount()).should_be(0);
560     },
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>");
569     },
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>");
578     },
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");
597     },
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("");
616     },
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");
637     },
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);
643     },
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);
649     },
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' }
655         ]);
656         ko.setTemplateEngine(new dummyTemplateEngine({
657             firstTemplate: "<div>Template1Output, [js:someProperty]</div>",
658             secondTemplate: "<div>Template2Output, [js:someProperty]</div>"
659         }));
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';
667         };
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>");
670     },
672     'Data binding \'templateOptions\' should be passed to template': function() {
673         var myModel = {
674             someAdditionalData: { myAdditionalProp: "someAdditionalValue" },
675             people: new ko.observableArray([
676                 { name: "Alpha" },
677                 { name: "Beta" }
678             ])
679         };
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>");
685     },
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"),
689             myModel = {
690                 subModel: ko.observable({ myObservable: myObservable })
691             };
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);
708     },
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");
718     },
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>"
723         }));
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");
727     },
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>"
732         }));
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");
736     },
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])"
743         }));
744         testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\", data: outerItem }'></div>";
746         ko.applyBindings({
747             val: "ROOT",
748             outerItem: {
749                 val: "OUTER",
750                 middleItem: {
751                     val: "MIDDLE",
752                     innerItem: { val: "INNER" }
753                 }
754             }
755         }, testNode);
756         value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("(Data:INNER, Parent:MIDDLE, Grandparent:OUTER, Root:ROOT, Depth:3)");
757     },
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:
762         //
763         // <div data-bind="foreach: someArray">
764         //     ${ somePropertyOfEachArrayItem }   <-- This gets evaluated *before* the foreach binds, so it can't reference array entries
765         // </div>
766         //
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>"
773         }));
774         testNode.innerHTML = "<div data-bind='template: { name: \"myTemplate\" }'></div>";
776         var didThrow = false;
777         try {
778             ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
779         } catch(ex) {
780             didThrow = true;
781             value_of(ex.message).should_be("This template engine does not support anonymous templates nested within its templates");
782         }
783         value_of(didThrow).should_be(true);
784     },
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) }
794             catch (ex) {
795                 didThrow = true;
796                 value_of(ex.message).should_be("This template engine does not support the '" + bindingName + "' binding within its templates");
797             }
798             if (!didThrow)
799                 throw new Error("Did not prevent use of " + bindingName);
800         });
801     },
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" +
807                 "--><!-- /ko -->",
808             innerTemplate: "Inner via inline binding: <span data-bind='text: \"someText\"'></span>"
809         }));
810         var model = { };
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 -->");
814     },
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");
821     },
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>');
830     },
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;
834         var initCalls = 0;
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);
839     },
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
851         }));
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.
856     }