From 51bbb57e1db626500bf839f1b3bf4fc81f0b3682 Mon Sep 17 00:00:00 2001 From: Diullei Gomes Date: Fri, 22 Feb 2013 12:22:46 -0300 Subject: [PATCH] adjusted knockout typings; added 'templating' tests; the test file structure was refactored; #312 --- knockout/all-tests.ts | 2 + knockout/knockout.d.ts | 226 +++- .../knockout-templatingBehaviors-tests.ts | 886 +++++++++++++ knockout/{ => tests}/knockout-tests.ts | 1148 ++++++++--------- 4 files changed, 1682 insertions(+), 580 deletions(-) create mode 100644 knockout/all-tests.ts create mode 100644 knockout/tests/knockout-templatingBehaviors-tests.ts rename knockout/{ => tests}/knockout-tests.ts (96%) diff --git a/knockout/all-tests.ts b/knockout/all-tests.ts new file mode 100644 index 0000000000..3fe7a471f1 --- /dev/null +++ b/knockout/all-tests.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/knockout/knockout.d.ts b/knockout/knockout.d.ts index 58479d846e..aefcc797a4 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 0000000000..b000015f2e --- /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: "
The item is [js: personName]
" })); + testNode.innerHTML = "
"; + + ko.applyBindings({ myCollection: myArray }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
the item is bob
the item is frank
"); + 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: "
[js:name()]
" })); + testNode.innerHTML = "
"; + + // Bind against array, referencing an observable property + var myItem = { name: ko.observable("a") }; + ko.applyBindings({ items: [myItem] }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
a
"); + + // 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)]
" })); + testNode.innerHTML = "
"; + + ko.applyBindings({ myCollection: myArray }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
the item is bob
the item is steve
the item is another
"); + var originalBobNode = testNode.childNodes[0].childNodes[0]; + + myObservable("Steve2"); + expect(testNode.childNodes[0]).toContainHtml("
the item is bob
the item is steve2
the item is another
"); + 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: "
someProp=[js: someProp]
" })); + testNode.innerHTML = "
"; + + ko.applyBindings({ myCollection: myArray }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
someprop=1
someprop=3
someprop=4
"); + }); + + 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: "
someProp=[js: someProp]
" })); + testNode.innerHTML = "
"; + + ko.applyBindings({ myCollection: myArray }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
someprop=1
someprop=2
someprop=3
"); + }); + + it('Data binding syntax should support \"if\" condition', function() { + ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Value: [js: myProp().childProp]" })); + testNode.innerHTML = "
"; + + var viewModel = { myProp: ko.observable({ childProp: 'abc' }) }; + ko.applyBindings(viewModel, testNode); + + // Initially there is a value + expect(testNode.childNodes[0]).toContainText("Value: abc"); + + // Causing the condition to become false causes the output to be removed + viewModel.myProp(null); + expect(testNode.childNodes[0]).toContainText(""); + + // Causing the condition to become true causes the output to reappear + viewModel.myProp({ childProp: 'def' }); + expect(testNode.childNodes[0]).toContainText("Value: def"); + }); + + it('Data binding syntax should support \"ifnot\" condition', function() { + ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Hello" })); + testNode.innerHTML = "
"; + + var viewModel = { shouldHide: ko.observable(true) }; + ko.applyBindings(viewModel, testNode); + + // Initially there is no output (shouldHide=true) + expect(testNode.childNodes[0]).toContainText(""); + + // Causing the condition to become false causes the output to be displayed + viewModel.shouldHide(false); + expect(testNode.childNodes[0]).toContainText("Hello"); + + // Causing the condition to become true causes the output to disappear + viewModel.shouldHide(true); + expect(testNode.childNodes[0]).toContainText(""); + }); + + it('Data binding syntax should support \"if\" condition in conjunction with foreach', function() { + ko.setTemplateEngine(new dummyTemplateEngine({ myTemplate: "Value: [js: myProp().childProp]" })); + testNode.innerHTML = "
"; + + var viewModel = { myProp: ko.observable({ childProp: 'abc' }) }; + ko.applyBindings(viewModel, testNode); + expect(testNode.childNodes[0].childNodes[0].nodeValue).toEqual("Value: abc"); + expect(testNode.childNodes[0].childNodes[1].nodeValue).toEqual("Value: abc"); + expect(testNode.childNodes[0].childNodes[2].nodeValue).toEqual("Value: abc"); + + // Causing the condition to become false causes the output to be removed + viewModel.myProp(null); + expect(testNode.childNodes[0]).toContainText(""); + + // Causing the condition to become true causes the output to reappear + viewModel.myProp({ childProp: 'def' }); + expect(testNode.childNodes[0].childNodes[0].nodeValue).toEqual("Value: def"); + expect(testNode.childNodes[0].childNodes[1].nodeValue).toEqual("Value: def"); + expect(testNode.childNodes[0].childNodes[2].nodeValue).toEqual("Value: def"); + }); + + it('Should be able to populate checkboxes from inside templates, despite IE6 limitations', function () { + ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" })); + ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { isChecked: true } }, testNode); + expect(testNode.childNodes[0].checked).toEqual(true); + }); + + it('Should be able to populate radio buttons from inside templates, despite IE6 limitations', function () { + ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" })); + ko.renderTemplate("someTemplate", null, { templateRenderingVariablesInScope: { someValue: 'abc' } }, testNode); + expect(testNode.childNodes[0].checked).toEqual(true); + }); + + it('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() { + var myArray = new ko.observableArray([ + { preferredTemplate: 1, someProperty: 'firstItemValue' }, + { preferredTemplate: 2, someProperty: 'secondItemValue' } + ]); + ko.setTemplateEngine(new dummyTemplateEngine({ + firstTemplate: "
Template1Output, [js:someProperty]
", + secondTemplate: "
Template2Output, [js:someProperty]
" + })); + testNode.innerHTML = "
"; + + var getTemplate = function(dataItem, bindingContext) { + // Having the item's binding context available means you can read sibling or parent level properties + expect(bindingContext.$parent.anotherProperty).toEqual(123); + + return dataItem.preferredTemplate == 1 ? 'firstTemplate' : 'secondTemplate'; + }; + ko.applyBindings({ myCollection: myArray, getTemplateModelProperty: getTemplate, anotherProperty: 123 }, testNode); + expect(testNode.childNodes[0]).toContainHtml("
template1output, firstitemvalue
template2output, seconditemvalue
"); + }); + + it('Data binding \'templateOptions\' should be passed to template', function() { + var myModel = { + someAdditionalData: { myAdditionalProp: "someAdditionalValue" }, + people: new ko.observableArray([ + { name: "Alpha" }, + { name: "Beta" } + ]) + }; + ko.setTemplateEngine(new dummyTemplateEngine({myTemplate: "
Person [js:name] has additional property [js:templateOptions.myAdditionalProp]
"})); + testNode.innerHTML = "
"; + + ko.applyBindings(myModel, testNode); + expect(testNode.childNodes[0]).toContainHtml("
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
hello
"; + ko.applyBindings(null, testNode); + expect(testNode).toContainHtml('start
hellohello
'); + }); + + 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: " " // The whitespace after the closing span is what triggers the strange HTML parsing + })); + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); + // Since the actual template markup was invalid, we don't really care what the + // resulting DOM looks like. We are only verifying there were no exceptions. + }); +}) \ No newline at end of file diff --git a/knockout/knockout-tests.ts b/knockout/tests/knockout-tests.ts similarity index 96% rename from knockout/knockout-tests.ts rename to knockout/tests/knockout-tests.ts index d669af816e..97a4c8776f 100644 --- a/knockout/knockout-tests.ts +++ b/knockout/tests/knockout-tests.ts @@ -1,576 +1,574 @@ -/// -/// - -declare var $; - -function test_creatingVMs() { - var myViewModel = { - personName: ko.observable('Bob'), - personAge: ko.observable(123) - }; - ko.applyBindings(myViewModel); - ko.applyBindings(myViewModel, document.getElementById('someElementId')); - - myViewModel.personName(); - myViewModel.personName('Mary'); - myViewModel.personAge(50); - - myViewModel.personName.subscribe(function (newValue) { - alert("The person's new name is " + newValue); - }); - - var subscription = myViewModel.personName.subscribe(function (newValue) { }); - subscription.dispose(); -} - -function test_computed() { - function AppViewModel() { - var self = this; - - self.firstName = ko.observable('Bob'); - self.lastName = ko.observable('Smith'); - self.fullName = ko.computed(function () { - return self.firstName() + " " + self.lastName(); - }); - } - - function MyViewModel() { - this.firstName = ko.observable('Planet'); - this.lastName = ko.observable('Earth'); - - this.fullName = ko.computed({ - read: function () { - return this.firstName() + " " + this.lastName(); - }, - write: function (value) { - var lastSpacePos = value.lastIndexOf(" "); - if (lastSpacePos > 0) { - this.firstName(value.substring(0, lastSpacePos)); - this.lastName(value.substring(lastSpacePos + 1)); - } - }, - owner: this - }); - } - - function MyViewModel() { - this.price = ko.observable(25.99); - - this.formattedPrice = ko.computed({ - read: function () { - return '$' + this.price().toFixed(2); - }, - write: function (value) { - value = parseFloat(value.replace(/[^\.\d]/g, "")); - this.price(isNaN(value) ? 0 : value); - }, - owner: this - }); - } - - function MyViewModel() { - this.acceptedNumericValue = ko.observable(123); - this.lastInputWasValid = ko.observable(true); - - this.attemptedValue = ko.computed({ - read: this.acceptedNumericValue, - write: function (value) { - if (isNaN(value)) - this.lastInputWasValid(false); - else { - this.lastInputWasValid(true); - this.acceptedNumericValue(value); - } - }, - owner: this - }); - } - - ko.applyBindings(new MyViewModel()); -} - -class GetterViewModel { - private _selectedRange: KnockoutObservableAny; - - constructor() { - this._selectedRange = ko.observable(); - } - - public get range() : KnockoutObservableAny { - return this._selectedRange; - } -} - -function testGetter() { - var model = new GetterViewModel(); - - model.range.subscribe((range: number) => { - }); -} - -function test_observableArrays() { - var myObservableArray = ko.observableArray(); - myObservableArray.push('Some value'); - var anotherObservableArray = ko.observableArray([ - { name: "Bungle", type: "Bear" }, - { name: "George", type: "Hippo" }, - { name: "Zippy", type: "Unknown" } - ]); - - myObservableArray().length; - myObservableArray()[0]; - - myObservableArray.indexOf('Blah'); - myObservableArray.push('Some new value'); - myObservableArray.pop(); - myObservableArray.unshift('Some new value'); - myObservableArray.shift(); - myObservableArray.reverse(); - myObservableArray.sort(function (left, right) { return left.lastName == right.lastName ? 0 : (left.lastName < right.lastName ? -1 : 1) }); - myObservableArray.splice(1, 3); - - myObservableArray.remove('Blah'); - myObservableArray.remove(function (item) { return item.age < 18 }); - myObservableArray.removeAll(['Chad', 132, undefined]); - myObservableArray.removeAll(); - myObservableArray.destroy('Blah'); - myObservableArray.destroy(function (someItem) { return someItem.age < 18 }); - myObservableArray.destroyAll(['Chad', 132, undefined]); - myObservableArray.destroyAll(); - - ko.utils.arrayForEach(myObservableArray(), function (item) { }); -} - -// You have to extend knockout for your own handlers -interface KnockoutBindingHandlers { - yourBindingName: KnockoutBindingHandler; - slideVisible: KnockoutBindingHandler; - hasFocus: KnockoutBindingHandler; - allowBindings: KnockoutBindingHandler; - withProperties: KnockoutBindingHandler; - randomOrder: KnockoutBindingHandler; -} - -function test_bindings() { - var currentProfit = ko.observable(150000); - ko.applyBindings({ - people: [ - { firstName: 'Bert', lastName: 'Bertington' }, - { firstName: 'Charles', lastName: 'Charlesforth' }, - { firstName: 'Denise', lastName: 'Dentiste' } - ] - }); - var viewModel = { availableCountries: ko.observableArray(['France', 'Germany', 'Spain']) }; - viewModel.availableCountries.push('China'); - - ko.bindingHandlers.yourBindingName = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - }, - update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - } - }; - ko.bindingHandlers.slideVisible = { - update: function (element, valueAccessor, allBindingsAccessor) { - var value = valueAccessor(), allBindings = allBindingsAccessor(); - var valueUnwrapped = ko.utils.unwrapObservable(value); - var duration = allBindings.slideDuration || 400; - if (valueUnwrapped == true) - $(element).slideDown(duration); - else - $(element).slideUp(duration); - }, - init: function (element, valueAccessor) { - var value = ko.utils.unwrapObservable(valueAccessor()); - $(element).toggle(value); - }, - update: function (element, valueAccessor, allBindingsAccessor) { - } - }; - ko.bindingHandlers.hasFocus = { - init: function (element, valueAccessor) { - $(element).focus(function () { - var value = valueAccessor(); - value(true); - }); - $(element).blur(function () { - var value = valueAccessor(); - value(false); - }); - }, - update: function (element, valueAccessor) { - var value = valueAccessor(); - if (ko.utils.unwrapObservable(value)) - element.focus(); - else - element.blur(); - } - }; - ko.bindingHandlers.allowBindings = { - init: function (elem, valueAccessor) { - var shouldAllowBindings = ko.utils.unwrapObservable(valueAccessor()); - return { controlsDescendantBindings: !shouldAllowBindings }; - } - }; - ko.bindingHandlers.withProperties = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var newProperties = valueAccessor(), - innerBindingContext = bindingContext.extend(newProperties); - ko.applyBindingsToDescendants(innerBindingContext, element); - return { controlsDescendantBindings: true }; - } - }; - ko.bindingHandlers.withProperties = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var newProperties = valueAccessor(), - childBindingContext = bindingContext.createChildContext(viewModel); - ko.utils.extend(childBindingContext, newProperties); - ko.applyBindingsToDescendants(childBindingContext, element); - return { controlsDescendantBindings: true }; - } - }; - ko.bindingHandlers.randomOrder = { - init: function (elem, valueAccessor) { - var child = ko.virtualElements.firstChild(elem), - childElems = []; - while (child) { - childElems.push(child); - child = ko.virtualElements.nextSibling(child); - } - ko.virtualElements.emptyNode(elem); - while (childElems.length) { - var randomIndex = Math.floor(Math.random() * childElems.length), - chosenChild = childElems.splice(randomIndex, 1); - ko.virtualElements.prepend(elem, chosenChild[0]); - } - } - }; - - var node, containerElem, nodeToInsert, insertAfter, nodeToPrepend, arrayOfNodes; - ko.virtualElements.allowedBindings.mySuperBinding = true; - ko.virtualElements.emptyNode(containerElem); - ko.virtualElements.firstChild(containerElem); - ko.virtualElements.insertAfter(containerElem, nodeToInsert, insertAfter); - ko.virtualElements.nextSibling(node); - ko.virtualElements.prepend(containerElem, nodeToPrepend); - ko.virtualElements.setDomNodeChildren(containerElem, arrayOfNodes); -} - -// Have to define your own extenders -interface KnockoutExtenders { - logChange(target, option); - numeric(target, precision); - required(target, overrideMessage); -} - -interface KnockoutObservableArrayFunctions { - filterByProperty(propName, matchValue): KnockoutComputed; -} - -declare var validate; - -function test_more() { - var viewModel = { - firstName: ko.observable("Bert"), - lastName: ko.observable("Smith"), - pets: ko.observableArray(["Cat", "Dog", "Fish"]), - type: "Customer", - hasALotOfPets: null - }; - viewModel.hasALotOfPets = ko.computed(function () { - return this.pets().length > 2 - }, viewModel); - var plainJs = ko.toJS(viewModel); - - ko.extenders.logChange = function (target, option) { - target.subscribe(function (newValue) { - console.log(option + ": " + newValue); - }); - return target; - }; - - ko.extenders.numeric = function (target, precision) { - var result = ko.computed({ - read: target, - write: function (newValue) { - var current = target(), - roundingMultiplier = Math.pow(10, precision), - newValueAsNum = isNaN(newValue) ? 0 : parseFloat(newValue), - valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier; - - if (valueToWrite !== current) { - target(valueToWrite); - } else { - if (newValue !== current) { - target.notifySubscribers(valueToWrite); - } - } - } - }); - - result(target()); - - return result; - }; - - function AppViewModel(one, two) { - this.myNumberOne = ko.observable(one).extend({ numeric: 0 }); - this.myNumberTwo = ko.observable(two).extend({ numeric: 2 }); - } - - ko.applyBindings(new AppViewModel(221.2234, 123.4525)); - - ko.extenders.required = function (target, overrideMessage) { - - target.hasError = ko.observable(); - target.validationMessage = ko.observable(); - - function validate(newValue) { - target.hasError(newValue ? false : true); - target.validationMessage(newValue ? "" : overrideMessage || "This field is required"); - } - - validate(target()); - - target.subscribe(validate); - - return target; - }; - - function AppViewModel(first, last) { - this.firstName = ko.observable(first).extend({ required: "Please enter a first name" }); - this.lastName = ko.observable(last).extend({ required: "" }); - } - - ko.applyBindings(new AppViewModel("Bob", "Smith")); - - var first; - this.firstName = ko.observable(first).extend({ required: "Please enter a first name", logChange: "first name" }); - - var upperCaseName = ko.computed(function () { - return name.toUpperCase(); - }).extend({ throttle: 500 }); - - function AppViewModel() { - this.instantaneousValue = ko.observable(); - this.throttledValue = ko.computed(this.instantaneousValue) - .extend({ throttle: 400 }); - - this.loggedValues = ko.observableArray([]); - this.throttledValue.subscribe(function (val) { - if (val !== '') - this.loggedValues.push(val); - }, this); - } - - function GridViewModel() { - this.pageSize = ko.observable(20); - this.pageIndex = ko.observable(1); - this.currentPageData = ko.observableArray(); - - ko.computed(function () { - var params = { page: this.pageIndex(), size: this.pageSize() }; - $.getJSON('/Some/Json/Service', params, this.currentPageData); - }, this); - } - this.setPageSize = function (newPageSize) { - this.pageSize(newPageSize); - this.pageIndex(1); - } - - ko.computed(function () { - var params = { page: this.pageIndex(), size: this.pageSize() }; - $.getJSON('/Some/Json/Service', params, this.currentPageData); - }, this).extend({ throttle: 1 }); - - $(".remove").click(function () { - viewModel.pets.remove(ko.dataFor(this)); - }); - $(".remove").live("click", function () { - viewModel.pets.remove(ko.dataFor(this)); - }); - - $("#people").delegate(".remove", "click", function () { - - var context = ko.contextFor(this), - parentArray = context.$parent.people || context.$parent.children; - - parentArray.remove(context.$data); - - return false; - }); - $("#people").delegate(".add", "click", function () { - var context = ko.contextFor(this), - childName = context.$data.name() + " child", - parentArray = context.$data.people || context.$data.children; - - context.$root.addChild(childName, parentArray); - - return false; - }); - ko.observableArray.fn.filterByProperty = function (propName, matchValue) { - return ko.computed(function () { - var allItems = this(), matchingItems = []; - for (var i = 0; i < allItems.length; i++) { - var current = allItems[i]; - if (ko.utils.unwrapObservable(current[propName]) === matchValue) - matchingItems.push(current); - } - return matchingItems; - }, this); - } - function Task(title, done) { - this.title = ko.observable(title); - this.done = ko.observable(done); - } - - function AppViewModel() { - this.tasks = ko.observableArray([ - new Task('Find new desktop background', true), - new Task('Put shiny stickers on laptop', false), - new Task('Request more reggae music in the office', true) - ]); - - this.doneTasks = this.tasks.filterByProperty("done", true); - } - - ko.applyBindings(new AppViewModel()); - this.doneTasks = ko.computed(function () { - var all = this.tasks(), done = []; - for (var i = 0; i < all.length; i++) - if (all[i].done()) - done.push(all[i]); - return done; - }, this); -} - -function test_mappingplugin() { - var viewModel = { - serverTime: ko.observable(), - numUsers: ko.observable() - } - var data = { - serverTime: '2010-01-07', - numUsers: 3 - }; - viewModel.serverTime(data.serverTime); - viewModel.numUsers(data.numUsers); - - var viewModel = ko.mapping.fromJS(data); - ko.mapping.fromJS(data, viewModel); - var unmapped = ko.mapping.toJS(viewModel); - - var viewModel = ko.mapping.fromJS(data); - ko.mapping.fromJS(data, viewModel); - - var myChildModel = function (data) { - ko.mapping.fromJS(data, {}, this); - - this.nameLength = ko.computed(function () { - return this.name().length; - }, this); - } - - var oldOptions = ko.mapping.defaultOptions().include; - ko.mapping.defaultOptions().include = ["alwaysIncludeThis"]; - - var oldOptions = ko.mapping.defaultOptions().copy; - ko.mapping.defaultOptions().copy = ["alwaysCopyThis"]; - - var someObject; - ko.mapping.fromJS(data, {}, someObject); - ko.mapping.fromJS(data, {}, this); - - var alice, aliceMappingOptions, bob, bobMappingOptions; - var viewModel = ko.mapping.fromJS(alice, aliceMappingOptions); - ko.mapping.fromJS(bob, bobMappingOptions, viewModel); - - var obj; - var result = ko.mapping.fromJS(obj, { - key: function (item) { - return ko.utils.unwrapObservable(item.id); - } - }); - - result.mappedRemove({ id: 2 }); - var newItem = result.mappedCreate({ id: 3 }); -} - -// Define your own functions -interface KnockoutSubscribableFunctions { - publishOn(topic: string): any; - subscribeTo(topic: string): any; -} - -interface KnockoutBindingHandlers { - isolatedOptions: KnockoutBindingHandler; -} - -function test_misc() { - // define dummy vars - var callback: any; - var target: any; - var topic: any; - var vm: any; - var value: any; - - var postbox = new ko.subscribable(); - postbox.subscribe(callback, target, topic); - - postbox.subscribe(function (newValue) { - this.latestTopic(newValue); - }, vm, "mytopic"); - postbox.notifySubscribers(value, "mytopic"); - - ko.subscribable.fn.publishOn = function (topic) { - this.subscribe(function (newValue) { - postbox.notifySubscribers(newValue, topic); - }); - - return this; - }; - - this.myObservable = ko.observable("myValue").publishOn("myTopic"); - - ko.subscribable.fn.subscribeTo = function (topic) { - postbox.subscribe(this, null, topic); - - return this; - }; - - this.observableFromAnotherVM = ko.observable().subscribeTo("myTopic"); - - postbox.subscribe(function (newValue) { - this(newValue); - }, this, topic); - - ko.bindingHandlers.isolatedOptions = { - init: function (element, valueAccessor) { - var args = arguments; - ko.computed({ - read: function () { - ko.utils.unwrapObservable(valueAccessor()); - ko.bindingHandlers.options.update.apply(this, args); - }, - owner: this, - disposeWhenNodeIsRemoved: element - }); - } - }; - - ko.subscribable.fn.publishOn = function (topic) { - this.subscribe(function (newValue) { - postbox.notifySubscribers(newValue, topic); - }); - - return this; - }; - - this.myObservable = ko.observable("myValue").publishOn("myTopic"); - - var x = ko.observableArray([1, 2, 3]); - - var element; - ko.utils.domNodeDisposal.addDisposeCallback(element, function () { - $(element).datepicker("destroy"); - }); +/// +/// + +declare var $; + +function test_creatingVMs() { + var myViewModel = { + personName: ko.observable('Bob'), + personAge: ko.observable(123) + }; + ko.applyBindings(myViewModel); + ko.applyBindings(myViewModel, document.getElementById('someElementId')); + + myViewModel.personName(); + myViewModel.personName('Mary'); + myViewModel.personAge(50); + + myViewModel.personName.subscribe(function (newValue) { + alert("The person's new name is " + newValue); + }); + + var subscription = myViewModel.personName.subscribe(function (newValue) { }); + subscription.dispose(); +} + +function test_computed() { + function AppViewModel() { + var self = this; + + self.firstName = ko.observable('Bob'); + self.lastName = ko.observable('Smith'); + self.fullName = ko.computed(function () { + return self.firstName() + " " + self.lastName(); + }); + } + + function MyViewModel() { + this.firstName = ko.observable('Planet'); + this.lastName = ko.observable('Earth'); + + this.fullName = ko.computed({ + read: function () { + return this.firstName() + " " + this.lastName(); + }, + write: function (value) { + var lastSpacePos = value.lastIndexOf(" "); + if (lastSpacePos > 0) { + this.firstName(value.substring(0, lastSpacePos)); + this.lastName(value.substring(lastSpacePos + 1)); + } + }, + owner: this + }); + } + + function MyViewModel() { + this.price = ko.observable(25.99); + + this.formattedPrice = ko.computed({ + read: function () { + return '$' + this.price().toFixed(2); + }, + write: function (value) { + value = parseFloat(value.replace(/[^\.\d]/g, "")); + this.price(isNaN(value) ? 0 : value); + }, + owner: this + }); + } + + function MyViewModel() { + this.acceptedNumericValue = ko.observable(123); + this.lastInputWasValid = ko.observable(true); + + this.attemptedValue = ko.computed({ + read: this.acceptedNumericValue, + write: function (value) { + if (isNaN(value)) + this.lastInputWasValid(false); + else { + this.lastInputWasValid(true); + this.acceptedNumericValue(value); + } + }, + owner: this + }); + } + + ko.applyBindings(new MyViewModel()); +} + +class GetterViewModel { + private _selectedRange: KnockoutObservableAny; + + constructor() { + this._selectedRange = ko.observable(); + } + + public range: KnockoutObservableAny; +} + +function testGetter() { + var model = new GetterViewModel(); + + model.range.subscribe((range: number) => { + }); +} + +function test_observableArrays() { + var myObservableArray = ko.observableArray(); + myObservableArray.push('Some value'); + var anotherObservableArray = ko.observableArray([ + { name: "Bungle", type: "Bear" }, + { name: "George", type: "Hippo" }, + { name: "Zippy", type: "Unknown" } + ]); + + myObservableArray().length; + myObservableArray()[0]; + + myObservableArray.indexOf('Blah'); + myObservableArray.push('Some new value'); + myObservableArray.pop(); + myObservableArray.unshift('Some new value'); + myObservableArray.shift(); + myObservableArray.reverse(); + myObservableArray.sort(function (left, right) { return left.lastName == right.lastName ? 0 : (left.lastName < right.lastName ? -1 : 1) }); + myObservableArray.splice(1, 3); + + myObservableArray.remove('Blah'); + myObservableArray.remove(function (item) { return item.age < 18 }); + myObservableArray.removeAll(['Chad', 132, undefined]); + myObservableArray.removeAll(); + myObservableArray.destroy('Blah'); + myObservableArray.destroy(function (someItem) { return someItem.age < 18 }); + myObservableArray.destroyAll(['Chad', 132, undefined]); + myObservableArray.destroyAll(); + + ko.utils.arrayForEach(myObservableArray(), function (item) { }); +} + +// You have to extend knockout for your own handlers +interface KnockoutBindingHandlers { + yourBindingName: KnockoutBindingHandler; + slideVisible: KnockoutBindingHandler; + hasFocus: KnockoutBindingHandler; + allowBindings: KnockoutBindingHandler; + withProperties: KnockoutBindingHandler; + randomOrder: KnockoutBindingHandler; +} + +function test_bindings() { + var currentProfit = ko.observable(150000); + ko.applyBindings({ + people: [ + { firstName: 'Bert', lastName: 'Bertington' }, + { firstName: 'Charles', lastName: 'Charlesforth' }, + { firstName: 'Denise', lastName: 'Dentiste' } + ] + }); + var viewModel = { availableCountries: ko.observableArray(['France', 'Germany', 'Spain']) }; + viewModel.availableCountries.push('China'); + + ko.bindingHandlers.yourBindingName = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + }, + update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + } + }; + ko.bindingHandlers.slideVisible = { + update: function (element, valueAccessor, allBindingsAccessor) { + var value = valueAccessor(), allBindings = allBindingsAccessor(); + var valueUnwrapped = ko.utils.unwrapObservable(value); + var duration = allBindings.slideDuration || 400; + if (valueUnwrapped == true) + $(element).slideDown(duration); + else + $(element).slideUp(duration); + }, + init: function (element, valueAccessor) { + var value = ko.utils.unwrapObservable(valueAccessor()); + $(element).toggle(value); + }, + update: function (element, valueAccessor, allBindingsAccessor) { + } + }; + ko.bindingHandlers.hasFocus = { + init: function (element, valueAccessor) { + $(element).focus(function () { + var value = valueAccessor(); + value(true); + }); + $(element).blur(function () { + var value = valueAccessor(); + value(false); + }); + }, + update: function (element, valueAccessor) { + var value = valueAccessor(); + if (ko.utils.unwrapObservable(value)) + element.focus(); + else + element.blur(); + } + }; + ko.bindingHandlers.allowBindings = { + init: function (elem, valueAccessor) { + var shouldAllowBindings = ko.utils.unwrapObservable(valueAccessor()); + return { controlsDescendantBindings: !shouldAllowBindings }; + } + }; + ko.bindingHandlers.withProperties = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var newProperties = valueAccessor(), + innerBindingContext = bindingContext.extend(newProperties); + ko.applyBindingsToDescendants(innerBindingContext, element); + return { controlsDescendantBindings: true }; + } + }; + ko.bindingHandlers.withProperties = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var newProperties = valueAccessor(), + childBindingContext = bindingContext.createChildContext(viewModel); + ko.utils.extend(childBindingContext, newProperties); + ko.applyBindingsToDescendants(childBindingContext, element); + return { controlsDescendantBindings: true }; + } + }; + ko.bindingHandlers.randomOrder = { + init: function (elem, valueAccessor) { + var child = ko.virtualElements.firstChild(elem), + childElems = []; + while (child) { + childElems.push(child); + child = ko.virtualElements.nextSibling(child); + } + ko.virtualElements.emptyNode(elem); + while (childElems.length) { + var randomIndex = Math.floor(Math.random() * childElems.length), + chosenChild = childElems.splice(randomIndex, 1); + ko.virtualElements.prepend(elem, chosenChild[0]); + } + } + }; + + var node, containerElem, nodeToInsert, insertAfter, nodeToPrepend, arrayOfNodes; + ko.virtualElements.allowedBindings.mySuperBinding = true; + ko.virtualElements.emptyNode(containerElem); + ko.virtualElements.firstChild(containerElem); + ko.virtualElements.insertAfter(containerElem, nodeToInsert, insertAfter); + ko.virtualElements.nextSibling(node); + ko.virtualElements.prepend(containerElem, nodeToPrepend); + ko.virtualElements.setDomNodeChildren(containerElem, arrayOfNodes); +} + +// Have to define your own extenders +interface KnockoutExtenders { + logChange(target, option); + numeric(target, precision); + required(target, overrideMessage); +} + +interface KnockoutObservableArrayFunctions { + filterByProperty(propName, matchValue): KnockoutComputed; +} + +declare var validate; + +function test_more() { + var viewModel = { + firstName: ko.observable("Bert"), + lastName: ko.observable("Smith"), + pets: ko.observableArray(["Cat", "Dog", "Fish"]), + type: "Customer", + hasALotOfPets: null + }; + viewModel.hasALotOfPets = ko.computed(function () { + return this.pets().length > 2 + }, viewModel); + var plainJs = ko.toJS(viewModel); + + ko.extenders.logChange = function (target, option) { + target.subscribe(function (newValue) { + console.log(option + ": " + newValue); + }); + return target; + }; + + ko.extenders.numeric = function (target, precision) { + var result = ko.computed({ + read: target, + write: function (newValue) { + var current = target(), + roundingMultiplier = Math.pow(10, precision), + newValueAsNum = isNaN(newValue) ? 0 : parseFloat(newValue), + valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier; + + if (valueToWrite !== current) { + target(valueToWrite); + } else { + if (newValue !== current) { + target.notifySubscribers(valueToWrite); + } + } + } + }); + + result(target()); + + return result; + }; + + function AppViewModel(one, two) { + this.myNumberOne = ko.observable(one).extend({ numeric: 0 }); + this.myNumberTwo = ko.observable(two).extend({ numeric: 2 }); + } + + ko.applyBindings(new AppViewModel(221.2234, 123.4525)); + + ko.extenders.required = function (target, overrideMessage) { + + target.hasError = ko.observable(); + target.validationMessage = ko.observable(); + + function validate(newValue) { + target.hasError(newValue ? false : true); + target.validationMessage(newValue ? "" : overrideMessage || "This field is required"); + } + + validate(target()); + + target.subscribe(validate); + + return target; + }; + + function AppViewModel(first, last) { + this.firstName = ko.observable(first).extend({ required: "Please enter a first name" }); + this.lastName = ko.observable(last).extend({ required: "" }); + } + + ko.applyBindings(new AppViewModel("Bob", "Smith")); + + var first; + this.firstName = ko.observable(first).extend({ required: "Please enter a first name", logChange: "first name" }); + + var upperCaseName = ko.computed(function () { + return name.toUpperCase(); + }).extend({ throttle: 500 }); + + function AppViewModel() { + this.instantaneousValue = ko.observable(); + this.throttledValue = ko.computed(this.instantaneousValue) + .extend({ throttle: 400 }); + + this.loggedValues = ko.observableArray([]); + this.throttledValue.subscribe(function (val) { + if (val !== '') + this.loggedValues.push(val); + }, this); + } + + function GridViewModel() { + this.pageSize = ko.observable(20); + this.pageIndex = ko.observable(1); + this.currentPageData = ko.observableArray(); + + ko.computed(function () { + var params = { page: this.pageIndex(), size: this.pageSize() }; + $.getJSON('/Some/Json/Service', params, this.currentPageData); + }, this); + } + this.setPageSize = function (newPageSize) { + this.pageSize(newPageSize); + this.pageIndex(1); + } + + ko.computed(function () { + var params = { page: this.pageIndex(), size: this.pageSize() }; + $.getJSON('/Some/Json/Service', params, this.currentPageData); + }, this).extend({ throttle: 1 }); + + $(".remove").click(function () { + viewModel.pets.remove(ko.dataFor(this)); + }); + $(".remove").live("click", function () { + viewModel.pets.remove(ko.dataFor(this)); + }); + + $("#people").delegate(".remove", "click", function () { + + var context = ko.contextFor(this), + parentArray = context.$parent.people || context.$parent.children; + + parentArray.remove(context.$data); + + return false; + }); + $("#people").delegate(".add", "click", function () { + var context = ko.contextFor(this), + childName = context.$data.name() + " child", + parentArray = context.$data.people || context.$data.children; + + context.$root.addChild(childName, parentArray); + + return false; + }); + ko.observableArray.fn.filterByProperty = function (propName, matchValue) { + return ko.computed(function () { + var allItems = this(), matchingItems = []; + for (var i = 0; i < allItems.length; i++) { + var current = allItems[i]; + if (ko.utils.unwrapObservable(current[propName]) === matchValue) + matchingItems.push(current); + } + return matchingItems; + }, this); + } + function Task(title, done) { + this.title = ko.observable(title); + this.done = ko.observable(done); + } + + function AppViewModel() { + this.tasks = ko.observableArray([ + new Task('Find new desktop background', true), + new Task('Put shiny stickers on laptop', false), + new Task('Request more reggae music in the office', true) + ]); + + this.doneTasks = this.tasks.filterByProperty("done", true); + } + + ko.applyBindings(new AppViewModel()); + this.doneTasks = ko.computed(function () { + var all = this.tasks(), done = []; + for (var i = 0; i < all.length; i++) + if (all[i].done()) + done.push(all[i]); + return done; + }, this); +} + +function test_mappingplugin() { + var viewModel = { + serverTime: ko.observable(), + numUsers: ko.observable() + } + var data = { + serverTime: '2010-01-07', + numUsers: 3 + }; + viewModel.serverTime(data.serverTime); + viewModel.numUsers(data.numUsers); + + var viewModel = ko.mapping.fromJS(data); + ko.mapping.fromJS(data, viewModel); + var unmapped = ko.mapping.toJS(viewModel); + + var viewModel = ko.mapping.fromJS(data); + ko.mapping.fromJS(data, viewModel); + + var myChildModel = function (data) { + ko.mapping.fromJS(data, {}, this); + + this.nameLength = ko.computed(function () { + return this.name().length; + }, this); + } + + var oldOptions = ko.mapping.defaultOptions().include; + ko.mapping.defaultOptions().include = ["alwaysIncludeThis"]; + + var oldOptions = ko.mapping.defaultOptions().copy; + ko.mapping.defaultOptions().copy = ["alwaysCopyThis"]; + + var someObject; + ko.mapping.fromJS(data, {}, someObject); + ko.mapping.fromJS(data, {}, this); + + var alice, aliceMappingOptions, bob, bobMappingOptions; + var viewModel = ko.mapping.fromJS(alice, aliceMappingOptions); + ko.mapping.fromJS(bob, bobMappingOptions, viewModel); + + var obj; + var result = ko.mapping.fromJS(obj, { + key: function (item) { + return ko.utils.unwrapObservable(item.id); + } + }); + + result.mappedRemove({ id: 2 }); + var newItem = result.mappedCreate({ id: 3 }); +} + +// Define your own functions +interface KnockoutSubscribableFunctions { + publishOn(topic: string): any; + subscribeTo(topic: string): any; +} + +interface KnockoutBindingHandlers { + isolatedOptions: KnockoutBindingHandler; +} + +function test_misc() { + // define dummy vars + var callback: any; + var target: any; + var topic: any; + var vm: any; + var value: any; + + var postbox = new ko.subscribable(); + postbox.subscribe(callback, target, topic); + + postbox.subscribe(function (newValue) { + this.latestTopic(newValue); + }, vm, "mytopic"); + postbox.notifySubscribers(value, "mytopic"); + + ko.subscribable.fn.publishOn = function (topic) { + this.subscribe(function (newValue) { + postbox.notifySubscribers(newValue, topic); + }); + + return this; + }; + + this.myObservable = ko.observable("myValue").publishOn("myTopic"); + + ko.subscribable.fn.subscribeTo = function (topic) { + postbox.subscribe(this, null, topic); + + return this; + }; + + this.observableFromAnotherVM = ko.observable().subscribeTo("myTopic"); + + postbox.subscribe(function (newValue) { + this(newValue); + }, this, topic); + + ko.bindingHandlers.isolatedOptions = { + init: function (element, valueAccessor) { + var args = arguments; + ko.computed({ + read: function () { + ko.utils.unwrapObservable(valueAccessor()); + ko.bindingHandlers.options.update.apply(this, args); + }, + owner: this, + disposeWhenNodeIsRemoved: element + }); + } + }; + + ko.subscribable.fn.publishOn = function (topic) { + this.subscribe(function (newValue) { + postbox.notifySubscribers(newValue, topic); + }); + + return this; + }; + + this.myObservable = ko.observable("myValue").publishOn("myTopic"); + + var x = ko.observableArray([1, 2, 3]); + + var element; + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + $(element).datepicker("destroy"); + }); } \ No newline at end of file