diff --git a/knockout/all-tests.ts b/knockout/all-tests.ts
new file mode 100644
index 000000000..3fe7a471f
--- /dev/null
+++ b/knockout/all-tests.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/knockout/knockout.d.ts b/knockout/knockout.d.ts
index 58479d846..aefcc797a 100644
--- a/knockout/knockout.d.ts
+++ b/knockout/knockout.d.ts
@@ -81,6 +81,9 @@ interface KnockoutObservableArrayStatic {
(): KnockoutObservableArray;
(value: any[]): KnockoutObservableArray;
+
+ new(): KnockoutObservableArray;
+ new(value: any[]): KnockoutObservableArray;
}
interface KnockoutObservableArray extends KnockoutObservableArrayFunctions {
@@ -99,10 +102,17 @@ interface KnockoutObservableStatic {
(value: number): KnockoutObservableNumber;
(value: bool): KnockoutObservableBool;
(value?: any): KnockoutObservableAny;
+
+ new(value: string): KnockoutObservableString;
+ new(value: Date): KnockoutObservableDate;
+ new(value: number): KnockoutObservableNumber;
+ new(value: bool): KnockoutObservableBool;
+ new(value?: any): KnockoutObservableAny;
}
/** use as method to get/set the value */
interface KnockoutObservableBase extends KnockoutObservableFunctions {
+ getSubscriptionsCount(): number;
}
/** use as method to get/set the value
@@ -229,56 +239,184 @@ interface KnockoutExtenders {
interface KnockoutUtils {
+ //////////////////////////////////
+ // utils.domManipulation.js
+ //////////////////////////////////
+
+ simpleHtmlParse(html: string);
+
+ jQueryHtmlParse(html: string);
+
+ parseHtmlFragment(html: string);
+
+ setHtml(node: Element, html: string): void;
+
+ setHtml(node: Element, html: () => string): void;
+
+ //////////////////////////////////
+ // utils.domData.js
+ //////////////////////////////////
+
+ domData: {
+ get (node: Element, key: string);
+
+ set (node: Element, key: string, value: any);
+
+ getAll(node: Element, createIfNotFound: bool);
+
+ clear(node: Element);
+ };
+
+ //////////////////////////////////
+ // utils.domNodeDisposal.js
+ //////////////////////////////////
+
+ domNodeDisposal: {
+ addDisposeCallback(node: Element, callback: Function);
+
+ removeDisposeCallback(node: Element, callback: Function);
+
+ cleanNode(node: Element);
+
+ removeNode(node: Element);
+ };
+
+ //////////////////////////////////
+ // utils.js
+ //////////////////////////////////
+
fieldsIncludedWithJsonPost: any[];
arrayForEach(array: any[], action: (any) => void ): void;
+
arrayIndexOf(array: any[], item: any): number;
+
arrayFirst(array: any[], predicate: (item) => bool, predicateOwner?: any): any;
+
arrayRemoveItem(array: any[], itemToRemove: any): void;
+
arrayGetDistinctValues(array: any[]): any[];
+
arrayMap(array: any[], mapping: (item) => any): any[];
+
arrayFilter(array: any[], predicate: (item) => bool): any[];
+
arrayPushAll(array: any[], valuesToPush: any[]): any[];
extend(target, source);
emptyDomNode(domNode): void;
+
moveCleanedNodesToContainerElement(nodes: any[]): HTMLElement;
+
cloneNodes(nodesArray: any[], shouldCleanNodes: bool): any[];
+
setDomNodeChildren(domNode: any, childNodes: any[]): void;
+
replaceDomNodes(nodeToReplaceOrNodeArray: any, newNodesArray: any[]): void;
+
setOptionNodeSelectionState(optionNode: any, isSelected: bool): void;
+
stringTrim(str: string): string;
+
stringTokenize(str: string, delimiter: string): string;
+
stringStartsWith(str: string, startsWith: string): string;
+
domNodeIsContainedBy(node: any, containedByNode: any): bool;
+
domNodeIsAttachedToDocument(node: any): bool;
+
tagNameLower(element: any): string;
+
registerEventHandler(element: any, eventType: any, handler: Function): void;
+
triggerEvent(element: any, eventType: any): void;
+
unwrapObservable(value: any): any;
+
toggleDomNodeCssClass(node: any, className: string, shouldHaveClass: bool): void;
+
setTextContent(element: any, textContent: string): void;
+
setElementName(element: any, name: string): void;
+
ensureSelectElementIsRenderedCorrectly(selectElement);
+
forceRefresh(node: any): void;
+
ensureSelectElementIsRenderedCorrectly(selectElement: any): void;
+
range(min: any, max: any): any;
+
makeArray(arrayLikeObject: any): any[];
+
getFormFields(form: any, fieldName: string): any[];
+
parseJson(jsonString: string): any;
+
stringifyJson(data: any, replacer: Function, space: string): string;
+
postJson(urlOrForm: any, data: any, options: any): void;
- setHtml(node: Element, html: string): void;
- setHtml(node: Element, html: () => string): void;
ieVersion: number;
- isIe6: bool;
- isIe7: bool;
- domNodeDisposal;
+ isIe6: bool;
+
+ isIe7: bool;
}
+//////////////////////////////////
+// templateSources.js
+//////////////////////////////////
+
+interface KnockoutTemplateSourcesDomElement {
+
+ text(valueToWrite?);
+
+ data(key, valueToWrite?);
+}
+
+
+interface KnockoutTemplateSources {
+
+ domElement: KnockoutTemplateSourcesDomElement;
+
+ anonymousTemplate: {
+
+ prototype: KnockoutTemplateSourcesDomElement;
+
+ new (element: Element): KnockoutTemplateSourcesDomElement;
+ };
+};
+
+//////////////////////////////////
+// nativeTemplateEngine.js
+//////////////////////////////////
+
+interface KnockoutNativeTemplateEngine {
+
+ renderTemplateSource(templateSource, bindingContext, options?);
+}
+
+//////////////////////////////////
+// templateEngine.js
+//////////////////////////////////
+
+interface KnockoutTemplateEngine extends KnockoutNativeTemplateEngine {
+
+ createJavaScriptEvaluatorBlock(script: string);
+
+ makeTemplateSource(template, templateDocument);
+
+ renderTemplate(template, bindingContext, options, templateDocument);
+
+ isTemplateRewritten(template, templateDocument): bool;
+
+ rewriteTemplate(template, rewriterCallback, templateDocument);
+}
+
+/////////////////////////////////
interface KnockoutStatic {
utils: KnockoutUtils;
@@ -305,6 +443,84 @@ interface KnockoutStatic {
dataFor(node: any): any;
removeNode(node: Element);
cleanNode(node: Element);
+ renderTemplate(template: Function, viewModel: any, options?: any, target?: any, renderMode?: any);
+ renderTemplate(template: string, viewModel: any, options?: any, target?: any, renderMode?: any);
+
+ //////////////////////////////////
+ // templateSources.js
+ //////////////////////////////////
+
+ templateSources: KnockoutTemplateSources;
+
+ //////////////////////////////////
+ // templateEngine.js
+ //////////////////////////////////
+
+ templateEngine: {
+
+ prototype: KnockoutTemplateEngine;
+
+ new (): KnockoutTemplateEngine;
+ };
+
+ //////////////////////////////////
+ // templateRewriting.js
+ //////////////////////////////////
+
+ templateRewriting: {
+
+ ensureTemplateIsRewritten(template, templateEngine, templateDocument);
+
+ memoizeBindingAttributeSyntax(htmlString: string, templateEngine: KnockoutTemplateEngine);
+
+ applyMemoizedBindingsToNextSibling(bindings);
+ };
+
+ //////////////////////////////////
+ // nativeTemplateEngine.js
+ //////////////////////////////////
+
+ nativeTemplateEngine: {
+
+ prototype: KnockoutNativeTemplateEngine;
+
+ new (): KnockoutNativeTemplateEngine;
+
+ instance: KnockoutNativeTemplateEngine;
+ };
+
+ //////////////////////////////////
+ // jqueryTmplTemplateEngine.js
+ //////////////////////////////////
+
+ jqueryTmplTemplateEngine: {
+
+ prototype: KnockoutTemplateEngine;
+
+ renderTemplateSource(templateSource, bindingContext, options);
+
+ createJavaScriptEvaluatorBlock(script: string): string;
+
+ addTemplate(templateName, templateMarkup);
+ };
+
+ //////////////////////////////////
+ // templating.js
+ //////////////////////////////////
+
+ setTemplateEngine(templateEngine: KnockoutNativeTemplateEngine);
+
+ renderTemplate(template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode);
+
+ renderTemplateForEach(template, arrayOrObservableArray, options, targetNode, parentBindingContext);
+
+ expressionRewriting: {
+ bindingRewriteValidators: any;
+ };
+
+ /////////////////////////////////
+
+ bindingProvider: any;
}
declare var ko: KnockoutStatic;
\ No newline at end of file
diff --git a/knockout/tests/knockout-templatingBehaviors-tests.ts b/knockout/tests/knockout-templatingBehaviors-tests.ts
new file mode 100644
index 000000000..b000015f2
--- /dev/null
+++ b/knockout/tests/knockout-templatingBehaviors-tests.ts
@@ -0,0 +1,886 @@
+///
+///
+///
+
+declare var $;
+
+var dummyTemplateEngine = function (templates?) {
+ var inMemoryTemplates = templates || {};
+ var inMemoryTemplateData = {};
+
+ function dummyTemplateSource(id) {
+ this.id = id;
+ }
+ dummyTemplateSource.prototype = {
+ text: function(val) {
+ if (arguments.length >= 1)
+ inMemoryTemplates[this.id] = val;
+ return inMemoryTemplates[this.id];
+ },
+ data: function(key, val) {
+ if (arguments.length >= 2) {
+ inMemoryTemplateData[this.id] = inMemoryTemplateData[this.id] || {};
+ inMemoryTemplateData[this.id][key] = val;
+ }
+ return (inMemoryTemplateData[this.id] || {})[key];
+ }
+ }
+
+ this.makeTemplateSource = function(template) {
+ if (typeof template == "string")
+ return new dummyTemplateSource(template); // Named template comes from the in-memory collection
+ else if ((template.nodeType == 1) || (template.nodeType == 8))
+ return new ko.templateSources.anonymousTemplate(template); // Anonymous template
+ };
+
+ this.renderTemplateSource = function (templateSource, bindingContext, options) {
+ var data = bindingContext['$data'];
+ options = options || {};
+ var templateText = templateSource.text();
+ if (typeof templateText == "function")
+ templateText = templateText(data, options);
+
+ templateText = options.showParams ? templateText + ", data=" + data + ", options=" + options : templateText;
+ var templateOptions = options.templateOptions; // Have templateOptions in scope to support [js:templateOptions.foo] syntax
+
+ var result;
+ //with (bindingContext)
+ {
+ //with (data || {})
+ {
+ //with (options.templateRenderingVariablesInScope || {})
+ {
+ // Dummy [renderTemplate:...] syntax
+ result = templateText.replace(/\[renderTemplate\:(.*?)\]/g, function (match, templateName) {
+ return ko.renderTemplate(templateName, data, options);
+ });
+
+
+ var evalHandler = function (match, script) {
+ try {
+ var evalResult = eval(script);
+ return (evalResult === null) || (evalResult === undefined) ? "" : evalResult.toString();
+ } catch (ex) {
+ throw new Error("Error evaluating script: [js: " + script + "]\n\nException: " + ex.toString());
+ }
+ }
+
+ // Dummy [[js:...]] syntax (in case you need to use square brackets inside the expression)
+ result = result.replace(/\[\[js\:([\s\S]*?)\]\]/g, evalHandler);
+
+ // Dummy [js:...] syntax
+ result = result.replace(/\[js\:([\s\S]*?)\]/g, evalHandler);
+ }
+ }
+ }
+
+ // Use same HTML parsing code as real template engine so as to trigger same combination of IE weirdnesses
+ // 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.
+ return ko.utils.arrayPushAll([], ko.utils.parseHtmlFragment(result));
+ };
+
+ this.rewriteTemplate = function (template, rewriterCallback) {
+ // Only rewrite if the template isn't a function (can't rewrite those)
+ var templateSource = new ko.templateSources.anonymousTemplate(template); //this.makeTemplateSource(template);
+ if (typeof templateSource.text() != "function")
+ return ko.templateEngine.prototype.rewriteTemplate.call(this, template, rewriterCallback);
+ };
+ this.createJavaScriptEvaluatorBlock = function (script) { return "[js:" + script + "]"; };
+};
+dummyTemplateEngine.prototype = new ko.templateEngine();
+
+describe('Templating', function() {
+ beforeEach(function() {
+ ko.setTemplateEngine(new ko.nativeTemplateEngine());
+ });
+ //beforeEach(jasmine.prepareTestNode);
+ var testNode: any;
+
+ it('Template engines can return an array of DOM nodes', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ x: [document.createElement("div"), document.createElement("span")] }));
+ ko.renderTemplate("x", null);
+ });
+
+ it('Should not be able to render a template until a template engine is provided', function () {
+ var threw = false;
+ ko.setTemplateEngine(undefined);
+ try { ko.renderTemplate("someTemplate", {}) }
+ catch (ex) { threw = true }
+ expect(threw).toEqual(true);
+ });
+
+ it('Should be able to render a template into a given DOM element', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" }));
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("ABC");
+ });
+
+ it('Should be able to render an empty template', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({ emptyTemplate: "" }));
+ ko.renderTemplate("emptyTemplate", null, null, testNode);
+ expect(testNode.childNodes.length).toEqual(0);
+ });
+
+ it('Should be able to access newly rendered/inserted elements in \'afterRender\' callaback', function () {
+ var passedElement, passedDataItem;
+ var myCallback = function(elementsArray, dataItem) {
+ expect(elementsArray.length).toEqual(1);
+ passedElement = elementsArray[0];
+ passedDataItem = dataItem;
+ }
+ var myModel = {};
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" }));
+ ko.renderTemplate("someTemplate", myModel, { afterRender: myCallback }, testNode);
+ expect(passedElement.nodeValue).toEqual("ABC");
+ expect(passedDataItem).toEqual(myModel);
+ });
+
+ it('Should automatically rerender into DOM element when dependencies change', function () {
+ var dependency = new ko.observable("A");
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: function () {
+ return "Value = " + dependency();
+ }
+ }));
+
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = A");
+
+ dependency("B");
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = B");
+ });
+
+ it('Should not rerender DOM element if observable accessed in \'afterRender\' callaback is changed', function () {
+ var observable = new ko.observable("A"), count = 0;
+ var myCallback = function(elementsArray, dataItem) {
+ observable(); // access observable in callback
+ };
+ var myTemplate = function() {
+ return "Value = " + (++count);
+ };
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: myTemplate }));
+ ko.renderTemplate("someTemplate", {}, { afterRender: myCallback }, testNode);
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = 1");
+
+ observable("B");
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = 1");
+ });
+
+ it('If the supplied data item is observable, evaluates it and has subscription on it', function () {
+ var observable = new ko.observable("A");
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: function (data) {
+ return "Value = " + data;
+ }
+ }));
+ ko.renderTemplate("someTemplate", observable, null, testNode);
+ expect(testNode.innerHTML).toEqual("Value = A");
+
+ observable("B");
+ expect(testNode.innerHTML).toEqual("Value = B");
+ });
+
+ it('Should stop updating DOM nodes when the dependency next changes if the DOM node has been removed from the document', function () {
+ var dependency = new ko.observable("A");
+ var template = { someTemplate: function () { return "Value = " + dependency() } };
+ ko.setTemplateEngine(new dummyTemplateEngine(template));
+
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = A");
+
+ testNode.parentNode.removeChild(testNode);
+ dependency("B");
+ expect(testNode.childNodes.length).toEqual(1);
+ expect(testNode.innerHTML).toEqual("Value = A");
+ });
+
+ it('Should be able to render a template using data-bind syntax', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "template output" }));
+ testNode.innerHTML = "
";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0].innerHTML).toEqual("template output");
+ });
+
+ it('Should be able to tell data-bind syntax which object to pass as data for the template (otherwise, uses viewModel)', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someProp: { childProp: 123} }, testNode);
+ expect(testNode.childNodes[0].innerHTML).toEqual("result = 123");
+ });
+
+ it('Should re-render a named template when its data item notifies about mutation', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
+ testNode.innerHTML = "";
+
+ var myData = ko.observable({ childProp: 123 });
+ ko.applyBindings({ someProp: myData }, testNode);
+ expect(testNode.childNodes[0].innerHTML).toEqual("result = 123");
+
+ // Now mutate and notify
+ myData().childProp = 456;
+ myData.valueHasMutated();
+ expect(testNode.childNodes[0].innerHTML).toEqual("result = 456");
+ });
+
+ it('Should stop tracking inner observables immediately when the container node is removed from the document', function() {
+ var innerObservable = ko.observable("some value");
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp()]" }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someProp: { childProp: innerObservable} }, testNode);
+
+ expect(innerObservable.getSubscriptionsCount()).toEqual(1);
+ ko.removeNode(testNode.childNodes[0]);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(0);
+ });
+
+ it('Should be able to pick template via an observable model property', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ firstTemplate: "First template output",
+ secondTemplate: "Second template output"
+ }));
+
+ var chosenTemplate = ko.observable("firstTemplate");
+ testNode.innerHTML = "";
+ ko.applyBindings({ chosenTemplate: chosenTemplate }, testNode);
+ expect(testNode.childNodes[0].innerHTML).toEqual("First template output");
+
+ chosenTemplate("secondTemplate");
+ expect(testNode.childNodes[0].innerHTML).toEqual("Second template output");
+ });
+
+ it('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 () {
+ var templatePicker = function(dataItem, bindingContext) {
+ // Having the entire binding context available means you can read sibling or parent level properties
+ expect(bindingContext.$parent.anotherProperty).toEqual(456);
+ return dataItem.myTemplate;
+ };
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someProp: { childProp: 123, myTemplate: "someTemplate" }, templateSelectorFunction: templatePicker, anotherProperty: 456 }, testNode);
+ expect(testNode.childNodes[0].innerHTML).toEqual("result = 123");
+ });
+
+ it('Should be able to chain templates, rendering one from inside another', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "outer template output, [renderTemplate:innerTemplate]", // [renderTemplate:...] is special syntax supported by dummy template engine
+ innerTemplate: "inner template output "
+ }));
+ testNode.innerHTML = "";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("outer template output, inner template output 123");
+ });
+
+ it('Should rerender chained templates when their dependencies change, without rerendering parent templates', function () {
+ var observable = new ko.observable("ABC");
+ var timesRenderedOuter = 0, timesRenderedInner = 0;
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: function () { timesRenderedOuter++; return "outer template output, [renderTemplate:innerTemplate]" }, // [renderTemplate:...] is special syntax supported by dummy template engine
+ innerTemplate: function () { timesRenderedInner++; return observable() }
+ }));
+ testNode.innerHTML = "";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("outer template output, abc");
+ expect(timesRenderedOuter).toEqual(1);
+ expect(timesRenderedInner).toEqual(1);
+
+ observable("DEF");
+ expect(testNode.childNodes[0]).toContainHtml("outer template output, def");
+ expect(timesRenderedOuter).toEqual(1);
+ expect(timesRenderedInner).toEqual(2);
+ });
+
+ it('Should stop tracking inner observables referenced by a chained template as soon as the chained template output node is removed from the document', function() {
+ var innerObservable = ko.observable("some value");
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "outer template output, [renderTemplate:innerTemplate]",
+ innerTemplate: "result = [js: childProp()]"
+ }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someProp: { childProp: innerObservable} }, testNode);
+
+ expect(innerObservable.getSubscriptionsCount()).toEqual(1);
+ ko.removeNode(document.getElementById('innerTemplateOutput'));
+ expect(innerObservable.getSubscriptionsCount()).toEqual(0);
+ });
+
+ it('Should handle data-bind attributes from inside templates, regardless of element and attribute casing', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("Hi");
+ });
+
+ it('Should handle data-bind attributes that include newlines from inside templates', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("Hi");
+ });
+
+ it('Data binding syntax should be able to reference variables put into scope by the template engine', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
+ ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { message: "hello"} }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("hello");
+ });
+
+ it('Data binding syntax should be able to use $element in binding value', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
+ ko.renderTemplate("someTemplate", null, null, testNode);
+ expect(testNode.childNodes[0]).toContainText("DIV");
+ });
+
+ it('Data binding syntax should be able to use $context in binding value to refer to the context object', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
+ ko.renderTemplate("someTemplate", {}, null, testNode);
+ expect(testNode.childNodes[0]).toContainText("true");
+ });
+
+ it('Data binding syntax should defer evaluation of variables until the end of template rendering (so bindings can take independent subscriptions to them)', function () {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ someTemplate: "[js: message = 'goodbye'; undefined; ]"
+ }));
+ ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { message: "hello"} }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("goodbye");
+ });
+
+ it('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() {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ someTemplate: ""
+ }));
+ var viewModel = {
+ didCallMyFunction : false,
+ someFunctionOnModel : function() { this.didCallMyFunction = true }
+ };
+ ko.renderTemplate("someTemplate", viewModel, null, testNode);
+ var buttonNode = testNode.childNodes[0];
+ expect(buttonNode.tagName).toEqual("BUTTON"); // Be sure we're clicking the right thing
+ buttonNode.click();
+ expect(viewModel.didCallMyFunction).toEqual(true);
+ });
+
+ it('Data binding syntax should permit nested templates, and only bind inner templates once', function() {
+ // Will verify that bindings are applied only once for both inline (rewritten) bindings,
+ // and external (non-rewritten) ones
+ var originalBindingProvider = ko.bindingProvider.instance;
+ ko.bindingProvider.instance = {
+ nodeHasBindings: function(node, bindingContext) {
+ return (node.tagName == 'EM') || originalBindingProvider.nodeHasBindings(node, bindingContext);
+ },
+ getBindings: function(node, bindingContext) {
+ if (node.tagName == 'EM')
+ return { text: ++model.numBindings };
+ return originalBindingProvider.getBindings(node, bindingContext);
+ }
+ };
+
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "Outer ",
+ innerTemplate: "Inner via inline binding: "
+ + "Inner via external binding: "
+ }));
+ var model = { numBindings: 0 };
+ testNode.innerHTML = "";
+ ko.applyBindings(model, testNode);
+ expect(model.numBindings).toEqual(2);
+ expect(testNode.childNodes[0]).toContainHtml("outer
inner via inline binding: 2inner via external binding: 1
");
+
+ ko.bindingProvider.instance = originalBindingProvider;
+ });
+
+ it('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 () {
+ var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "
");
+ var originalBobNode = testNode.childNodes[0].childNodes[0];
+ var originalFrankNode = testNode.childNodes[0].childNodes[1];
+
+ myArray.push({ personName: "Steve" });
+ expect(testNode.childNodes[0]).toContainHtml("
the item is bob
the item is frank
the item is steve
");
+ expect(testNode.childNodes[0].childNodes[0]).toEqual(originalBobNode);
+ expect(testNode.childNodes[0].childNodes[1]).toEqual(originalFrankNode);
+ });
+
+ it('Data binding \'foreach\' option should apply bindings within the context of each item in the array', function () {
+ var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is " }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("the item is bobthe item is frank");
+ });
+
+ it('Data binding \'foreach\' options should only bind each group of output nodes once', function() {
+ var initCalls = 0;
+ (ko.bindingHandlers).countInits = { init: function() { initCalls++ } };
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "" }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: [1,2,3] }, testNode);
+ expect(initCalls).toEqual(3); // 3 because there were 3 items in myCollection
+ });
+
+ it('Data binding \'foreach\' should handle templates in which the very first node has a binding', function() {
+ // Represents https://github.com/SteveSanderson/knockout/pull/440
+ // Previously, the rewriting (which introduces a comment node before the bound node) was interfering
+ // with the array-to-DOM-node mapping state tracking
+ ko.setTemplateEngine(new dummyTemplateEngine({ mytemplate: "" }));
+ testNode.innerHTML = "";
+
+ // Bind against initial array containing one entry. UI just shows "original"
+ var myArray = ko.observableArray(["original"]);
+ ko.applyBindings({ items: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("
original
");
+
+ // Now replace the entire array contents with one different entry.
+ // UI just shows "new" (previously with bug, showed "original" AND "new")
+ myArray(["new"]);
+ expect(testNode.childNodes[0]).toContainHtml("
new
");
+ });
+
+ it('Data binding \'foreach\' should handle chained templates in which the very first node has a binding', function() {
+ // See https://github.com/SteveSanderson/knockout/pull/440 and https://github.com/SteveSanderson/knockout/pull/144
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "[renderTemplate:innerTemplate]x", // [renderTemplate:...] is special syntax supported by dummy template engine
+ innerTemplate: "inner "
+ }));
+ testNode.innerHTML = "";
+
+ // Bind against initial array containing one entry.
+ var myArray = ko.observableArray(["original"]);
+ ko.applyBindings({ items: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("
original
inner 123x");
+
+ // Now replace the entire array contents with one different entry.
+ myArray(["new"]);
+ expect(testNode.childNodes[0]).toContainHtml("
new
inner 123x");
+ });
+
+ it('Data binding \'foreach\' should handle templates in which the very first node has a binding but it does not reference any observables', function() {
+ // Represents https://github.com/SteveSanderson/knockout/issues/739
+ // Previously, the rewriting (which introduces a comment node before the bound node) was interfering
+ // with the array-to-DOM-node mapping state tracking
+ ko.setTemplateEngine(new dummyTemplateEngine({ mytemplate: "
");
+
+ // Modify the observable property and check that UI is updated
+ // Previously with the bug, it wasn't updated because the removal of the memo comment caused the array-to-DOM-node computed to be disposed
+ myItem.name("b");
+ expect(testNode.childNodes[0]).toContainHtml("
b
");
+ });
+
+ it('Data binding \'foreach\' option should apply bindings with an $index in the context', function () {
+ var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item # is " }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("the item # is 0the item # is 1");
+ });
+
+ it('Data binding \'foreach\' option should update bindings that reference an $index if the list changes', function () {
+ var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is " }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("the item bobis 0the item frankis 1");
+
+ var frank = myArray.pop(); // remove frank
+ expect(testNode.childNodes[0]).toContainHtml("the item bobis 0");
+
+ myArray.unshift(frank); // put frank in the front
+ expect(testNode.childNodes[0]).toContainHtml("the item frankis 0the item bobis 1");
+
+ });
+
+ it('Data binding \'foreach\' option should accept array with "undefined" and "null" items', function () {
+ var myArray = new ko.observableArray([undefined, null]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is " }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("the item is undefinedthe item is null");
+ });
+
+ it('Data binding \'foreach\' option should update DOM nodes when a dependency of their mapping function changes', function() {
+ var myObservable = new ko.observable("Steve");
+ var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: myObservable }, { personName: "Another" }]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "
The item is [js: ko.utils.unwrapObservable(personName)]
");
+ expect(testNode.childNodes[0].childNodes[0]).toEqual(originalBobNode);
+
+ // Ensure we can still remove the corresponding nodes (even though they've changed), and that doing so causes the subscription to be disposed
+ expect(myObservable.getSubscriptionsCount()).toEqual(1);
+ myArray.splice(1, 1);
+ expect(testNode.childNodes[0]).toContainHtml("
the item is bob
the item is another
");
+ myObservable("Something else"); // Re-evaluating the observable causes the orphaned subscriptions to be disposed
+ expect(myObservable.getSubscriptionsCount()).toEqual(0);
+ });
+
+ it('Data binding \'foreach\' option should treat a null parameter as meaning \'no items\'', function() {
+ var myArray = new ko.observableArray(["A", "B"]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "hello" }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0].childNodes.length).toEqual(2);
+
+ // Now set the observable to null and check it's treated like an empty array
+ // (because how else should null be interpreted?)
+ myArray(null);
+ expect(testNode.childNodes[0].childNodes.length).toEqual(0);
+ });
+
+ it('Data binding \'foreach\' option should accept an \"as\" option to define an alias for the iteration variable', function() {
+ // Note: There are more detailed specs (e.g., covering nesting) associated with the "foreach" binding which
+ // uses this templating functionality internally.
+ var myArray = new ko.observableArray(["A", "B"]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "[js:myAliasedItem]" }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(testNode.childNodes[0]).toContainText("AB");
+ });
+
+ it('Data binding \'foreach\' option should stop tracking inner observables when the container node is removed', function() {
+ var innerObservable = ko.observable("some value");
+ var myArray = new ko.observableArray([{obsVal:innerObservable}, {obsVal:innerObservable}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is [js: ko.utils.unwrapObservable(obsVal)]" }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(2);
+
+ ko.removeNode(testNode.childNodes[0]);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(0);
+ });
+
+ it('Data binding \'foreach\' option should stop tracking inner observables related to each array item when that array item is removed', function() {
+ var innerObservable = ko.observable("some value");
+ var myArray = new ko.observableArray([{obsVal:innerObservable}, {obsVal:innerObservable}]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item is [js: ko.utils.unwrapObservable(obsVal)]" }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({ myCollection: myArray }, testNode);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(2);
+
+ myArray.splice(1, 1);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(1);
+ myArray([]);
+ expect(innerObservable.getSubscriptionsCount()).toEqual(0);
+ });
+
+ it('Data binding syntax should omit any items whose \'_destroy\' flag is set (unwrapping the flag if it is observable)', function() {
+ var myArray = new ko.observableArray([{ someProp: 1 }, { someProp: 2, _destroy: 'evals to true' }, { someProp : 3 }, { someProp: 4, _destroy: ko.observable(false) }]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "
");
+ });
+
+ it('Data binding syntax should include any items whose \'_destroy\' flag is set if you use includeDestroyed', function() {
+ var myArray = new ko.observableArray([{ someProp: 1 }, { someProp: 2, _destroy: 'evals to true' }, { someProp : 3 }]);
+ ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "
person alpha has additional property someadditionalvalue
person beta has additional property someadditionalvalue
");
+ });
+
+ it('If the template binding is updated, should dispose any template subscriptions previously associated with the element', function() {
+ var myObservable = ko.observable("some value"),
+ myModel = {
+ subModel: ko.observable({ myObservable: myObservable })
+ };
+ ko.setTemplateEngine(new dummyTemplateEngine({myTemplate: "The value is [js:myObservable()]"}));
+ testNode.innerHTML = "";
+ ko.applyBindings(myModel, testNode);
+
+ // Right now the template references myObservable, so there should be exactly one subscription on it
+ expect(testNode.childNodes[0]).toContainText("The value is some value");
+ expect(myObservable.getSubscriptionsCount()).toEqual(1);
+ var renderedNode1 = testNode.childNodes[0].childNodes[0];
+
+ // By changing the object for subModel, we force the data-bind value to be re-evaluated and the template to be re-rendered,
+ // setting up a new template subscription, so there have now existed two subscriptions on myObservable...
+ myModel.subModel({ myObservable: myObservable });
+ expect(testNode.childNodes[0].childNodes[0]).not.toEqual(renderedNode1);
+
+ // ...but, because the old subscription should have been disposed automatically, there should only be one left
+ expect(myObservable.getSubscriptionsCount()).toEqual(1);
+ });
+
+ it('Should be able to specify a template engine instance using data-bind syntax', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({ theTemplate: "Default output" })); // Not going to use this one
+ var alternativeTemplateEngine = new dummyTemplateEngine({ theTemplate: "Alternative output" });
+
+ testNode.innerHTML = "";
+ ko.applyBindings({ chosenEngine: alternativeTemplateEngine }, testNode);
+
+ expect(testNode.childNodes[0]).toContainText("Alternative output");
+ });
+
+ it('Should be able to bind $data to an alias using \'as\'', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ myTemplate: "ValueLiteral: [js:item.prop], ValueBound: "
+ }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someItem: { prop: 'Hello' } }, testNode);
+ expect(testNode.childNodes[0]).toContainText("ValueLiteral: Hello, ValueBound: Hello");
+ });
+
+ it('Data-bind syntax should expose parent binding context as $parent if binding with an explicit \"data\" value', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ myTemplate: "ValueLiteral: [js:$parent.parentProp], ValueBound: "
+ }));
+ testNode.innerHTML = "";
+ ko.applyBindings({ someItem: {}, parentProp: 'Hello' }, testNode);
+ expect(testNode.childNodes[0]).toContainText("ValueLiteral: Hello, ValueBound: Hello");
+ });
+
+ it('Data-bind syntax should expose all ancestor binding contexts as $parents', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "",
+ middleTemplate: "",
+ innerTemplate: "(Data:[js:$data.val], Parent:[[js:$parents[0].val]], Grandparent:[[js:$parents[1].val]], Root:[js:$root.val], Depth:[js:$parents.length])"
+ }));
+ testNode.innerHTML = "";
+
+ ko.applyBindings({
+ val: "ROOT",
+ outerItem: {
+ val: "OUTER",
+ middleItem: {
+ val: "MIDDLE",
+ innerItem: { val: "INNER" }
+ }
+ }
+ }, testNode);
+ expect(testNode.childNodes[0].childNodes[0]).toContainText("(Data:INNER, Parent:MIDDLE, Grandparent:OUTER, Root:ROOT, Depth:3)");
+ });
+
+ it('Should not be allowed to rewrite templates that embed anonymous templates', function() {
+ // The reason is that your template engine's native control flow and variable evaluation logic is going to run first, independently
+ // of any KO-native control flow, so variables would get evaluated in the wrong context. Example:
+ //
+ //
+ // ${ somePropertyOfEachArrayItem } <-- This gets evaluated *before* the foreach binds, so it can't reference array entries
+ //
+ //
+ // It should be perfectly OK to fix this just by preventing anonymous templates within rewritten templates, because
+ // (1) The developer can always use their template engine's native control flow syntax instead of the KO-native ones - that will work
+ // (2) The developer can use KO's native templating instead, if they are keen on KO-native control flow or anonymous templates
+
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ myTemplate: "
Childprop: [js: childProp]
"
+ }));
+ testNode.innerHTML = "";
+
+ var didThrow = false;
+ try {
+ ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
+ } catch(ex) {
+ didThrow = true;
+ expect(ex.message).toEqual("This template engine does not support anonymous templates nested within its templates");
+ }
+ expect(didThrow).toEqual(true);
+ });
+
+ it('Should not be allowed to rewrite templates that embed control flow bindings', function() {
+ // Same reason as above
+ ko.utils.arrayForEach(['if', 'ifnot', 'with', 'foreach'], function(bindingName) {
+ ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "
Hello
" }));
+ testNode.innerHTML = "";
+
+ var didThrow = false;
+ ko.utils.domData.clear(testNode);
+ try { ko.applyBindings({ someData: { childProp: 'abc' } }, testNode) }
+ catch (ex) {
+ didThrow = true;
+ expect(ex.message).toEqual("This template engine does not support the '" + bindingName + "' binding within its templates");
+ }
+ if (!didThrow)
+ throw new Error("Did not prevent use of " + bindingName);
+ });
+ });
+
+ it('Data binding syntax should permit nested templates using virtual containers (with arbitrary internal whitespace and newlines)', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ outerTemplate: "Outer ",
+ innerTemplate: "Inner via inline binding: "
+ }));
+ var model = { };
+ testNode.innerHTML = "";
+ ko.applyBindings(model, testNode);
+ expect(testNode.childNodes[0]).toContainHtml("outer inner via inline binding: sometext");
+ });
+
+ it('Should be able to render anonymous templates using virtual containers', function() {
+ ko.setTemplateEngine(new dummyTemplateEngine());
+ testNode.innerHTML = "Start Childprop: [js: childProp] End";
+ ko.applyBindings({ someData: { childProp: 'abc' } }, testNode);
+ expect(testNode).toContainHtml("start childprop: abcend");
+ });
+
+ it('Should be able to use anonymous templates that contain first-child comment nodes', function() {
+ // This represents issue https://github.com/SteveSanderson/knockout/issues/188
+ // (IE < 9 strips out leading comment nodes when you use .innerHTML)
+ ko.setTemplateEngine(new dummyTemplateEngine({}));
+ testNode.innerHTML = "start
');
+ });
+
+ it('Should allow anonymous templates output to include top-level virtual elements, and will bind their virtual children only once', function() {
+ delete (ko.bindingHandlers).nonexistentHandler;
+ var initCalls = 0;
+ (ko.bindingHandlers).countInits = { init: function () { initCalls++ } };
+ testNode.innerHTML = "
";
+ ko.applyBindings(null, testNode);
+ expect(initCalls).toEqual(1);
+ });
+
+ it('Should not throw errors if trying to apply text to a non-rendered node', function() {
+ // Represents https://github.com/SteveSanderson/knockout/issues/660
+ // A can't go directly into a
, so modern browsers will silently strip it. We need to verify this doesn't
+ // throw errors during unmemoization (when unmemoizing, it will try to apply the text to the following text node
+ // instead of the node you intended to bind to).
+ // Note that IE < 9 won't strip the
; instead it has much stranger behaviors regarding unexpected DOM structures.
+ // It just happens not to give an error in this particular case, though it would throw errors in many other cases
+ // of malformed template DOM.
+ ko.setTemplateEngine(new dummyTemplateEngine({
+ myTemplate: "