From 2725dc0c999c9e9197724e28880c0f87366cb2ed Mon Sep 17 00:00:00 2001 From: GilesBradshaw Date: Thu, 15 Mar 2012 13:40:26 +0000 Subject: [PATCH 1/7] ignoring my visual studio files --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 7f54548c1..537d6e9e6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ desktop.ini .eprj perf/* *.orig + +MBestKnockout.csproj +MBestKnockout.sln +Properties/AssemblyInfo.cs +Web.config +Web.Debug.config +Web.Release.config \ No newline at end of file From 553a8307360e1748d20c5433b8c5b94069f3c519 Mon Sep 17 00:00:00 2001 From: GilesBradshaw Date: Thu, 15 Mar 2012 16:32:21 +0000 Subject: [PATCH 2/7] Allow configuration of name, bindingAttribute and virtualElementTag for bindingProviders --- spec/bindingAttributeBehaviors.js | 1659 ++++++++++++++------------- src/binding/bindingProvider.js | 40 +- src/templating/templateRewriting.js | 16 +- src/virtualElements.js | 14 +- 4 files changed, 886 insertions(+), 843 deletions(-) diff --git a/spec/bindingAttributeBehaviors.js b/spec/bindingAttributeBehaviors.js index 8179bb7ea..6ff95255f 100644 --- a/spec/bindingAttributeBehaviors.js +++ b/spec/bindingAttributeBehaviors.js @@ -1,867 +1,886 @@ -describe('Binding attribute syntax', { - before_each: function () { - var existingNode = document.getElementById("testNode"); - if (existingNode != null) - existingNode.parentNode.removeChild(existingNode); - testNode = document.createElement("div"); - testNode.id = "testNode"; - document.body.appendChild(testNode); - }, - - 'applyBindings should accept no parameters and then act on document.body with undefined model': function() { - var didInit = false; - ko.bindingHandlers.test = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel) { - value_of(element.id).should_be("testElement"); - value_of(viewModel).should_be(undefined); - didInit = true; - } - }; - testNode.innerHTML = "
"; - ko.applyBindings(); - value_of(didInit).should_be(true); - - // Just to avoid interfering with other specs: - ko.utils.domData.clear(document.body); - }, - - 'applyBindings should accept one parameter and then act on document.body with parameter as model': function() { - var didInit = false; - var suppliedViewModel = {}; - ko.bindingHandlers.test = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel) { - value_of(element.id).should_be("testElement"); - value_of(viewModel).should_be(suppliedViewModel); - didInit = true; - } - }; - testNode.innerHTML = "
"; - ko.applyBindings(suppliedViewModel); - value_of(didInit).should_be(true); - - // Just to avoid interfering with other specs: - ko.utils.domData.clear(document.body); - }, - - 'applyBindings should accept two parameters and then act on second param as DOM node with first param as model': function() { - var didInit = false; - var suppliedViewModel = {}; - ko.bindingHandlers.test = { - init: function (element, valueAccessor, allBindingsAccessor, viewModel) { - value_of(element.id).should_be("testElement"); - value_of(viewModel).should_be(suppliedViewModel); - didInit = true; - } - }; - testNode.innerHTML = "
"; - var shouldNotMatchNode = document.createElement("DIV"); - shouldNotMatchNode.innerHTML = "
"; - document.body.appendChild(shouldNotMatchNode); - try { - ko.applyBindings(suppliedViewModel, testNode); - value_of(didInit).should_be(true); - } finally { - shouldNotMatchNode.parentNode.removeChild(shouldNotMatchNode); - } - }, - - 'Should tolerate whitespace and nonexistent handlers': function () { - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); // No exception means success - }, - - 'Should tolerate arbitrary literals as the values for a handler': function () { - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); // No exception means success - }, - - 'Should tolerate wacky IE conditional comments': function() { - // Represents issue https://github.com/SteveSanderson/knockout/issues/186. Would fail on IE9, but work on earlier IE versions. - testNode.innerHTML = "
Hello
"; - ko.applyBindings(null, testNode); // No exception means success - }, - - 'Should invoke registered handlers\' init() then update() methods passing binding data': function () { - var methodsInvoked = []; - ko.bindingHandlers.test = { - init: function (element, valueAccessor, allBindingsAccessor) { - methodsInvoked.push("init"); - value_of(element.id).should_be("testElement"); - value_of(valueAccessor()).should_be("Hello"); - value_of(allBindingsAccessor().another).should_be(123); - }, - update: function (element, valueAccessor, allBindingsAccessor) { - methodsInvoked.push("update"); - value_of(element.id).should_be("testElement"); - value_of(valueAccessor()).should_be("Hello"); - value_of(allBindingsAccessor().another).should_be(123); - } - } - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); - value_of(methodsInvoked.length).should_be(2); - value_of(methodsInvoked[0]).should_be("init"); - value_of(methodsInvoked[1]).should_be("update"); - }, - - 'If the binding handler depends on an observable, invokes the init handler once and the update handler whenever a new value is available': function () { - var observable = new ko.observable(); - var initPassedValues = [], updatePassedValues = []; - ko.bindingHandlers.test = { - init: function (element, valueAccessor) { initPassedValues.push(valueAccessor()()); }, - update: function (element, valueAccessor) { updatePassedValues.push(valueAccessor()()); } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode); - value_of(initPassedValues.length).should_be(1); - value_of(updatePassedValues.length).should_be(1); - value_of(initPassedValues[0]).should_be(undefined); - value_of(updatePassedValues[0]).should_be(undefined); - - observable("A"); - value_of(initPassedValues.length).should_be(1); - value_of(updatePassedValues.length).should_be(2); - value_of(updatePassedValues[1]).should_be("A"); - }, - - 'If the associated DOM element was removed by KO, handler subscriptions are disposed immediately': function () { - var observable = new ko.observable("A"); - ko.bindingHandlers.anyHandler = { - update: function (element, valueAccessor) { valueAccessor(); } - }; - testNode.innerHTML = "
"; - ko.applyBindings({ myObservable: observable }, testNode); - - value_of(observable.getSubscriptionsCount()).should_be(1); +var bindingAttributeBehaviors = - ko.cleanAndRemoveNode(testNode); - - value_of(observable.getSubscriptionsCount()).should_be(0); - }, - - 'If the associated DOM element was removed independently of KO, handler subscriptions are disposed on the next evaluation': function () { - var observable = new ko.observable("A"); - ko.bindingHandlers.anyHandler = { - update: function (element, valueAccessor) { valueAccessor(); } - }; - testNode.innerHTML = "
"; - ko.applyBindings({ myObservable: observable }, testNode); - - value_of(observable.getSubscriptionsCount()).should_be(1); - - testNode.parentNode.removeChild(testNode); - observable("B"); // Force re-evaluation - - value_of(observable.getSubscriptionsCount()).should_be(0); - }, - - 'If the binding attribute involves an observable, re-invokes the bindings if the observable notifies a change': function () { - var observable = new ko.observable({ message: "hello" }); - var passedValues = []; - ko.bindingHandlers.test = { update: function (element, valueAccessor) { passedValues.push(valueAccessor()); } }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode); - value_of(passedValues.length).should_be(1); - value_of(passedValues[0]).should_be("hello"); - - observable({ message: "goodbye" }); - value_of(passedValues.length).should_be(2); - value_of(passedValues[1]).should_be("goodbye"); - }, +function (testConfiguration) { + var existingBindingProvider = ko.bindingProvider.instance; + var myConfiguration = testConfiguration; - 'Should be able to refer to the bound object itself (at the root scope, the viewmodel) via $data': function() { - testNode.innerHTML = "
"; - ko.applyBindings({ someProp: 'My prop value' }, testNode); - value_of(testNode).should_contain_text("My prop value"); - }, - - 'Should be able to update bindings (including callbacks) using an observable view model': function() { - testNode.innerHTML = ""; - var input = testNode.childNodes[0], vm = ko.observable({ someProp: 'My prop value' }); - ko.applyBindings(vm, input); - - value_of(input.value).should_be("My prop value"); - - // a change to the input value should be written to the model - input.value = "some user-entered value"; - ko.utils.triggerEvent(input, "change"); - value_of(vm().someProp).should_be("some user-entered value"); - - // set the view-model to a new object - vm({ someProp: ko.observable('My new prop value') }); - value_of(input.value).should_be("My new prop value"); - - // a change to the input value should be written to the new model - input.value = "some new user-entered value"; - ko.utils.triggerEvent(input, "change"); - value_of(vm().someProp()).should_be("some new user-entered value"); - - // clear the element and the view-model (shouldn't be any errors) - testNode.innerHTML = ""; - vm(null); - }, - - 'Updates to an observable view model should update all child contexts (uncluding values copied from the parent)': function() { - ko.bindingHandlers.setChildContext = { - flags: ko.bindingFlags.contentBind, - init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - ko.applyBindingsToDescendants( - bindingContext.createChildContext(function() { return ko.utils.unwrapObservable(valueAccessor()) }), - element, true); - } - }; + describe('Binding attribute syntax ' + (myConfiguration ? myConfiguration.name : "default provider settings"), { + before_each: function () { + var existingNode = document.getElementById("testNode"); + if (existingNode != null) + existingNode.parentNode.removeChild(existingNode); + testNode = document.createElement("div"); + testNode.id = "testNode"; + document.body.appendChild(testNode); + ko.bindingProvider["instance"] = new ko.bindingProvider(myConfiguration); + }, - testNode.innerHTML = "
"; - var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view model"}); - ko.applyBindings(vm, testNode); - value_of(testNode).should_contain_text("First view model"); - - // change view model to new object - vm({obj1: {prop1: "Second view "}, prop2: "model"}); - value_of(testNode).should_contain_text("Second view model"); - - // change it again - vm({obj1: {prop1: "Third view model"}, prop2: ""}); - value_of(testNode).should_contain_text("Third view model"); - }, - - 'Should be able to get all updates to observables in both init and update': function() { - var lastBoundValueInit, lastBoundValueUpdate; - ko.bindingHandlers.testInit = { - init: function(element, valueAccessor) { - ko.dependentObservable(function() { - lastBoundValueInit = ko.utils.unwrapObservable(valueAccessor()); - }); - } - }; - ko.bindingHandlers.testUpdate = { - update: function(element, valueAccessor) { - lastBoundValueUpdate = ko.utils.unwrapObservable(valueAccessor()); - } - }; - testNode.innerHTML = "
"; - var vm = ko.observable({ myProp: ko.observable("initial value") }); - ko.applyBindings(vm, testNode); - value_of(lastBoundValueInit).should_be("initial value"); - value_of(lastBoundValueUpdate).should_be("initial value"); - - // update value of observable - vm().myProp("second value"); - value_of(lastBoundValueInit).should_be("second value"); - value_of(lastBoundValueUpdate).should_be("second value"); - - // update value of observable to another observable - vm().myProp(ko.observable("third value")); - value_of(lastBoundValueInit).should_be("third value"); - value_of(lastBoundValueUpdate).should_be("third value"); - - // update view model with brand-new property - vm({ myProp: function() {return "fourth value"; }}); - value_of(lastBoundValueInit).should_be("fourth value"); - value_of(lastBoundValueUpdate).should_be("fourth value"); - }, - - 'Should be able to specify two-level bindings through a sub-object and through dot syntax': function() { - var results = {}, firstName = ko.observable('bob'), lastName = ko.observable('smith'); - ko.bindingHandlers.twoLevelBinding = { - flags: ko.bindingFlags.twoLevel, - update: function(element, valueAccessor) { - var value = valueAccessor(); - for (var prop in value) { - results[prop] = ko.utils.unwrapObservable(value[prop]); + after_each : function() { + ko.bindingProvider.instance = existingBindingProvider; + + }, + + 'applyBindings should accept no parameters and then act on document.body with undefined model': function () { + var didInit = false; + ko.bindingHandlers.test = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel) { + value_of(element.id).should_be("testElement"); + value_of(viewModel).should_be(undefined); + didInit = true; + } + }; + testNode.innerHTML = "
"; + ko.applyBindings(); + value_of(didInit).should_be(true); + + // Just to avoid interfering with other specs: + ko.utils.domData.clear(document.body); + }, + + 'applyBindings should accept one parameter and then act on document.body with parameter as model': function () { + var didInit = false; + var suppliedViewModel = {}; + ko.bindingHandlers.test = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel) { + value_of(element.id).should_be("testElement"); + value_of(viewModel).should_be(suppliedViewModel); + didInit = true; } + }; + testNode.innerHTML = "
"; + ko.applyBindings(suppliedViewModel); + value_of(didInit).should_be(true); + + // Just to avoid interfering with other specs: + ko.utils.domData.clear(document.body); + }, + + 'applyBindings should accept two parameters and then act on second param as DOM node with first param as model': function () { + var didInit = false; + var suppliedViewModel = {}; + ko.bindingHandlers.test = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel) { + value_of(element.id).should_be("testElement"); + value_of(viewModel).should_be(suppliedViewModel); + didInit = true; + } + }; + testNode.innerHTML = "
"; + var shouldNotMatchNode = document.createElement("DIV"); + shouldNotMatchNode.innerHTML = "
"; + document.body.appendChild(shouldNotMatchNode); + try { + ko.applyBindings(suppliedViewModel, testNode); + value_of(didInit).should_be(true); + } finally { + shouldNotMatchNode.parentNode.removeChild(shouldNotMatchNode); } - }; - testNode.innerHTML = "
"; - ko.applyBindings({ firstName: firstName, lastName: lastName }, testNode); - value_of(results.first).should_be("bob"); - value_of(results.last).should_be("smith"); - value_of(results.full).should_be("bob smith"); - - lastName('jones'); - value_of(results.last).should_be("jones"); - value_of(results.full).should_be("bob jones"); - }, - - 'Value of \'this\' in call to event handler should be the function\'s object if option set': function() { - ko.bindingHandlers.testEvent = { - flags: ko.bindingFlags.eventHandler, - init: function(element, valueAccessor) { - valueAccessor()(); // call the function + }, + + 'Should tolerate whitespace and nonexistent handlers': function () { + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); // No exception means success + }, + + 'Should tolerate arbitrary literals as the values for a handler': function () { + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); // No exception means success + }, + + 'Should tolerate wacky IE conditional comments': function () { + // Represents issue https://github.com/SteveSanderson/knockout/issues/186. Would fail on IE9, but work on earlier IE versions. + testNode.innerHTML = "
Hello
"; + ko.applyBindings(null, testNode); // No exception means success + }, + + 'Should invoke registered handlers\' init() then update() methods passing binding data': function () { + var methodsInvoked = []; + ko.bindingHandlers.test = { + init: function (element, valueAccessor, allBindingsAccessor) { + methodsInvoked.push("init"); + value_of(element.id).should_be("testElement"); + value_of(valueAccessor()).should_be("Hello"); + value_of(allBindingsAccessor().another).should_be(123); + }, + update: function (element, valueAccessor, allBindingsAccessor) { + methodsInvoked.push("update"); + value_of(element.id).should_be("testElement"); + value_of(valueAccessor()).should_be("Hello"); + value_of(allBindingsAccessor().another).should_be(123); + } } - }; - var eventCalls = 0, vm = { - topLevelFunction: function() { - value_of(this).should_be(vm); - eventCalls++; - }, - level2: { - secondLevelFunction: function() { - value_of(this).should_be(vm.level2); + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); + value_of(methodsInvoked.length).should_be(2); + value_of(methodsInvoked[0]).should_be("init"); + value_of(methodsInvoked[1]).should_be("update"); + }, + + 'If the binding handler depends on an observable, invokes the init handler once and the update handler whenever a new value is available': function () { + var observable = new ko.observable(); + var initPassedValues = [], updatePassedValues = []; + ko.bindingHandlers.test = { + init: function (element, valueAccessor) { initPassedValues.push(valueAccessor()()); }, + update: function (element, valueAccessor) { updatePassedValues.push(valueAccessor()()); } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode); + value_of(initPassedValues.length).should_be(1); + value_of(updatePassedValues.length).should_be(1); + value_of(initPassedValues[0]).should_be(undefined); + value_of(updatePassedValues[0]).should_be(undefined); + + observable("A"); + value_of(initPassedValues.length).should_be(1); + value_of(updatePassedValues.length).should_be(2); + value_of(updatePassedValues[1]).should_be("A"); + }, + + 'If the associated DOM element was removed by KO, handler subscriptions are disposed immediately': function () { + var observable = new ko.observable("A"); + ko.bindingHandlers.anyHandler = { + update: function (element, valueAccessor) { valueAccessor(); } + }; + testNode.innerHTML = "
"; + ko.applyBindings({ myObservable: observable }, testNode); + + value_of(observable.getSubscriptionsCount()).should_be(1); + + ko.cleanAndRemoveNode(testNode); + + value_of(observable.getSubscriptionsCount()).should_be(0); + }, + + 'If the associated DOM element was removed independently of KO, handler subscriptions are disposed on the next evaluation': function () { + var observable = new ko.observable("A"); + ko.bindingHandlers.anyHandler = { + update: function (element, valueAccessor) { valueAccessor(); } + }; + testNode.innerHTML = "
"; + ko.applyBindings({ myObservable: observable }, testNode); + + value_of(observable.getSubscriptionsCount()).should_be(1); + + testNode.parentNode.removeChild(testNode); + observable("B"); // Force re-evaluation + + value_of(observable.getSubscriptionsCount()).should_be(0); + }, + + 'If the binding attribute involves an observable, re-invokes the bindings if the observable notifies a change': function () { + var observable = new ko.observable({ message: "hello" }); + var passedValues = []; + ko.bindingHandlers.test = { update: function (element, valueAccessor) { passedValues.push(valueAccessor()); } }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode); + value_of(passedValues.length).should_be(1); + value_of(passedValues[0]).should_be("hello"); + + observable({ message: "goodbye" }); + value_of(passedValues.length).should_be(2); + value_of(passedValues[1]).should_be("goodbye"); + }, + + 'Should be able to refer to the bound object itself (at the root scope, the viewmodel) via $data': function () { + testNode.innerHTML = "
"; + ko.applyBindings({ someProp: 'My prop value' }, testNode); + value_of(testNode).should_contain_text("My prop value"); + }, + + 'Should be able to update bindings (including callbacks) using an observable view model': function () { + testNode.innerHTML = ""; + var input = testNode.childNodes[0], vm = ko.observable({ someProp: 'My prop value' }); + ko.applyBindings(vm, input); + + value_of(input.value).should_be("My prop value"); + + // a change to the input value should be written to the model + input.value = "some user-entered value"; + ko.utils.triggerEvent(input, "change"); + value_of(vm().someProp).should_be("some user-entered value"); + + // set the view-model to a new object + vm({ someProp: ko.observable('My new prop value') }); + value_of(input.value).should_be("My new prop value"); + + // a change to the input value should be written to the new model + input.value = "some new user-entered value"; + ko.utils.triggerEvent(input, "change"); + value_of(vm().someProp()).should_be("some new user-entered value"); + + // clear the element and the view-model (shouldn't be any errors) + testNode.innerHTML = ""; + vm(null); + }, + + 'Updates to an observable view model should update all child contexts (uncluding values copied from the parent)': function () { + ko.bindingHandlers.setChildContext = { + flags: ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + ko.applyBindingsToDescendants( + bindingContext.createChildContext(function () { return ko.utils.unwrapObservable(valueAccessor()) }), + element, true); + } + }; + + testNode.innerHTML = "
"; + var vm = ko.observable({ obj1: { prop1: "First " }, prop2: "view model" }); + ko.applyBindings(vm, testNode); + value_of(testNode).should_contain_text("First view model"); + + // change view model to new object + vm({ obj1: { prop1: "Second view " }, prop2: "model" }); + value_of(testNode).should_contain_text("Second view model"); + + // change it again + vm({ obj1: { prop1: "Third view model" }, prop2: "" }); + value_of(testNode).should_contain_text("Third view model"); + }, + + 'Should be able to get all updates to observables in both init and update': function () { + var lastBoundValueInit, lastBoundValueUpdate; + ko.bindingHandlers.testInit = { + init: function (element, valueAccessor) { + ko.dependentObservable(function () { + lastBoundValueInit = ko.utils.unwrapObservable(valueAccessor()); + }); + } + }; + ko.bindingHandlers.testUpdate = { + update: function (element, valueAccessor) { + lastBoundValueUpdate = ko.utils.unwrapObservable(valueAccessor()); + } + }; + testNode.innerHTML = "
"; + var vm = ko.observable({ myProp: ko.observable("initial value") }); + ko.applyBindings(vm, testNode); + value_of(lastBoundValueInit).should_be("initial value"); + value_of(lastBoundValueUpdate).should_be("initial value"); + + // update value of observable + vm().myProp("second value"); + value_of(lastBoundValueInit).should_be("second value"); + value_of(lastBoundValueUpdate).should_be("second value"); + + // update value of observable to another observable + vm().myProp(ko.observable("third value")); + value_of(lastBoundValueInit).should_be("third value"); + value_of(lastBoundValueUpdate).should_be("third value"); + + // update view model with brand-new property + vm({ myProp: function () { return "fourth value"; } }); + value_of(lastBoundValueInit).should_be("fourth value"); + value_of(lastBoundValueUpdate).should_be("fourth value"); + }, + + 'Should be able to specify two-level bindings through a sub-object and through dot syntax': function () { + var results = {}, firstName = ko.observable('bob'), lastName = ko.observable('smith'); + ko.bindingHandlers.twoLevelBinding = { + flags: ko.bindingFlags.twoLevel, + update: function (element, valueAccessor) { + var value = valueAccessor(); + for (var prop in value) { + results[prop] = ko.utils.unwrapObservable(value[prop]); + } + } + }; + testNode.innerHTML = "
"; + ko.applyBindings({ firstName: firstName, lastName: lastName }, testNode); + value_of(results.first).should_be("bob"); + value_of(results.last).should_be("smith"); + value_of(results.full).should_be("bob smith"); + + lastName('jones'); + value_of(results.last).should_be("jones"); + value_of(results.full).should_be("bob jones"); + }, + + 'Value of \'this\' in call to event handler should be the function\'s object if option set': function () { + ko.bindingHandlers.testEvent = { + flags: ko.bindingFlags.eventHandler, + init: function (element, valueAccessor) { + valueAccessor()(); // call the function + } + }; + var eventCalls = 0, vm = { + topLevelFunction: function () { + value_of(this).should_be(vm); eventCalls++; + }, + level2: { + secondLevelFunction: function () { + value_of(this).should_be(vm.level2); + eventCalls++; + } } + }; + testNode.innerHTML = "
"; + ko.applyBindings(vm, testNode, { eventHandlersUseObjectForThis: true }); + value_of(eventCalls).should_be(2); + }, + + 'Should be able to leave off the value if a binding specifies it doesn\'t require one (will default to true)': function () { + var initCalls = 0; + ko.bindingHandlers.doesntRequireValue = { + flags: ko.bindingFlags.noValue, + init: function (element, valueAccessor) { if (valueAccessor()) initCalls++; } } - }; - testNode.innerHTML = "
"; - ko.applyBindings(vm, testNode, {eventHandlersUseObjectForThis: true}); - value_of(eventCalls).should_be(2); - }, - - 'Should be able to leave off the value if a binding specifies it doesn\'t require one (will default to true)': function() { - var initCalls = 0; - ko.bindingHandlers.doesntRequireValue = { - flags: ko.bindingFlags.noValue, - init: function(element, valueAccessor) { if (valueAccessor()) initCalls++; } - } - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); - value_of(initCalls).should_be(2); - }, - - 'Should not be able to leave off the value if a binding doesn\'t specify the noValue flag': function() { - var initCalls = 0, didThrow = false; - ko.bindingHandlers.doesRequireValue = { - init: function(element, valueAccessor) { if (valueAccessor()) initCalls++; } - } - testNode.innerHTML = "
"; - - try { ko.applyBindings(null, testNode) } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('Unable to parse bindings') } - value_of(didThrow).should_be(true); - }, - - 'Bindings can signal that they control descendant bindings by setting contentBind flag': function() { - ko.bindingHandlers.test = { - flags: ko.bindingFlags.contentBind - }; - testNode.innerHTML = "
" - + "
456
" - + "
" - + "
456
"; - ko.applyBindings(null, testNode); - - value_of(testNode.childNodes[0].childNodes[0].innerHTML).should_be("456"); - value_of(testNode.childNodes[1].innerHTML).should_be("123"); - }, - - 'Should not be allowed to have multiple bindings on the same element that claim to control descendant bindings': function() { - ko.bindingHandlers.test1 = { - flags: ko.bindingFlags.contentBind - }; - ko.bindingHandlers.test2 = ko.bindingHandlers.test1; - testNode.innerHTML = "
" - var didThrow = false; - - try { ko.applyBindings(null, testNode) } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('Multiple bindings (test1 and test2) are trying to control descendant bindings of the same element.') } - value_of(didThrow).should_be(true); - }, - - 'Binding should not be allowed to use \'controlsDescendantBindings\' style with independent bindings': function() { - ko.bindingHandlers.test = { - init: function() { return { controlsDescendantBindings : true } } - }; - testNode.innerHTML = "
" - var didThrow = false; - - try { ko.applyBindings(null, testNode, {independentBindings: true}) } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('contentBind flag') } - value_of(didThrow).should_be(true); - }, - - 'Binding should be allowed to use \'controlsDescendantBindings\' with standard bindings': function() { - ko.bindingHandlers.test = { - init: function() { return { controlsDescendantBindings : true } } - }; - testNode.innerHTML = "
" - - ko.applyBindings(null, testNode); - // shouldn't throw any error - }, - - 'Binding can use both \'controlsDescendantBindings\' and \'contentBind\' with independent bindings': function() { - ko.bindingHandlers.test = { - flags: ko.bindingFlags.contentBind, - init: function() { return { controlsDescendantBindings : true } } - }; - testNode.innerHTML = "
" - - ko.applyBindings(null, testNode, {independentBindings: true}); - // shouldn't throw any error - }, - - 'Binding can use both \'controlsDescendantBindings\' and \'contentBind\' with standard bindings': function() { - ko.bindingHandlers.test = { - flags: ko.bindingFlags.contentBind, - init: function() { return { controlsDescendantBindings : true } } - }; - testNode.innerHTML = "
" - - ko.applyBindings(null, testNode); - // shouldn't throw any error - }, - - 'Should use properties on the view model in preference to properties on the binding context': function() { - testNode.innerHTML = "
"; - ko.applyBindings({ '$data': { someProp: 'Inner value'}, someProp: 'Outer value' }, testNode); - value_of(testNode).should_contain_text("Inner value"); - }, - - 'Should be able to extend a binding context, adding new custom properties, without mutating the original binding context': function() { - ko.bindingHandlers.addCustomProperty = { - flags: ko.bindingFlags.contentBind, - init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element); + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); + value_of(initCalls).should_be(2); + }, + + 'Should not be able to leave off the value if a binding doesn\'t specify the noValue flag': function () { + var initCalls = 0, didThrow = false; + ko.bindingHandlers.doesRequireValue = { + init: function (element, valueAccessor) { if (valueAccessor()) initCalls++; } } - }; - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); - value_of(testNode).should_contain_text("my value"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$customProp).should_be("my value"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$customProp).should_be(undefined); // Should not affect original binding context - }, - - 'Binding contexts should inherit any custom properties from ancestor binding contexts': function() { - ko.bindingHandlers.addCustomProperty = { - flags: ko.bindingFlags.contentBind, - init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element); + testNode.innerHTML = "
"; + + try { ko.applyBindings(null, testNode) } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('Unable to parse bindings') } + value_of(didThrow).should_be(true); + }, + + 'Bindings can signal that they control descendant bindings by setting contentBind flag': function () { + ko.bindingHandlers.test = { + flags: ko.bindingFlags.contentBind + }; + testNode.innerHTML = "
" + + "
456
" + + "
" + + "
456
"; + ko.applyBindings(null, testNode); + + value_of(testNode.childNodes[0].childNodes[0].innerHTML).should_be("456"); + value_of(testNode.childNodes[1].innerHTML).should_be("123"); + }, + + 'Should not be allowed to have multiple bindings on the same element that claim to control descendant bindings': function () { + ko.bindingHandlers.test1 = { + flags: ko.bindingFlags.contentBind + }; + ko.bindingHandlers.test2 = ko.bindingHandlers.test1; + testNode.innerHTML = "
" + var didThrow = false; + + try { ko.applyBindings(null, testNode) } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('Multiple bindings (test1 and test2) are trying to control descendant bindings of the same element.') } + value_of(didThrow).should_be(true); + }, + + 'Binding should not be allowed to use \'controlsDescendantBindings\' style with independent bindings': function () { + ko.bindingHandlers.test = { + init: function () { return { controlsDescendantBindings: true} } + }; + testNode.innerHTML = "
" + var didThrow = false; + + try { ko.applyBindings(null, testNode, { independentBindings: true }) } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('contentBind flag') } + value_of(didThrow).should_be(true); + }, + + 'Binding should be allowed to use \'controlsDescendantBindings\' with standard bindings': function () { + ko.bindingHandlers.test = { + init: function () { return { controlsDescendantBindings: true} } + }; + testNode.innerHTML = "
" + + ko.applyBindings(null, testNode); + // shouldn't throw any error + }, + + 'Binding can use both \'controlsDescendantBindings\' and \'contentBind\' with independent bindings': function () { + ko.bindingHandlers.test = { + flags: ko.bindingFlags.contentBind, + init: function () { return { controlsDescendantBindings: true} } + }; + testNode.innerHTML = "
" + + ko.applyBindings(null, testNode, { independentBindings: true }); + // shouldn't throw any error + }, + + 'Binding can use both \'controlsDescendantBindings\' and \'contentBind\' with standard bindings': function () { + ko.bindingHandlers.test = { + flags: ko.bindingFlags.contentBind, + init: function () { return { controlsDescendantBindings: true} } + }; + testNode.innerHTML = "
" + + ko.applyBindings(null, testNode); + // shouldn't throw any error + }, + + 'Should use properties on the view model in preference to properties on the binding context': function () { + testNode.innerHTML = "
"; + ko.applyBindings({ '$data': { someProp: 'Inner value' }, someProp: 'Outer value' }, testNode); + value_of(testNode).should_contain_text("Inner value"); + }, + + 'Should be able to extend a binding context, adding new custom properties, without mutating the original binding context': function () { + ko.bindingHandlers.addCustomProperty = { + flags: ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element); + } + }; + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); + value_of(testNode).should_contain_text("my value"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$customProp).should_be("my value"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$customProp).should_be(undefined); // Should not affect original binding context + }, + + 'Binding contexts should inherit any custom properties from ancestor binding contexts': function () { + ko.bindingHandlers.addCustomProperty = { + flags: ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element); + } + }; + testNode.innerHTML = "
"; + ko.applyBindings(null, testNode); + value_of(testNode).should_contain_text("my value"); + }, + + 'Should be able to retrieve the binding context associated with any node': function () { + testNode.innerHTML = "
"; + ko.applyBindings({ name: 'Bert' }, testNode.childNodes[0]); + + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Bert"); + + // Can't get binding context for unbound nodes + value_of(ko.dataFor(testNode)).should_be(undefined); + value_of(ko.contextFor(testNode)).should_be(undefined); + + // Can get binding context for directly bound nodes + value_of(ko.dataFor(testNode.childNodes[0]).name).should_be("Bert"); + value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("Bert"); + + // Can get binding context for descendants of directly bound nodes + value_of(ko.dataFor(testNode.childNodes[0].childNodes[0]).name).should_be("Bert"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("Bert"); + }, + + 'Should not be allowed to use containerless binding syntax for bindings other than whitelisted ones': function () { + testNode.innerHTML = "Hello Some text Goodbye" + var didThrow = false; + try { + ko.applyBindings(null, testNode); + } catch (ex) { + didThrow = true; + value_of(ex.message).should_be("The binding 'visible' cannot be used with virtual elements"); } - }; - testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); - value_of(testNode).should_contain_text("my value"); - }, - - 'Should be able to retrieve the binding context associated with any node': function() { - testNode.innerHTML = "
"; - ko.applyBindings({ name: 'Bert' }, testNode.childNodes[0]); + value_of(didThrow).should_be(true); + }, + + 'Should be able to set a custom binding to use containerless binding using \'canUseVirtual\' flag': function () { + var initCalls = 0; + ko.bindingHandlers.test = { + flags: ko.bindingFlags.canUseVirtual, + init: function () { initCalls++; } + }; + testNode.innerHTML = "Hello Some text Goodbye" + ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Bert"); + value_of(initCalls).should_be(1); + value_of(testNode).should_contain_text("Hello Some text Goodbye"); + }, - // Can't get binding context for unbound nodes - value_of(ko.dataFor(testNode)).should_be(undefined); - value_of(ko.contextFor(testNode)).should_be(undefined); + 'Should be able to set a custom binding to use containerless binding using \'allowedBindings\'': function () { + var initCalls = 0; + ko.bindingHandlers.test = { init: function () { initCalls++ } }; + ko.virtualElements.allowedBindings['test'] = true; - // Can get binding context for directly bound nodes - value_of(ko.dataFor(testNode.childNodes[0]).name).should_be("Bert"); - value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("Bert"); + testNode.innerHTML = "Hello Some text Goodbye" + ko.applyBindings(null, testNode); - // Can get binding context for descendants of directly bound nodes - value_of(ko.dataFor(testNode.childNodes[0].childNodes[0]).name).should_be("Bert"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("Bert"); - }, - - 'Should not be allowed to use containerless binding syntax for bindings other than whitelisted ones': function() { - testNode.innerHTML = "Hello Some text Goodbye" - var didThrow = false; - try { + value_of(initCalls).should_be(1); + value_of(testNode).should_contain_text("Hello Some text Goodbye"); + }, + + 'Should be able to access virtual children in custom containerless binding': function () { + var countNodes = 0; + ko.bindingHandlers.test = { + flags: ko.bindingFlags.canUseVirtual | ko.bindingFlags.contentUpdate, + init: function (element, valueAccessor) { + // Counts the number of virtual children, and overwrites the text contents of any text nodes + for (var node = ko.virtualElements.firstChild(element); node; node = ko.virtualElements.nextSibling(node)) { + countNodes++; + if (node.nodeType === 3) + node.data = 'new text'; + } + } + }; + testNode.innerHTML = "Hello Some text Goodbye" ko.applyBindings(null, testNode); - } catch(ex) { - didThrow = true; - value_of(ex.message).should_be("The binding 'visible' cannot be used with virtual elements"); - } - value_of(didThrow).should_be(true); - }, - - 'Should be able to set a custom binding to use containerless binding using \'canUseVirtual\' flag': function() { - var initCalls = 0; - ko.bindingHandlers.test = { - flags: ko.bindingFlags.canUseVirtual, - init: function () { initCalls++; } - }; - testNode.innerHTML = "Hello Some text Goodbye" - ko.applyBindings(null, testNode); - - value_of(initCalls).should_be(1); - value_of(testNode).should_contain_text("Hello Some text Goodbye"); - }, - - 'Should be able to set a custom binding to use containerless binding using \'allowedBindings\'': function() { - var initCalls = 0; - ko.bindingHandlers.test = { init: function () { initCalls++ } }; - ko.virtualElements.allowedBindings['test'] = true; - testNode.innerHTML = "Hello Some text Goodbye" - ko.applyBindings(null, testNode); + value_of(countNodes).should_be(1); + value_of(testNode).should_contain_text("Hello new text Goodbye"); + }, + + 'Should only bind containerless binding once inside template': function () { + var initCalls = 0; + ko.bindingHandlers.test = { + flags: ko.bindingFlags.canUseVirtual, + init: function () { initCalls++; } + }; + testNode.innerHTML = "Hello Some text Goodbye" + ko.applyBindings(null, testNode); - value_of(initCalls).should_be(1); - value_of(testNode).should_contain_text("Hello Some text Goodbye"); - }, - - 'Should be able to access virtual children in custom containerless binding': function() { - var countNodes = 0; - ko.bindingHandlers.test = { - flags: ko.bindingFlags.canUseVirtual | ko.bindingFlags.contentUpdate, - init: function (element, valueAccessor) { - // Counts the number of virtual children, and overwrites the text contents of any text nodes - for (var node = ko.virtualElements.firstChild(element); node; node = ko.virtualElements.nextSibling(node)) { - countNodes++; - if (node.nodeType === 3) - node.data = 'new text'; + value_of(initCalls).should_be(1); + value_of(testNode).should_contain_text("Hello Some text Goodbye"); + }, + + 'Bindings in containerless binding in templates should be bound only once': function () { + delete ko.bindingHandlers.nonexistentHandler; + var initCalls = 0; + ko.bindingHandlers.test = { init: function () { initCalls++; } }; + testNode.innerHTML = "
xxx
"; + ko.applyBindings({}, testNode); + value_of(initCalls).should_be(1); + }, + + 'Should automatically bind virtual descendants of containerless markers if no binding controlsDescendantBindings': function () { + testNode.innerHTML = "Hello Some text Goodbye"; + ko.applyBindings(null, testNode); + value_of(testNode).should_contain_text("Hello WasBound Goodbye"); + }, + + 'Should be able to set and access correct context in custom containerless binding': function () { + ko.bindingHandlers.bindChildrenWithCustomContext = { + flags: ko.bindingFlags.canUseVirtual | ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); + ko.applyBindingsToDescendants(innerContext, element, true); } - } - }; - testNode.innerHTML = "Hello Some text Goodbye" - ko.applyBindings(null, testNode); + }; - value_of(countNodes).should_be(1); - value_of(testNode).should_contain_text("Hello new text Goodbye"); - }, - - 'Should only bind containerless binding once inside template': function() { - var initCalls = 0; - ko.bindingHandlers.test = { - flags: ko.bindingFlags.canUseVirtual, - init: function () { initCalls++; } - }; - testNode.innerHTML = "Hello Some text Goodbye" - ko.applyBindings(null, testNode); - - value_of(initCalls).should_be(1); - value_of(testNode).should_contain_text("Hello Some text Goodbye"); - }, - - 'Bindings in containerless binding in templates should be bound only once': function() { - delete ko.bindingHandlers.nonexistentHandler; - var initCalls = 0; - ko.bindingHandlers.test = { init: function () { initCalls++; } }; - testNode.innerHTML = "
xxx
"; - ko.applyBindings({}, testNode); - value_of(initCalls).should_be(1); - }, - - 'Should automatically bind virtual descendants of containerless markers if no binding controlsDescendantBindings': function() { - testNode.innerHTML = "Hello Some text Goodbye"; - ko.applyBindings(null, testNode); - value_of(testNode).should_contain_text("Hello WasBound Goodbye"); - }, - - 'Should be able to set and access correct context in custom containerless binding': function() { - ko.bindingHandlers.bindChildrenWithCustomContext = { - flags: ko.bindingFlags.canUseVirtual | ko.bindingFlags.contentBind, - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); - ko.applyBindingsToDescendants(innerContext, element, true); - } - }; + testNode.innerHTML = "Hello
Some text
Goodbye" + ko.applyBindings(null, testNode); - testNode.innerHTML = "Hello
Some text
Goodbye" - ko.applyBindings(null, testNode); + value_of(ko.dataFor(testNode.childNodes[2]).myCustomData).should_be(123); + }, - value_of(ko.dataFor(testNode.childNodes[2]).myCustomData).should_be(123); - }, - - 'Should be able to set and access correct context in nested containerless binding': function() { - delete ko.bindingHandlers.nonexistentHandler; - ko.bindingHandlers.bindChildrenWithCustomContext = { - flags: ko.bindingFlags.contentBind, - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); - ko.applyBindingsToDescendants(innerContext, element, true); - } - }; + 'Should be able to set and access correct context in nested containerless binding': function () { + delete ko.bindingHandlers.nonexistentHandler; + ko.bindingHandlers.bindChildrenWithCustomContext = { + flags: ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); + ko.applyBindingsToDescendants(innerContext, element, true); + } + }; - testNode.innerHTML = "Hello
Some text
Goodbye" - ko.applyBindings(null, testNode); + testNode.innerHTML = "Hello
Some text
Goodbye" + ko.applyBindings(null, testNode); - value_of(ko.dataFor(testNode.childNodes[1].childNodes[0]).myCustomData).should_be(123); - value_of(ko.dataFor(testNode.childNodes[1].childNodes[1]).myCustomData).should_be(123); - }, - - 'Should be able to access custom context variables in child context': function() { - ko.bindingHandlers.bindChildrenWithCustomContext = { - flags: ko.bindingFlags.contentBind, - init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { - var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); - innerContext.customValue = 'xyz'; - ko.applyBindingsToDescendants(innerContext, element, true); - } - }; + value_of(ko.dataFor(testNode.childNodes[1].childNodes[0]).myCustomData).should_be(123); + value_of(ko.dataFor(testNode.childNodes[1].childNodes[1]).myCustomData).should_be(123); + }, + + 'Should be able to access custom context variables in child context': function () { + ko.bindingHandlers.bindChildrenWithCustomContext = { + flags: ko.bindingFlags.contentBind, + init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + var innerContext = bindingContext.createChildContext({ myCustomData: 123 }); + innerContext.customValue = 'xyz'; + ko.applyBindingsToDescendants(innerContext, element, true); + } + }; - testNode.innerHTML = "Hello
Some text
Goodbye" - ko.applyBindings(null, testNode); + testNode.innerHTML = "Hello
Some text
Goodbye" + ko.applyBindings(null, testNode); - value_of(ko.contextFor(testNode.childNodes[1].childNodes[0]).customValue).should_be('xyz'); - value_of(ko.dataFor(testNode.childNodes[1].childNodes[1])).should_be(123); - value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parent.myCustomData).should_be(123); - value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parentContext.customValue).should_be('xyz'); - }, - - 'Should not reinvoke init for notifications triggered during first evaluation': function () { - var observable = ko.observable('A'); - var initCalls = 0; - ko.bindingHandlers.test = { - init: function (element, valueAccessor) { - initCalls++; - - var value = valueAccessor(); - - // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings) - // This logic probably wouldn't be in init but might be indirectly invoked by init - value(); - value('B'); - } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode); - value_of(initCalls).should_be(1); - }, - - 'Should not run update before init, even if an associated observable is updated by a different binding before init': function() { - // Represents the "theoretical issue" posed by Ryan in comments on https://github.com/SteveSanderson/knockout/pull/193 - - var observable = ko.observable('A'), hasInittedSecondBinding = false, hasUpdatedSecondBinding = false; - ko.bindingHandlers.test1 = { - init: function(element, valueAccessor) { - // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings) - // This logic probably wouldn't be in init but might be indirectly invoked by init - var value = valueAccessor(); - value(); - value('B'); - } - } - ko.bindingHandlers.test2 = { - init: function() { - hasInittedSecondBinding = true; - }, - update: function() { - if (!hasInittedSecondBinding) - throw new Error("Called 'update' before 'init'"); - hasUpdatedSecondBinding = true; - } - } - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode); - value_of(hasUpdatedSecondBinding).should_be(true); - }, - - 'Should not subscribe to observables accessed in init function if binding are run independently': function() { - var observable = ko.observable('A'); - ko.bindingHandlers.test = { - init: function(element, valueAccessor) { - var value = valueAccessor(); - value(); - } - } - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - value_of(observable.getSubscriptionsCount()).should_be(0); - }, - - 'Should not run updates for all bindings if only one needs to run if binding are run independently': function() { - var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; - ko.bindingHandlers.test1 = { - update: function(element, valueAccessor) { - valueAccessor()(); // access value to create a subscription - updateCount1++; - } - }; - ko.bindingHandlers.test2 = { - update: function() { - updateCount2++; + value_of(ko.contextFor(testNode.childNodes[1].childNodes[0]).customValue).should_be('xyz'); + value_of(ko.dataFor(testNode.childNodes[1].childNodes[1])).should_be(123); + value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parent.myCustomData).should_be(123); + value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parentContext.customValue).should_be('xyz'); + }, + + 'Should not reinvoke init for notifications triggered during first evaluation': function () { + var observable = ko.observable('A'); + var initCalls = 0; + ko.bindingHandlers.test = { + init: function (element, valueAccessor) { + initCalls++; + + var value = valueAccessor(); + + // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings) + // This logic probably wouldn't be in init but might be indirectly invoked by init + value(); + value('B'); + } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode); + value_of(initCalls).should_be(1); + }, + + 'Should not run update before init, even if an associated observable is updated by a different binding before init': function () { + // Represents the "theoretical issue" posed by Ryan in comments on https://github.com/SteveSanderson/knockout/pull/193 + + var observable = ko.observable('A'), hasInittedSecondBinding = false, hasUpdatedSecondBinding = false; + ko.bindingHandlers.test1 = { + init: function (element, valueAccessor) { + // Read the observable (to set up a dependency on it), and then also write to it (to trigger re-eval of bindings) + // This logic probably wouldn't be in init but might be indirectly invoked by init + var value = valueAccessor(); + value(); + value('B'); + } } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - value_of(updateCount1).should_be(1); - value_of(updateCount2).should_be(1); - - // update the observable and check that only the first binding was updated - observable('B'); - value_of(updateCount1).should_be(2); - value_of(updateCount2).should_be(1); - }, - - 'Update to a dependency should also update the dependent binding (independent mode)': function() { - var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; - ko.bindingHandlers.test1 = { - update: function(element, valueAccessor) { - valueAccessor()(); // access value to create a subscription - updateCount1++; + ko.bindingHandlers.test2 = { + init: function () { + hasInittedSecondBinding = true; + }, + update: function () { + if (!hasInittedSecondBinding) + throw new Error("Called 'update' before 'init'"); + hasUpdatedSecondBinding = true; + } } - }; - ko.bindingHandlers.test2 = { - dependencies: 'test1', - update: function() { - updateCount2++; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode); + value_of(hasUpdatedSecondBinding).should_be(true); + }, + + 'Should not subscribe to observables accessed in init function if binding are run independently': function () { + var observable = ko.observable('A'); + ko.bindingHandlers.test = { + init: function (element, valueAccessor) { + var value = valueAccessor(); + value(); + } } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - value_of(updateCount1).should_be(1); - value_of(updateCount2).should_be(1); - - // update the observable and check that both bindings were updated - observable('B'); - value_of(updateCount1).should_be(2); - value_of(updateCount2).should_be(2); - }, - - 'Binding should be able to return a subscribable value so dependent bindings can be updated (independent mode)': function() { - var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; - ko.bindingHandlers.test1 = { - update: function(element, valueAccessor) { - updateCount1++; - return ko.dependentObservable(function() { + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + value_of(observable.getSubscriptionsCount()).should_be(0); + }, + + 'Should not run updates for all bindings if only one needs to run if binding are run independently': function () { + var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; + ko.bindingHandlers.test1 = { + update: function (element, valueAccessor) { valueAccessor()(); // access value to create a subscription - }, null, {disposeWhenNodeIsRemoved: element}); - } - }; - ko.bindingHandlers.test2 = { - dependencies: 'test1', - update: function() { - updateCount2++; - } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - observable('B'); - value_of(updateCount1).should_be(1); // update happened inside inner dependentObservable so count isn't updated - value_of(updateCount2).should_be(2); - }, - - 'Binding should be able to return a subscribable value from \'init\' so dependent bindings can be updated (independent mode)': function() { - var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; - ko.bindingHandlers.test1 = { - init: function(element, valueAccessor) { - return { subscribable: ko.dependentObservable(function() { updateCount1++; + } + }; + ko.bindingHandlers.test2 = { + update: function () { + updateCount2++; + } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + value_of(updateCount1).should_be(1); + value_of(updateCount2).should_be(1); + + // update the observable and check that only the first binding was updated + observable('B'); + value_of(updateCount1).should_be(2); + value_of(updateCount2).should_be(1); + }, + + 'Update to a dependency should also update the dependent binding (independent mode)': function () { + var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; + ko.bindingHandlers.test1 = { + update: function (element, valueAccessor) { valueAccessor()(); // access value to create a subscription - }, null, {disposeWhenNodeIsRemoved: element}) }; - } - }; - ko.bindingHandlers.test2 = { - dependencies: 'test1', - update: function() { - updateCount2++; + updateCount1++; + } + }; + ko.bindingHandlers.test2 = { + dependencies: 'test1', + update: function () { + updateCount2++; + } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + value_of(updateCount1).should_be(1); + value_of(updateCount2).should_be(1); + + // update the observable and check that both bindings were updated + observable('B'); + value_of(updateCount1).should_be(2); + value_of(updateCount2).should_be(2); + }, + + 'Binding should be able to return a subscribable value so dependent bindings can be updated (independent mode)': function () { + var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; + ko.bindingHandlers.test1 = { + update: function (element, valueAccessor) { + updateCount1++; + return ko.dependentObservable(function () { + valueAccessor()(); // access value to create a subscription + }, null, { disposeWhenNodeIsRemoved: element }); + } + }; + ko.bindingHandlers.test2 = { + dependencies: 'test1', + update: function () { + updateCount2++; + } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + observable('B'); + value_of(updateCount1).should_be(1); // update happened inside inner dependentObservable so count isn't updated + value_of(updateCount2).should_be(2); + }, + + 'Binding should be able to return a subscribable value from \'init\' so dependent bindings can be updated (independent mode)': function () { + var observable = ko.observable('A'), updateCount1 = 0, updateCount2 = 0; + ko.bindingHandlers.test1 = { + init: function (element, valueAccessor) { + return { subscribable: ko.dependentObservable(function () { + updateCount1++; + valueAccessor()(); // access value to create a subscription + }, null, { disposeWhenNodeIsRemoved: element }) + }; + } + }; + ko.bindingHandlers.test2 = { + dependencies: 'test1', + update: function () { + updateCount2++; + } + }; + testNode.innerHTML = "
"; + + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + observable('B'); + value_of(updateCount1).should_be(2); + value_of(updateCount2).should_be(2); + }, + + 'Should update all bindings if a extra binding unwraps an observable (only in dependent mode)': function () { + delete ko.bindingHandlers.nonexistentHandler; + var countUpdates = 0, observable = ko.observable(1); + ko.bindingHandlers.existentHandler = { + update: function () { countUpdates++; } } - }; - testNode.innerHTML = "
"; - - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - observable('B'); - value_of(updateCount1).should_be(2); - value_of(updateCount2).should_be(2); - }, - - 'Should update all bindings if a extra binding unwraps an observable (only in dependent mode)': function() { - delete ko.bindingHandlers.nonexistentHandler; - var countUpdates = 0, observable = ko.observable(1); - ko.bindingHandlers.existentHandler = { - update: function() { countUpdates++; } - } - testNode.innerHTML = "
"; - - // dependent mode: should update - ko.applyBindings({ myObservable: observable }, testNode); - value_of(countUpdates).should_be(1); - observable(3); - value_of(countUpdates).should_be(2); - - // reset - countUpdates = 0; - ko.cleanNode(testNode); - ko.bindingProvider.instance.clearCache(); - - // independent mode: should not update - ko.applyBindings({ myObservable: observable }, testNode, {independentBindings: true}); - value_of(countUpdates).should_be(1); - observable(2); - value_of(countUpdates).should_be(1); - }, - - // TODO - This is a spec that succeeds in base Knockout, but fails with this update - /*'Should access latest value from extra binding when normal binding is updated': function() { + testNode.innerHTML = "
"; + + // dependent mode: should update + ko.applyBindings({ myObservable: observable }, testNode); + value_of(countUpdates).should_be(1); + observable(3); + value_of(countUpdates).should_be(2); + + // reset + countUpdates = 0; + ko.cleanNode(testNode); + ko.bindingProvider.instance.clearCache(); + + // independent mode: should not update + ko.applyBindings({ myObservable: observable }, testNode, { independentBindings: true }); + value_of(countUpdates).should_be(1); + observable(2); + value_of(countUpdates).should_be(1); + }, + + // TODO - This is a spec that succeeds in base Knockout, but fails with this update + /*'Should access latest value from extra binding when normal binding is updated': function() { delete ko.bindingHandlers.nonexistentHandler; var observable = ko.observable(), updateValue; var vm = {myObservable: observable, myNonObservable: "first value"}; ko.bindingHandlers.existentHandler = { - update: function(element, valueAccessor, allBindingsAccessor) { - valueAccessor()(); // create dependency - updateValue = allBindingsAccessor().nonexistentHandler; - } + update: function(element, valueAccessor, allBindingsAccessor) { + valueAccessor()(); // create dependency + updateValue = allBindingsAccessor().nonexistentHandler; + } } - testNode.innerHTML = "
"; + testNode.innerHTML = "
"; ko.applyBindings(vm, testNode); value_of(updateValue).should_be("first value"); vm.myNonObservable = "second value"; observable.notifySubscribers(); value_of(updateValue).should_be("second value"); - },*/ - - 'Should process bindings in a certain order based on their type and dependencies': function() { - var lastBindingIndex = 0; - function checkOrder(bindingIndex) { - if (bindingIndex < lastBindingIndex) - throw new Error("handler " + bindingIndex + " called after " + lastBindingIndex); - lastBindingIndex = bindingIndex; - } - ko.bindingHandlers.test1 = { flags: 0, update: function() { checkOrder(1); } }; - ko.bindingHandlers.test2 = { flags: ko.bindingFlags.contentSet, update: function() { checkOrder(2); } }; - ko.bindingHandlers.test3 = { flags: ko.bindingFlags.contentBind, update: function() { checkOrder(3); } }; - ko.bindingHandlers.test4 = { flags: ko.bindingFlags.contentUpdate, update: function() { checkOrder(4); } }; - ko.bindingHandlers.test5 = { update: function() { checkOrder(5); } }; + },*/ + + 'Should process bindings in a certain order based on their type and dependencies': function () { + var lastBindingIndex = 0; + function checkOrder(bindingIndex) { + if (bindingIndex < lastBindingIndex) + throw new Error("handler " + bindingIndex + " called after " + lastBindingIndex); + lastBindingIndex = bindingIndex; + } + ko.bindingHandlers.test1 = { flags: 0, update: function () { checkOrder(1); } }; + ko.bindingHandlers.test2 = { flags: ko.bindingFlags.contentSet, update: function () { checkOrder(2); } }; + ko.bindingHandlers.test3 = { flags: ko.bindingFlags.contentBind, update: function () { checkOrder(3); } }; + ko.bindingHandlers.test4 = { flags: ko.bindingFlags.contentUpdate, update: function () { checkOrder(4); } }; + ko.bindingHandlers.test5 = { update: function () { checkOrder(5); } }; - testNode.innerHTML = "
"; + testNode.innerHTML = "
"; - ko.applyBindings(null, testNode); - }, - - 'Should not be able to set recursive dependencies': function() { - ko.bindingHandlers.test1 = { }; - ko.bindingHandlers.test2 = { }; - - testNode.innerHTML = "
"; - - var didThrow = false; - try { ko.applyBindings(null, testNode) } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('recursive') } - value_of(didThrow).should_be(true); - }, - - 'Should not be able to set dependencies that conflict with the order set by flags': function() { - ko.bindingHandlers.test1 = { flags: ko.bindingFlags.contentSet }; - ko.bindingHandlers.test2 = { flags: ko.bindingFlags.contentUpdate }; - - testNode.innerHTML = "
"; - - var didThrow = false; - try { ko.applyBindings(null, testNode) } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('ordering') } - value_of(didThrow).should_be(true); - }, - - 'Changing type of binding handler won\'t clear binding cache, but cache can be cleared by calling clearCache': function() { - var vm = ko.observable(1), updateCalls = 0, didThrow = false; - ko.bindingHandlers.sometimesRequiresValue = { - flags: ko.bindingFlags.noValue, - update: function() { updateCalls++; } + ko.applyBindings(null, testNode); + }, + + 'Should not be able to set recursive dependencies': function () { + ko.bindingHandlers.test1 = {}; + ko.bindingHandlers.test2 = {}; + + testNode.innerHTML = "
"; + + var didThrow = false; + try { ko.applyBindings(null, testNode) } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('recursive') } + value_of(didThrow).should_be(true); + }, + + 'Should not be able to set dependencies that conflict with the order set by flags': function () { + ko.bindingHandlers.test1 = { flags: ko.bindingFlags.contentSet }; + ko.bindingHandlers.test2 = { flags: ko.bindingFlags.contentUpdate }; + + testNode.innerHTML = "
"; + + var didThrow = false; + try { ko.applyBindings(null, testNode) } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('ordering') } + value_of(didThrow).should_be(true); + }, + + 'Changing type of binding handler won\'t clear binding cache, but cache can be cleared by calling clearCache': function () { + var vm = ko.observable(1), updateCalls = 0, didThrow = false; + ko.bindingHandlers.sometimesRequiresValue = { + flags: ko.bindingFlags.noValue, + update: function () { updateCalls++; } + } + testNode.innerHTML = "
"; + // first time works fine + ko.applyBindings(vm, testNode); + value_of(updateCalls).should_be(1); + + // change type of handler; it will still work because of cache + delete ko.bindingHandlers.sometimesRequiresValue.flags; + vm(2); // forces reparsing of binding values (but cache will kick in) + value_of(updateCalls).should_be(2); + + // now clear the cache; reparsing will fail + ko.bindingProvider.instance.clearCache(); + try { vm(3); } + catch (ex) { didThrow = true; value_of(ex.message).should_contain('Unable to parse bindings') } + value_of(didThrow).should_be(true); + value_of(updateCalls).should_be(2); } - testNode.innerHTML = "
"; - // first time works fine - ko.applyBindings(vm, testNode); - value_of(updateCalls).should_be(1); - - // change type of handler; it will still work because of cache - delete ko.bindingHandlers.sometimesRequiresValue.flags; - vm(2); // forces reparsing of binding values (but cache will kick in) - value_of(updateCalls).should_be(2); - - // now clear the cache; reparsing will fail - ko.bindingProvider.instance.clearCache(); - try { vm(3); } - catch(ex) { didThrow = true; value_of(ex.message).should_contain('Unable to parse bindings') } - value_of(didThrow).should_be(true); - value_of(updateCalls).should_be(2); - } -}); \ No newline at end of file + }); + +}; + +bindingAttributeBehaviors({ name: "alternativeConfiguration", virtualElementTag: "ko1", bindingAttribute: "data-bind1" }); +bindingAttributeBehaviors(); + diff --git a/src/binding/bindingProvider.js b/src/binding/bindingProvider.js index 401ac599f..4e117033a 100644 --- a/src/binding/bindingProvider.js +++ b/src/binding/bindingProvider.js @@ -1,32 +1,32 @@ -(function() { - var defaultBindingAttributeName = "data-bind"; +(function () { - ko.bindingProvider = function() { + ko.bindingProvider = function (configuration) { + this.configuration = setDefaultConfiguration(configuration); this.bindingCache = {}; - this['clearCache'] = function() { + this['clearCache'] = function () { this.bindingCache = {}; }; }; ko.utils.extendInternal(ko.bindingProvider.prototype, { - 'nodeHasBindings': function(node) { + 'nodeHasBindings': function (node) { switch (node.nodeType) { - case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element + case 1: return node.getAttribute(this.configuration.bindingAttribute) != null; // Element case 8: return ko.virtualElements.virtualNodeBindingValue(node) != null; // Comment node default: return false; } }, - 'getBindings': function(node, bindingContext) { + 'getBindings': function (node, bindingContext) { var bindingsString = this['getBindingsString'](node, bindingContext); return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext) : null; }, // The following function is only used internally by this default provider. // It's not part of the interface definition for a general binding provider. - 'getBindingsString': function(node, bindingContext) { + 'getBindingsString': function (node, bindingContext) { switch (node.nodeType) { - case 1: return node.getAttribute(defaultBindingAttributeName); // Element + case 1: return node.getAttribute(this.configuration.bindingAttribute); // Element case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node default: return null; } @@ -34,7 +34,7 @@ // The following function is only used internally by this default provider. // It's not part of the interface definition for a general binding provider. - 'parseBindingsString': function(bindingsString, bindingContext) { + 'parseBindingsString': function (bindingsString, bindingContext) { try { var viewModel = bindingContext['$data'], scopes = (typeof viewModel == 'object' && viewModel != null) ? [viewModel, bindingContext] : [bindingContext], @@ -42,22 +42,36 @@ return bindingFunction(scopes); } catch (ex) { throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString); - } + } } }); ko.bindingProvider['instance'] = new ko.bindingProvider(); + ko.bindingProvider.configuration = function (bindingProvider) { + bindingProvider = bindingProvider ? bindingProvider : ko.bindingProvider["instance"]; + var configuration = bindingProvider.configuration ? bindingProvider.configuration : {}; + return setDefaultConfiguration(configuration); + }; + + function setDefaultConfiguration(configuration) { + if (!configuration) configuration = {}; + configuration.name = configuration.name ? configuration.name : 'default'; + configuration.bindingAttribute = configuration.bindingAttribute ? configuration.bindingAttribute : 'data-bind'; + configuration.virtualElementTag = configuration.virtualElementTag ? configuration.virtualElementTag : "ko"; + return configuration; + } + function createBindingsStringEvaluatorViaCache(bindingsString, bindingOptions, scopesCount, cache) { var cacheKey = scopesCount + '_' + bindingsString; - return cache[cacheKey] + return cache[cacheKey] || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, bindingOptions, scopesCount)); } function createBindingsStringEvaluator(bindingsString, bindingOptions, scopesCount) { var rewrittenBindings = " { " + ko.bindingExpressionRewriting.insertPropertyAccessors(bindingsString, bindingOptions) + " } "; return ko.utils.buildEvalWithinScopeFunction(rewrittenBindings, scopesCount); - } + } })(); ko.exportSymbol('bindingProvider', ko.bindingProvider); diff --git a/src/templating/templateRewriting.js b/src/templating/templateRewriting.js index f931a40dc..37e71f7c9 100644 --- a/src/templating/templateRewriting.js +++ b/src/templating/templateRewriting.js @@ -1,7 +1,11 @@ ko.templateRewriting = (function () { - var memoizeDataBindingAttributeSyntaxRegex = /(<[a-z]+\d*(\s+(?!data-bind=)[a-z0-9\-]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])([\s\S]*?)\5/gi; - var memoizeVirtualContainerBindingSyntaxRegex = //g; + var memoizeDataBindingAttributeSyntaxRegex = function (bindingProvider) { + return new RegExp("(<[a-z]+\\d*(\\s+(?!" + ko.bindingProvider.configuration(bindingProvider).bindingAttribute + "=)[a-z0-9\\-]+(=(\\\"[^\\\"]*\\\"|\\'[^\\']*\\'))?)*\\s+)" + ko.bindingProvider.configuration(bindingProvider).bindingAttribute + "=([\"'])([\\s\\S]*?)\\5", "gi"); + }; + var memoizeVirtualContainerBindingSyntaxRegex = function (bindingProvider) { + return new RegExp("", "g"); + } function validateDataBindValuesForRewriting(keyValueArray) { var allValidators = ko.templateRewriting.bindingRewriteValidators; @@ -44,10 +48,10 @@ ko.templateRewriting = (function () { }, memoizeBindingAttributeSyntax: function (htmlString, templateEngine) { - return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () { - return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[6], /* tagToRetain: */ arguments[1], templateEngine); - }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() { - return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "", templateEngine); + return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex(), function () { + return constructMemoizedTagReplacement(/* dataBindAttributeValue: */arguments[6], /* tagToRetain: */arguments[1], templateEngine); + }).replace(memoizeVirtualContainerBindingSyntaxRegex(), function () { + return constructMemoizedTagReplacement(/* dataBindAttributeValue: */arguments[1], /* tagToRetain: */"", templateEngine); }); }, diff --git a/src/virtualElements.js b/src/virtualElements.js index d8c82fd97..478dbcac9 100644 --- a/src/virtualElements.js +++ b/src/virtualElements.js @@ -11,19 +11,25 @@ ko.virtualElements = (function() { // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property. // So, use node.text where available, and node.nodeValue elsewhere var commentNodesHaveTextProperty = document.createComment("test").text === ""; + var startCommentRegex = function (bindingProvider) { + return commentNodesHaveTextProperty ? new RegExp("^$") : new RegExp("^\\s*" + ko.bindingProvider.configuration(bindingProvider).virtualElementTag + "\\s+(.*\\:.*)\\s*$"); + }; - var startCommentRegex = commentNodesHaveTextProperty ? /^$/ : /^\s*ko\s+(.*\:.*)\s*$/; - var endCommentRegex = commentNodesHaveTextProperty ? /^$/ : /^\s*\/ko\s*$/; + var endCommentRegex = function (bindingProvider) { + return commentNodesHaveTextProperty ? new RegExp("^$") : new RegExp("^\\s*\\/" + ko.bindingProvider.configuration(bindingProvider).virtualElementTag + "\\s*$"); + }; var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true }; + function isStartComment(node) { - return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex); + return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex()); } function isEndComment(node) { - return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex); + return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex()); } + function getVirtualChildren(startComment, allowUnbalanced) { var currentNode = startComment; var depth = 1; From d0bd78db8f72f59cb6368d47e16febdeb551fa8e Mon Sep 17 00:00:00 2001 From: GilesBradshaw Date: Fri, 16 Mar 2012 00:44:24 +0000 Subject: [PATCH 3/7] Tests updated to test substituting an alternative binding provider --- spec/defaultBindingsBehaviors.js | 3657 +++++++++++++++--------------- spec/templatingBehaviors.js | 1378 +++++------ src/templating/templateEngine.js | 4 +- 3 files changed, 2546 insertions(+), 2493 deletions(-) diff --git a/spec/defaultBindingsBehaviors.js b/spec/defaultBindingsBehaviors.js index f497b0d02..d3fb5b25b 100755 --- a/spec/defaultBindingsBehaviors.js +++ b/spec/defaultBindingsBehaviors.js @@ -1,1881 +1,1900 @@ -function prepareTestNode() { - var existingNode = document.getElementById("testNode"); - if (existingNode != null) - existingNode.parentNode.removeChild(existingNode); - testNode = document.createElement("div"); - testNode.id = "testNode"; - document.body.appendChild(testNode); -} - -function getSelectedValuesFromSelectNode(selectNode) { - var selectedNodes = ko.utils.arrayFilter(selectNode.childNodes, function (node) { return node.selected; }); - return ko.utils.arrayMap(selectedNodes, function (node) { return ko.selectExtensions.readValue(node); }); -} - -describe('Binding: Enable/Disable', { - before_each: prepareTestNode, - - 'Enable means the node is enabled only when the value is true': function () { - var observable = new ko.observable(); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - - value_of(testNode.childNodes[0].disabled).should_be(true); - observable(1); - value_of(testNode.childNodes[0].disabled).should_be(false); - }, - - 'Disable means the node is enabled only when the value is false': function () { - var observable = new ko.observable(); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - - value_of(testNode.childNodes[0].disabled).should_be(false); - observable(1); - value_of(testNode.childNodes[0].disabled).should_be(true); - }, - - 'Enable should unwrap observables implicitly': function () { - var observable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - value_of(testNode.childNodes[0].disabled).should_be(true); - }, - - 'Disable should unwrap observables implicitly': function () { - var observable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - value_of(testNode.childNodes[0].disabled).should_be(false); - } -}); - -describe('Binding: Visible', { - before_each: prepareTestNode, - - 'Should display the node only when the value is true': function () { - var observable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - - value_of(testNode.childNodes[0].style.display).should_be("none"); - observable(true); - value_of(testNode.childNodes[0].style.display).should_be(""); - }, - - 'Should unwrap observables implicitly': function () { - var observable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ myModelProperty: observable }, testNode); - value_of(testNode.childNodes[0].style.display).should_be("none"); - } -}); - -describe('Binding: Text', { - before_each: prepareTestNode, - - 'Should assign the value to the node, HTML-encoding the value': function () { - var model = { textProp: "'Val \"special\" characters'" }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].textContent || testNode.childNodes[0].innerText).should_be(model.textProp); - }, - - 'Should assign an empty string as value if the model value is null': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - var actualText = "textContent" in testNode.childNodes[0] ? testNode.childNodes[0].textContent : testNode.childNodes[0].innerText; - value_of(actualText).should_be(""); - }, - - 'Should assign an empty string as value if the model value is undefined': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - var actualText = "textContent" in testNode.childNodes[0] ? testNode.childNodes[0].textContent : testNode.childNodes[0].innerText; - value_of(actualText).should_be(""); - }, - - 'Should work with virtual elements, adding a text node between the comments': function () { - var observable = ko.observable("Some text"); - testNode.innerHTML = "xxx "; - ko.applyBindings({textProp: observable}, testNode); - value_of(testNode).should_contain_text("xxx Some text"); - value_of(testNode).should_contain_html("xxx some text"); - - // update observable; should update text - observable("New text"); - value_of(testNode).should_contain_text("xxx New text"); - value_of(testNode).should_contain_html("xxx new text"); - - // clear observable; should remove text - observable(undefined); - value_of(testNode).should_contain_text("xxx "); - value_of(testNode).should_contain_html("xxx "); - }, - - 'Should work with virtual elements, removing any existing stuff between the comments': function () { - testNode.innerHTML = "xxx some random thing that won't be here later"; - ko.applyBindings(null, testNode); - value_of(testNode).should_contain_text("xxx "); - value_of(testNode).should_contain_html("xxx "); - } -}); - -describe('Binding: HTML', { - before_each: prepareTestNode, - - 'Should assign the value to the node without HTML-encoding the value': function () { - var model = { textProp: "My HTML-containing value" }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].innerHTML.toLowerCase()).should_be(model.textProp.toLowerCase()); - value_of(testNode.childNodes[0].childNodes[1].innerHTML).should_be("HTML-containing"); - }, - - 'Should assign an empty string as value if the model value is null': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].innerHTML).should_be(""); - }, - - 'Should assign an empty string as value if the model value is undefined': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].innerHTML).should_be(""); - }, - - 'Should be able to write arbitrary HTML, even if it is not semantically correct': function() { - // Represents issue #98 (https://github.com/SteveSanderson/knockout/issues/98) - // IE 8 and earlier is excessively strict about the use of .innerHTML - it throws - // if you try to write a

tag inside an existing

tag, for example. - var model = { textProp: "

hello

this isn't semantically correct

" }; - testNode.innerHTML = "

"; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0]).should_contain_html(model.textProp); - }, - - 'Should be able to write arbitrary HTML, including elements into tables': function() { - // Some HTML elements are awkward, because the browser implicitly adds surrounding - // elements, or won't allow those elements to be direct children of others. - // The most common examples relate to tables. - var model = { textProp: "hello" }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - - // Accept either of the following outcomes - there may or may not be an implicitly added . - var tr = testNode.childNodes[0].childNodes[0]; - if (tr.tagName == 'TBODY') - tr = tr.childNodes[0]; - - var td = tr.childNodes[0]; - - value_of(tr.tagName).should_be("TR"); - value_of(td.tagName).should_be("TD"); - value_of('innerText' in td ? td.innerText : td.textContent).should_be("hello"); - } -}); - -describe('Binding: Value', { - before_each: prepareTestNode, - - 'Should assign the value to the node': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].value).should_be(123); - }, - - 'Should treat null values as empty strings': function () { - testNode.innerHTML = ""; - ko.applyBindings({ myProp: ko.observable(0) }, testNode); - value_of(testNode.childNodes[0].value).should_be("0"); - }, - - 'Should assign an empty string as value if the model value is null': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].value).should_be(""); - }, - - 'Should assign an empty string as value if the model value is undefined': function () { - testNode.innerHTML = ""; - ko.applyBindings(null, testNode); - value_of(testNode.childNodes[0].value).should_be(""); - }, - - 'For observable values, should unwrap the value and update on change': function () { - var myobservable = new ko.observable(123); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - value_of(testNode.childNodes[0].value).should_be(123); - myobservable(456); - value_of(testNode.childNodes[0].value).should_be(456); - }, - - 'For writeable observable values, should catch the node\'s onchange and write values back to the observable': function () { - var myobservable = new ko.observable(123); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - testNode.childNodes[0].value = "some user-entered value"; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(myobservable()).should_be("some user-entered value"); - }, - - 'For non-observable property values, should catch the node\'s onchange and write values back to the property': function () { - var model = { modelProperty123: 456 }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].value).should_be(456); - - testNode.childNodes[0].value = 789; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(model.modelProperty123).should_be(789); - }, - - 'Should be able to write to observable subproperties of an observable, even after the parent observable has changed': function () { - // This spec represents https://github.com/SteveSanderson/knockout/issues#issue/13 - var originalSubproperty = ko.observable("original value"); - var newSubproperty = ko.observable(); - var model = { myprop: ko.observable({ subproperty : originalSubproperty }) }; - - // Set up a text box whose value is linked to the subproperty of the observable's current value - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].value).should_be("original value"); - - model.myprop({ subproperty : newSubproperty }); // Note that myprop (and hence its subproperty) is changed *after* the bindings are applied - testNode.childNodes[0].value = "Some new value"; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - - // Verify that the change was written to the *new* subproperty, not the one referenced when the bindings were first established - value_of(newSubproperty()).should_be("Some new value"); - value_of(originalSubproperty()).should_be("original value"); - }, - - 'Should only register one single onchange handler': function () { - var notifiedValues = []; - var myobservable = new ko.observable(123); - myobservable.subscribe(function (value) { notifiedValues.push(value); }); - value_of(notifiedValues.length).should_be(0); - - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - // Implicitly observe the number of handlers by seeing how many times "myobservable" - // receives a new value for each onchange on the text box. If there's just one handler, - // we'll see one new value per onchange event. More handlers cause more notifications. - testNode.childNodes[0].value = "ABC"; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(notifiedValues.length).should_be(1); - - testNode.childNodes[0].value = "DEF"; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(notifiedValues.length).should_be(2); - }, - - 'Should be able to catch updates after specific events (e.g., keyup) instead of onchange': function () { - var myobservable = new ko.observable(123); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - testNode.childNodes[0].value = "some user-entered value"; - ko.utils.triggerEvent(testNode.childNodes[0], "keyup"); - value_of(myobservable()).should_be("some user-entered value"); - }, - - 'Should catch updates on change as well as the nominated valueUpdate event': function () { - // Represents issue #102 (https://github.com/SteveSanderson/knockout/issues/102) - var myobservable = new ko.observable(123); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - testNode.childNodes[0].value = "some user-entered value"; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(myobservable()).should_be("some user-entered value"); - }, - - 'For select boxes, should update selectedIndex when the model changes (options specified before value)': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(1); - value_of(observable()).should_be('B'); - - observable('A'); - value_of(testNode.childNodes[0].selectedIndex).should_be(0); - value_of(observable()).should_be('A'); - }, - - 'For select boxes, should update selectedIndex when the model changes (value specified before options)': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(1); - value_of(observable()).should_be('B'); - - observable('A'); - value_of(testNode.childNodes[0].selectedIndex).should_be(0); - value_of(observable()).should_be('A'); - }, - - 'For select boxes, should display the caption when the model value changes to undefined': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(2); - observable(undefined); - value_of(testNode.childNodes[0].selectedIndex).should_be(0); - }, - - 'For select boxes, should update the model value when the UI is changed (setting it to undefined when the caption is selected)': function () { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - var dropdown = testNode.childNodes[0]; - - dropdown.selectedIndex = 1; - ko.utils.triggerEvent(dropdown, "change"); - value_of(observable()).should_be("A"); - - dropdown.selectedIndex = 0; - ko.utils.triggerEvent(dropdown, "change"); - value_of(observable()).should_be(undefined); - }, - - 'For select boxes, should be able to associate option values with arbitrary objects (not just strings)': function() { - var x = {}, y = {}; - var selectedValue = ko.observable(y); - testNode.innerHTML = ""; - var dropdown = testNode.childNodes[0]; - ko.applyBindings({ myOptions: [x, y], selectedValue: selectedValue }, testNode); - - // Check the UI displays the entry corresponding to the chosen value - value_of(dropdown.selectedIndex).should_be(1); - - // Check that when we change the model value, the UI is updated - selectedValue(x); - value_of(dropdown.selectedIndex).should_be(0); - - // Check that when we change the UI, this changes the model value - dropdown.selectedIndex = 1; - ko.utils.triggerEvent(dropdown, "change"); - value_of(selectedValue()).should_be(y); - }, - - 'For select boxes, should automatically initialize the model property to match the first option value if no option value matches the current model property value': function() { - // The rationale here is that we always want the model value to match the option that appears to be selected in the UI - // * If there is *any* option value that equals the model value, we'd initalise the select box such that *that* option is the selected one - // * If there is *no* option value that equals the model value (often because the model value is undefined), we should set the model - // value to match an arbitrary option value to avoid inconsistency between the visible UI and the model - var observable = new ko.observable(); // Undefined by default - - // Should work with options specified before value - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(observable()).should_be("A"); - - // ... and with value specified before options - testNode.innerHTML = ""; - observable(undefined); - value_of(observable()).should_be(undefined); - ko.applyBindings({ myObservable: observable }, testNode); - value_of(observable()).should_be("A"); - }, - - 'For nonempty select boxes, should reject model values that don\'t match any option value, resetting the model value to whatever is visibly selected in the UI': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(1); - - observable('D'); // This change should be rejected, as there's no corresponding option in the UI - value_of(observable()).should_not_be('D'); - }, - - 'For empty select boxes, should reject model values that don\'t match any option value, resetting the model value to undefined': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(-1); // nothing selected - - observable('D'); // This change should be rejected, as there's no corresponding option in the UI - value_of(observable()).should_be(undefined); - }, - - 'For empty select boxes, should reject model values that don\'t match any option value (value before options [also with no options])': function() { - var observable = new ko.observable('B'); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(-1); // nothing selected - - observable('D'); // This change should be rejected, as there's no corresponding option in the UI - value_of(observable()).should_be(undefined); - }, - - 'For select boxes, should clear value if selected item is deleted': function() { - var observable = new ko.observable('B'); - var observableArray = new ko.observableArray(["A", "B"]); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(2); - - observableArray.remove('B'); - value_of(testNode.childNodes[0].selectedIndex).should_be(0); - value_of(observable()).should_be(undefined); - }, - - 'For select boxes, should set value to first in list if selected item is deleted and no caption': function() { - var observable = new ko.observable('B'); - var observableArray = new ko.observableArray(["A", "B"]); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(1); - - observableArray.remove('B'); - value_of(testNode.childNodes[0].selectedIndex).should_be(0); - value_of(observable()).should_be("A"); - }, - - 'For select boxes, should clear value if select is cleared and no caption': function() { - var observable = new ko.observable('B'); - var observableArray = new ko.observableArray(["A", "B"]); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); - value_of(testNode.childNodes[0].selectedIndex).should_be(1); - - observableArray([]); - value_of(testNode.childNodes[0].selectedIndex).should_be(-1); - value_of(observable()).should_be(undefined); - }, - - 'For select boxes, option values can be numerical, and are not implicitly converted to strings': function() { - var observable = new ko.observable(30); - testNode.innerHTML = ""; - ko.applyBindings({ myObservable: observable }, testNode); - - // First check that numerical model values will match a dropdown option - value_of(testNode.childNodes[0].selectedIndex).should_be(2); // 3rd element, zero-indexed - - // Then check that dropdown options map back to numerical model values - testNode.childNodes[0].selectedIndex = 1; - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(typeof observable()).should_be("number"); - value_of(observable()).should_be(20); - } -}) - -describe('Binding: Options', { - before_each: prepareTestNode, - - // Todo: when the options list is populated, this should trigger a change event so that observers are notified of the new value (i.e., the default selection) - - 'Should only be applicable to SELECT nodes': function () { - var threw = false; - testNode.innerHTML = ""; - try { ko.applyBindings({}, testNode); } - catch (ex) { threw = true; } - value_of(threw).should_be(true); - }, - - 'Should set the SELECT node\'s options set to match the model value': function () { - var observable = new ko.observableArray(["A", "B", "C"]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: observable }, testNode); - var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); - value_of(displayedOptions).should_be(["A", "B", "C"]); - }, - - 'Should accept optionsText and optionsValue params to display subproperties of the model values': function() { - var modelValues = new ko.observableArray([ - { name: 'bob', id: ko.observable(6) }, // Note that subproperties can be observable - { name: ko.observable('frank'), id: 13 } - ]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: modelValues }, testNode); - var displayedText = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); - var displayedValues = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.value; }); - value_of(displayedText).should_be(["bob", "frank"]); - value_of(displayedValues).should_be([6, 13]); - }, - - 'Should accept function in optionsText param to display subproperties of the model values': function() { - var modelValues = new ko.observableArray([ - { name: 'bob', job: 'manager' }, - { name: 'frank', job: 'coder & tester' } - ]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: modelValues }, testNode); - var displayedText = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerText || node.textContent; }); - value_of(displayedText).should_be(["bob (manager)", "frank (coder & tester)"]); - }, - - 'Should update the SELECT node\'s options if the model changes': function () { - var observable = new ko.observableArray(["A", "B", "C"]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: observable }, testNode); - observable.splice(1, 1); - var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); - value_of(displayedOptions).should_be(["A", "C"]); - }, - - 'Should retain as much selection as possible when changing the SELECT node\'s options': function () { - var observable = new ko.observableArray(["A", "B", "C"]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: observable }, testNode); - value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be(["B"]); - }, - - 'Should place a caption at the top of the options list and display it when the model value is undefined': function() { - testNode.innerHTML = ""; - ko.applyBindings({}, testNode); - var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); - value_of(displayedOptions).should_be(["Select one...", "A", "B"]); - } -}); - -describe('Binding: Selected Options', { - before_each: prepareTestNode, - - 'Should only be applicable to SELECT nodes': function () { - var threw = false; - testNode.innerHTML = ""; - try { ko.applyBindings({}, testNode); } - catch (ex) { threw = true; } - value_of(threw).should_be(true); - }, - - 'Should set selection in the SELECT node to match the model': function () { - var bObject = {}; - var values = new ko.observableArray(["A", bObject, "C"]); - var selection = new ko.observableArray([bObject]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: values, mySelection: selection }, testNode); - - value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be([bObject]); - selection.push("C"); - value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be([bObject, "C"]); - }, - - 'Should update the model when selection in the SELECT node changes': function () { - function setMultiSelectOptionSelectionState(optionElement, state) { - // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) - if (/MSIE 6/i.test(navigator.userAgent)) - optionElement.setAttribute('selected', state); - else - optionElement.selected = state; +var defaultBindingsBehaviors = + function (testConfiguration) { + var existingBindingProvider = ko.bindingProvider.instance; + var myConfiguration = testConfiguration; + var bindingAttribute; + + + function prepareTestNode() { + var existingNode = document.getElementById("testNode"); + if (existingNode != null) + existingNode.parentNode.removeChild(existingNode); + testNode = document.createElement("div"); + testNode.id = "testNode"; + document.body.appendChild(testNode); + ko.bindingProvider["instance"] = new ko.bindingProvider(myConfiguration); + virtualElementTag = ko.bindingProvider["instance"].configuration.virtualElementTag; + bindingAttribute = ko.bindingProvider["instance"].configuration.bindingAttribute; + + } - - var cObject = {}; - var values = new ko.observableArray(["A", "B", cObject]); - var selection = new ko.observableArray(["B"]); - testNode.innerHTML = ""; - ko.applyBindings({ myValues: values, mySelection: selection }, testNode); - - value_of(selection()).should_be(["B"]); - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0], true); - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[1], false); - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[2], true); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - - value_of(selection()).should_be(["A", cObject]); - value_of(selection()[1] === cObject).should_be(true); // Also check with strict equality, because we don't want to falsely accept [object Object] == cObject - }, - - 'Should update the model when selection in the SELECT node inside an optgroup changes': function () { - function setMultiSelectOptionSelectionState(optionElement, state) { - // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) - if (/MSIE 6/i.test(navigator.userAgent)) - optionElement.setAttribute('selected', state); - else - optionElement.selected = state; + + function restore() { + ko.bindingProvider.instance = existingBindingProvider; } - var selection = new ko.observableArray([]); - testNode.innerHTML = ""; - ko.applyBindings({ mySelection: selection }, testNode); + function getSelectedValuesFromSelectNode(selectNode) { + var selectedNodes = ko.utils.arrayFilter(selectNode.childNodes, function (node) { return node.selected; }); + return ko.utils.arrayMap(selectedNodes, function (node) { return ko.selectExtensions.readValue(node); }); + } - value_of(selection()).should_be([]); + describe('Binding: Enable/Disable ' + (myConfiguration ? myConfiguration.name : "default provider settings"), { + before_each: prepareTestNode, + after_each: restore, + 'Enable means the node is enabled only when the value is true': function () { + var observable = new ko.observable(); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); + + value_of(testNode.childNodes[0].disabled).should_be(true); + observable(1); + value_of(testNode.childNodes[0].disabled).should_be(false); + }, - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[0], true); - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[1], false); - setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[2], true); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); + 'Disable means the node is enabled only when the value is false': function () { + var observable = new ko.observable(); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); - value_of(selection()).should_be(['a', 'c']); - } -}); - -describe('Binding: Submit', { - before_each: prepareTestNode, - - 'Should invoke the supplied function on submit and prevent default action, using model as \'this\' param and the form node as a param to the handler': function () { - var firstParamStored; - var model = { wasCalled: false, doCall: function (firstParam) { this.wasCalled = true; firstParamStored = firstParam; } }; - testNode.innerHTML = "
"; - var formNode = testNode.childNodes[0]; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "submit"); - value_of(model.wasCalled).should_be(true); - value_of(firstParamStored).should_be(formNode); - } -}); + value_of(testNode.childNodes[0].disabled).should_be(false); + observable(1); + value_of(testNode.childNodes[0].disabled).should_be(true); + }, + + 'Enable should unwrap observables implicitly': function () { + var observable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); + value_of(testNode.childNodes[0].disabled).should_be(true); + }, + + 'Disable should unwrap observables implicitly': function () { + var observable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); + value_of(testNode.childNodes[0].disabled).should_be(false); + } + }); -describe('Binding: Event', { - before_each: prepareTestNode, + describe('Binding: Visible', { + before_each: prepareTestNode, + after_each: restore, + 'Should display the node only when the value is true': function () { + var observable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); + + value_of(testNode.childNodes[0].style.display).should_be("none"); + observable(true); + value_of(testNode.childNodes[0].style.display).should_be(""); + }, - 'Should invoke the supplied function when the event occurs, using model as \'this\' param and first arg, and event as second arg': function () { - var model = { - firstWasCalled: false, - firstHandler: function (passedModel, evt) { - value_of(evt.type).should_be("click"); - value_of(this).should_be(model); - value_of(passedModel).should_be(model); + 'Should unwrap observables implicitly': function () { + var observable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ myModelProperty: observable }, testNode); + value_of(testNode.childNodes[0].style.display).should_be("none"); + } + }); - value_of(model.firstWasCalled).should_be(false); - model.firstWasCalled = true; + describe('Binding: Text', { + before_each: prepareTestNode, + after_each: restore, + 'Should assign the value to the node, HTML-encoding the value': function () { + var model = { textProp: "'Val \"special\" characters'" }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].textContent || testNode.childNodes[0].innerText).should_be(model.textProp); }, - secondWasCalled: false, - secondHandler: function (passedModel, evt) { - value_of(evt.type).should_be("mouseover"); - value_of(this).should_be(model); - value_of(passedModel).should_be(model); + 'Should assign an empty string as value if the model value is null': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + var actualText = "textContent" in testNode.childNodes[0] ? testNode.childNodes[0].textContent : testNode.childNodes[0].innerText; + value_of(actualText).should_be(""); + }, - value_of(model.secondWasCalled).should_be(false); - model.secondWasCalled = true; + 'Should assign an empty string as value if the model value is undefined': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + var actualText = "textContent" in testNode.childNodes[0] ? testNode.childNodes[0].textContent : testNode.childNodes[0].innerText; + value_of(actualText).should_be(""); + }, + + 'Should work with virtual elements, adding a text node between the comments': function () { + var observable = ko.observable("Some text"); + testNode.innerHTML = "xxx "; + ko.applyBindings({ textProp: observable }, testNode); + value_of(testNode).should_contain_text("xxx Some text"); + value_of(testNode).should_contain_html("xxx some text"); + + // update observable; should update text + observable("New text"); + value_of(testNode).should_contain_text("xxx New text"); + value_of(testNode).should_contain_html("xxx new text"); + + // clear observable; should remove text + observable(undefined); + value_of(testNode).should_contain_text("xxx "); + value_of(testNode).should_contain_html("xxx "); + }, + + 'Should work with virtual elements, removing any existing stuff between the comments': function () { + testNode.innerHTML = "xxx some random thing that won't be here later"; + ko.applyBindings(null, testNode); + value_of(testNode).should_contain_text("xxx "); + value_of(testNode).should_contain_html("xxx "); } - }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.firstWasCalled).should_be(true); - value_of(model.secondWasCalled).should_be(false); - ko.utils.triggerEvent(testNode.childNodes[0], "mouseover"); - value_of(model.secondWasCalled).should_be(true); - ko.utils.triggerEvent(testNode.childNodes[0], "mouseout"); // Shouldn't do anything (specifically, shouldn't throw) - }, - - 'Should prevent default action': function () { - testNode.innerHTML = "hey"; - ko.applyBindings(null, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - // Assuming we haven't been redirected to http://www.example.com/, this spec has now passed - }, - - 'Should let bubblable events bubble to parent elements by default': function() { - var model = { - innerWasCalled: false, innerDoCall: function () { this.innerWasCalled = true; }, - outerWasCalled: false, outerDoCall: function () { this.outerWasCalled = true; } - }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(model.innerWasCalled).should_be(true); - value_of(model.outerWasCalled).should_be(true); - }, - - 'Should be able to prevent bubbling of bubblable events using the (eventname)Bubble:false option': function() { - var model = { - innerWasCalled: false, innerDoCall: function () { this.innerWasCalled = true; }, - outerWasCalled: false, outerDoCall: function () { this.outerWasCalled = true; } - }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(model.innerWasCalled).should_be(true); - value_of(model.outerWasCalled).should_be(false); - }, - - 'Should be able to supply event type as event.type': function() { - var model = { clickCalled: false }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.clickCalled).should_be(true); - }, - - 'Should call function in correct context if using \'eventHandlersUseObjectForThis\' option': function() { - var model = { subModel: { wasCalled: false, clickFunc: function() {this.wasCalled = true} } }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode, {eventHandlersUseObjectForThis: true}); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.subModel.wasCalled).should_be(true); - }, - - 'Should be able to supply handler params using "bind" helper': function() { - // Using "bind" like this just eliminates the function literal wrapper - it's purely stylistic - var didCallHandler = false, someObj = {}; - var myHandler = function() { - value_of(this).should_be(someObj); - value_of(arguments.length).should_be(5); - - // First x args will be the ones you bound - value_of(arguments[0]).should_be(123); - value_of(arguments[1]).should_be("another"); - value_of(arguments[2].something).should_be(true); - - // Then you get the args we normally pass to handlers, i.e., the model then the event - value_of(arguments[3]).should_be(viewModel); - value_of(arguments[4].type).should_be("mouseover"); - - didCallHandler = true; - }; - testNode.innerHTML = ""; - var viewModel = { myHandler: myHandler, someObj: someObj }; - ko.applyBindings(viewModel, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "mouseover"); - value_of(didCallHandler).should_be(true); - } -}); - -describe('Binding: Click', { - // This is just a special case of the "event" binding, so not necessary to respecify all its behaviours - before_each: prepareTestNode, - - 'Should invoke the supplied function on click, using model as \'this\' param and first arg, and event as second arg': function () { - var model = { - wasCalled: false, - doCall: function (arg1, arg2) { - this.wasCalled = true; - value_of(arg1).should_be(model); - value_of(arg2.type).should_be("click"); - } - }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.wasCalled).should_be(true); - } -}); - -describe('Binding: CSS class name', { - before_each: prepareTestNode, - - 'Should give the element the specific CSS class only when the specified value is true': function () { - var observable1 = new ko.observable(); - var observable2 = new ko.observable(true); - testNode.innerHTML = "
Hallo
"; - ko.applyBindings({ someModelProperty: observable1, anotherModelProperty: observable2 }, testNode); - - value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 anotherRule"); - observable1(true); - value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 anotherRule myRule"); - observable2(false); - value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 myRule"); - }, - - 'Should give the element a single CSS class without a leading space when the specified value is true': function() { - var observable1 = new ko.observable(); - testNode.innerHTML = "
Hallo
"; - ko.applyBindings({ someModelProperty: observable1 }, testNode); - - value_of(testNode.childNodes[0].className).should_be(""); - observable1(true); - value_of(testNode.childNodes[0].className).should_be("myRule"); - } -}); + }); -describe('Binding: CSS style', { - before_each: prepareTestNode, + describe('Binding: HTML', { + before_each: prepareTestNode, + after_each: restore, + 'Should assign the value to the node without HTML-encoding the value': function () { + var model = { textProp: "My HTML-containing value" }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].innerHTML.toLowerCase()).should_be(model.textProp.toLowerCase()); + value_of(testNode.childNodes[0].childNodes[1].innerHTML).should_be("HTML-containing"); + }, - 'Should give the element the specified CSS style value': function () { - var myObservable = new ko.observable("red"); - testNode.innerHTML = "
Hallo
"; - ko.applyBindings({ colorValue: myObservable }, testNode); + 'Should assign an empty string as value if the model value is null': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + value_of(testNode.childNodes[0].innerHTML).should_be(""); + }, - value_of(testNode.childNodes[0].style.backgroundColor).should_be_one_of(["red", "#ff0000"]); // Opera returns style color values in #rrggbb notation, unlike other browsers - myObservable("green"); - value_of(testNode.childNodes[0].style.backgroundColor).should_be_one_of(["green", "#008000"]); - myObservable(undefined); - value_of(testNode.childNodes[0].style.backgroundColor).should_be(""); - } -}); + 'Should assign an empty string as value if the model value is undefined': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + value_of(testNode.childNodes[0].innerHTML).should_be(""); + }, -describe('Binding: Unique Name', { - before_each: prepareTestNode, + 'Should be able to write arbitrary HTML, even if it is not semantically correct': function () { + // Represents issue #98 (https://github.com/SteveSanderson/knockout/issues/98) + // IE 8 and earlier is excessively strict about the use of .innerHTML - it throws + // if you try to write a

tag inside an existing

tag, for example. + var model = { textProp: "

hello

this isn't semantically correct

" }; + testNode.innerHTML = "

"; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0]).should_contain_html(model.textProp); + }, - 'Should apply a different name to each element': function () { - testNode.innerHTML = "
"; - ko.applyBindings({}, testNode); + 'Should be able to write arbitrary HTML, including elements into tables': function () { + // Some HTML elements are awkward, because the browser implicitly adds surrounding + // elements, or won't allow those elements to be direct children of others. + // The most common examples relate to tables. + var model = { textProp: "hello" }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].name.length > 0).should_be(true); - value_of(testNode.childNodes[1].name.length > 0).should_be(true); - value_of(testNode.childNodes[0].name == testNode.childNodes[1].name).should_be(false); - }, + // Accept either of the following outcomes - there may or may not be an implicitly added . + var tr = testNode.childNodes[0].childNodes[0]; + if (tr.tagName == 'TBODY') + tr = tr.childNodes[0]; - 'Should work without a value': function () { - testNode.innerHTML = "
"; - ko.applyBindings({}, testNode); + var td = tr.childNodes[0]; - value_of(testNode.childNodes[0].name.length > 0).should_be(true); - value_of(testNode.childNodes[1].name.length > 0).should_be(true); - value_of(testNode.childNodes[0].name == testNode.childNodes[1].name).should_be(false); - } -}); - -describe('Binding: Checked', { - before_each: prepareTestNode, - - 'Triggering a click should toggle a checkbox\'s checked state before the event handler fires': function() { - // This isn't strictly to do with the checked binding, but if this doesn't work, the rest of the specs aren't meaningful - testNode.innerHTML = ""; - var clickHandlerFireCount = 0, expectedCheckedStateInHandler; - ko.utils.registerEventHandler(testNode.childNodes[0], "click", function() { - clickHandlerFireCount++; - value_of(testNode.childNodes[0].checked).should_be(expectedCheckedStateInHandler); + value_of(tr.tagName).should_be("TR"); + value_of(td.tagName).should_be("TD"); + value_of('innerText' in td ? td.innerText : td.textContent).should_be("hello"); + } + }); + + describe('Binding: Value', { + before_each: prepareTestNode, + after_each: restore, + 'Should assign the value to the node': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + value_of(testNode.childNodes[0].value).should_be(123); + }, + + 'Should treat null values as empty strings': function () { + testNode.innerHTML = ""; + ko.applyBindings({ myProp: ko.observable(0) }, testNode); + value_of(testNode.childNodes[0].value).should_be("0"); + }, + + 'Should assign an empty string as value if the model value is null': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + value_of(testNode.childNodes[0].value).should_be(""); + }, + + 'Should assign an empty string as value if the model value is undefined': function () { + testNode.innerHTML = ""; + ko.applyBindings(null, testNode); + value_of(testNode.childNodes[0].value).should_be(""); + }, + + 'For observable values, should unwrap the value and update on change': function () { + var myobservable = new ko.observable(123); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + value_of(testNode.childNodes[0].value).should_be(123); + myobservable(456); + value_of(testNode.childNodes[0].value).should_be(456); + }, + + 'For writeable observable values, should catch the node\'s onchange and write values back to the observable': function () { + var myobservable = new ko.observable(123); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + testNode.childNodes[0].value = "some user-entered value"; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(myobservable()).should_be("some user-entered value"); + }, + + 'For non-observable property values, should catch the node\'s onchange and write values back to the property': function () { + var model = { modelProperty123: 456 }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].value).should_be(456); + + testNode.childNodes[0].value = 789; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(model.modelProperty123).should_be(789); + }, + + 'Should be able to write to observable subproperties of an observable, even after the parent observable has changed': function () { + // This spec represents https://github.com/SteveSanderson/knockout/issues#issue/13 + var originalSubproperty = ko.observable("original value"); + var newSubproperty = ko.observable(); + var model = { myprop: ko.observable({ subproperty: originalSubproperty }) }; + + // Set up a text box whose value is linked to the subproperty of the observable's current value + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].value).should_be("original value"); + + model.myprop({ subproperty: newSubproperty }); // Note that myprop (and hence its subproperty) is changed *after* the bindings are applied + testNode.childNodes[0].value = "Some new value"; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + + // Verify that the change was written to the *new* subproperty, not the one referenced when the bindings were first established + value_of(newSubproperty()).should_be("Some new value"); + value_of(originalSubproperty()).should_be("original value"); + }, + + 'Should only register one single onchange handler': function () { + var notifiedValues = []; + var myobservable = new ko.observable(123); + myobservable.subscribe(function (value) { notifiedValues.push(value); }); + value_of(notifiedValues.length).should_be(0); + + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + // Implicitly observe the number of handlers by seeing how many times "myobservable" + // receives a new value for each onchange on the text box. If there's just one handler, + // we'll see one new value per onchange event. More handlers cause more notifications. + testNode.childNodes[0].value = "ABC"; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(notifiedValues.length).should_be(1); + + testNode.childNodes[0].value = "DEF"; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(notifiedValues.length).should_be(2); + }, + + 'Should be able to catch updates after specific events (e.g., keyup) instead of onchange': function () { + var myobservable = new ko.observable(123); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + testNode.childNodes[0].value = "some user-entered value"; + ko.utils.triggerEvent(testNode.childNodes[0], "keyup"); + value_of(myobservable()).should_be("some user-entered value"); + }, + + 'Should catch updates on change as well as the nominated valueUpdate event': function () { + // Represents issue #102 (https://github.com/SteveSanderson/knockout/issues/102) + var myobservable = new ko.observable(123); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + testNode.childNodes[0].value = "some user-entered value"; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(myobservable()).should_be("some user-entered value"); + }, + + 'For select boxes, should update selectedIndex when the model changes (options specified before value)': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(1); + value_of(observable()).should_be('B'); + + observable('A'); + value_of(testNode.childNodes[0].selectedIndex).should_be(0); + value_of(observable()).should_be('A'); + }, + + 'For select boxes, should update selectedIndex when the model changes (value specified before options)': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(1); + value_of(observable()).should_be('B'); + + observable('A'); + value_of(testNode.childNodes[0].selectedIndex).should_be(0); + value_of(observable()).should_be('A'); + }, + + 'For select boxes, should display the caption when the model value changes to undefined': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(2); + observable(undefined); + value_of(testNode.childNodes[0].selectedIndex).should_be(0); + }, + + 'For select boxes, should update the model value when the UI is changed (setting it to undefined when the caption is selected)': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + var dropdown = testNode.childNodes[0]; + + dropdown.selectedIndex = 1; + ko.utils.triggerEvent(dropdown, "change"); + value_of(observable()).should_be("A"); + + dropdown.selectedIndex = 0; + ko.utils.triggerEvent(dropdown, "change"); + value_of(observable()).should_be(undefined); + }, + + 'For select boxes, should be able to associate option values with arbitrary objects (not just strings)': function () { + var x = {}, y = {}; + var selectedValue = ko.observable(y); + testNode.innerHTML = ""; + var dropdown = testNode.childNodes[0]; + ko.applyBindings({ myOptions: [x, y], selectedValue: selectedValue }, testNode); + + // Check the UI displays the entry corresponding to the chosen value + value_of(dropdown.selectedIndex).should_be(1); + + // Check that when we change the model value, the UI is updated + selectedValue(x); + value_of(dropdown.selectedIndex).should_be(0); + + // Check that when we change the UI, this changes the model value + dropdown.selectedIndex = 1; + ko.utils.triggerEvent(dropdown, "change"); + value_of(selectedValue()).should_be(y); + }, + + 'For select boxes, should automatically initialize the model property to match the first option value if no option value matches the current model property value': function () { + // The rationale here is that we always want the model value to match the option that appears to be selected in the UI + // * If there is *any* option value that equals the model value, we'd initalise the select box such that *that* option is the selected one + // * If there is *no* option value that equals the model value (often because the model value is undefined), we should set the model + // value to match an arbitrary option value to avoid inconsistency between the visible UI and the model + var observable = new ko.observable(); // Undefined by default + + // Should work with options specified before value + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(observable()).should_be("A"); + + // ... and with value specified before options + testNode.innerHTML = ""; + observable(undefined); + value_of(observable()).should_be(undefined); + ko.applyBindings({ myObservable: observable }, testNode); + value_of(observable()).should_be("A"); + }, + + 'For nonempty select boxes, should reject model values that don\'t match any option value, resetting the model value to whatever is visibly selected in the UI': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(1); + + observable('D'); // This change should be rejected, as there's no corresponding option in the UI + value_of(observable()).should_not_be('D'); + }, + + 'For empty select boxes, should reject model values that don\'t match any option value, resetting the model value to undefined': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(-1); // nothing selected + + observable('D'); // This change should be rejected, as there's no corresponding option in the UI + value_of(observable()).should_be(undefined); + }, + + 'For empty select boxes, should reject model values that don\'t match any option value (value before options [also with no options])': function () { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(-1); // nothing selected + + observable('D'); // This change should be rejected, as there's no corresponding option in the UI + value_of(observable()).should_be(undefined); + }, + + 'For select boxes, should clear value if selected item is deleted': function () { + var observable = new ko.observable('B'); + var observableArray = new ko.observableArray(["A", "B"]); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(2); + + observableArray.remove('B'); + value_of(testNode.childNodes[0].selectedIndex).should_be(0); + value_of(observable()).should_be(undefined); + }, + + 'For select boxes, should set value to first in list if selected item is deleted and no caption': function () { + var observable = new ko.observable('B'); + var observableArray = new ko.observableArray(["A", "B"]); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(1); + + observableArray.remove('B'); + value_of(testNode.childNodes[0].selectedIndex).should_be(0); + value_of(observable()).should_be("A"); + }, + + 'For select boxes, should clear value if select is cleared and no caption': function () { + var observable = new ko.observable('B'); + var observableArray = new ko.observableArray(["A", "B"]); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable, myObservableArray: observableArray }, testNode); + value_of(testNode.childNodes[0].selectedIndex).should_be(1); + + observableArray([]); + value_of(testNode.childNodes[0].selectedIndex).should_be(-1); + value_of(observable()).should_be(undefined); + }, + + 'For select boxes, option values can be numerical, and are not implicitly converted to strings': function () { + var observable = new ko.observable(30); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + + // First check that numerical model values will match a dropdown option + value_of(testNode.childNodes[0].selectedIndex).should_be(2); // 3rd element, zero-indexed + + // Then check that dropdown options map back to numerical model values + testNode.childNodes[0].selectedIndex = 1; + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(typeof observable()).should_be("number"); + value_of(observable()).should_be(20); + } }) - value_of(testNode.childNodes[0].checked).should_be(false); - expectedCheckedStateInHandler = true; - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(testNode.childNodes[0].checked).should_be(true); - value_of(clickHandlerFireCount).should_be(1); - - expectedCheckedStateInHandler = false; - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(testNode.childNodes[0].checked).should_be(false); - value_of(clickHandlerFireCount).should_be(2); - }, - - 'Should be able to control a checkbox\'s checked state': function () { - var myobservable = new ko.observable(true); - testNode.innerHTML = ""; - - ko.applyBindings({ someProp: myobservable }, testNode); - value_of(testNode.childNodes[0].checked).should_be(true); - - myobservable(false); - value_of(testNode.childNodes[0].checked).should_be(false); - }, - - 'Should update observable properties on the underlying model when the checkbox click event fires': function () { - var myobservable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(myobservable()).should_be(true); - }, - - 'Should only notify observable properties on the underlying model *once* even if the checkbox change events fire multiple times': function () { - var myobservable = new ko.observable(); - var timesNotified = 0; - myobservable.subscribe(function() { timesNotified++ }); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - // Multiple events only cause one notification... - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(timesNotified).should_be(1); - - // ... until the checkbox value actually changes - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(timesNotified).should_be(2); - }, - - 'Should update non-observable properties on the underlying model when the checkbox click event fires': function () { - var model = { someProp: false }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.someProp).should_be(true); - }, - - 'Should update observable properties on the underlying model when the checkbox is clicked': function () { - var myobservable = new ko.observable(false); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(myobservable()).should_be(true); - }, - - 'Should update non-observable properties on the underlying model when the checkbox is clicked': function () { - var model = { someProp: false }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.someProp).should_be(true); - }, - - 'Should make a radio button checked if and only if its value matches the bound model property': function () { - var myobservable = new ko.observable("another value"); - testNode.innerHTML = ""; - - ko.applyBindings({ someProp: myobservable }, testNode); - value_of(testNode.childNodes[0].checked).should_be(false); - - myobservable("This Radio Button Value"); - value_of(testNode.childNodes[0].checked).should_be(true); - }, - - 'Should set an observable model property to this radio button\'s value when checked': function () { - var myobservable = new ko.observable("another value"); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - value_of(myobservable()).should_be("another value"); - testNode.childNodes[0].click(); - value_of(myobservable()).should_be("this radio button value"); - }, - - 'Should only notify observable properties on the underlying model *once* even if the radio button change/click events fire multiple times': function () { - var myobservable = new ko.observable("original value"); - var timesNotified = 0; - myobservable.subscribe(function() { timesNotified++ }); - testNode.innerHTML = ""; - ko.applyBindings({ someProp: myobservable }, testNode); - - // Multiple events only cause one notification... - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - ko.utils.triggerEvent(testNode.childNodes[0], "change"); - value_of(timesNotified).should_be(1); - - // ... until you click something with a different value - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - ko.utils.triggerEvent(testNode.childNodes[1], "change"); - value_of(timesNotified).should_be(2); - }, - - 'Should set a non-observable model property to this radio button\'s value when checked': function () { - var model = { someProp: "another value" }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "click"); - value_of(model.someProp).should_be("this radio button value"); - }, - - 'When a checkbox is bound to an array, the checkbox should control whether its value is in that array': function() { - var model = { myArray: ["Existing value", "Unrelated value"] }; - testNode.innerHTML = "" - + ""; - ko.applyBindings(model, testNode); - - value_of(model.myArray).should_be(["Existing value", "Unrelated value"]); - - // Checkbox initial state is determined by whether the value is in the array - value_of(testNode.childNodes[0].checked).should_be(true); - value_of(testNode.childNodes[1].checked).should_be(false); - // Checking the checkbox puts it in the array - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - value_of(testNode.childNodes[1].checked).should_be(true); - value_of(model.myArray).should_be(["Existing value", "Unrelated value", "New value"]); - // Unchecking the checkbox removes it from the array - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - value_of(testNode.childNodes[1].checked).should_be(false); - value_of(model.myArray).should_be(["Existing value", "Unrelated value"]); - }, - - 'When a checkbox is bound to an observable array, the checkbox checked state responds to changes in the array': function() { - var model = { myObservableArray: ko.observableArray(["Unrelated value"]) }; - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - value_of(testNode.childNodes[0].checked).should_be(false); - - // Put the value in the array; observe the checkbox reflect this - model.myObservableArray.push("My value"); - value_of(testNode.childNodes[0].checked).should_be(true); - - // Remove the value from the array; observe the checkbox reflect this - model.myObservableArray.remove("My value"); - value_of(testNode.childNodes[0].checked).should_be(false); - } -}); - -describe('Binding: Attr', { - before_each: prepareTestNode, - - 'Should be able to set arbitrary attribute values': function() { - var model = { myValue: "first value" }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].getAttribute("firstAttribute")).should_be("first value"); - value_of(testNode.childNodes[0].getAttribute("second-attribute")).should_be("true"); - }, - - 'Should be able to set arbitrary attribute values (without quotes)': function() { - var model = { myValue: "first value" }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].getAttribute("firstAttribute")).should_be("first value"); - value_of(testNode.childNodes[0].getAttribute("second-attribute")).should_be("true"); - }, - - 'Should respond to changes in an observable value': function() { - var model = { myprop : ko.observable("initial value") }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("initial value"); - - // Change the observable; observe it reflected in the DOM - model.myprop("new value"); - value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("new value"); - }, - - 'Should remove the attribute if the value is strictly false, null, or undefined': function() { - var model = { myprop : ko.observable() }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); - ko.utils.arrayForEach([false, null, undefined], function(testValue) { - model.myprop("nonempty value"); - value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("nonempty value"); - model.myprop(testValue); - value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be(null); - }); - }, - - 'Should be able to set class attribute and access it using className property': function() { - var model = { myprop : ko.observable("newClass") }; - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].className).should_be("oldClass"); - ko.applyBindings(model, testNode); - value_of(testNode.childNodes[0].className).should_be("newClass"); - // Should be able to clear class also - model.myprop(undefined); - value_of(testNode.childNodes[0].className).should_be(""); - value_of(testNode.childNodes[0].getAttribute("class")).should_be(null); - } -}); - -describe('Binding: Hasfocus', { - before_each: prepareTestNode, - - 'Should respond to changes on an observable value by blurring or focusing the element': function() { - var currentState; - var model = { myVal: ko.observable() } - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - ko.utils.registerEventHandler(testNode.childNodes[0], "focusin", function() { currentState = true }); - ko.utils.registerEventHandler(testNode.childNodes[0], "focusout", function() { currentState = false }); - - // When the value becomes true, we focus - model.myVal(true); - value_of(currentState).should_be(true); - - // When the value becomes false, we blur - model.myVal(false); - value_of(currentState).should_be(false); - }, - - 'Should set an observable value to be true on focus and false on blur': function() { - var model = { myVal: ko.observable() } - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - // Need to raise "focusin" and "focusout" manually, because simply calling ".focus()" and ".blur()" - // in IE doesn't reliably trigger the "focus" and "blur" events synchronously - - ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); - value_of(model.myVal()).should_be(true); - - // Move the focus elsewhere - ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); - value_of(model.myVal()).should_be(false); - }, - - 'Should set a non-observable value to be true on focus and false on blur': function() { - var model = { myVal: null } - testNode.innerHTML = ""; - ko.applyBindings(model, testNode); - - ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); - value_of(model.myVal).should_be(true); - - // Move the focus elsewhere - ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); - value_of(model.myVal).should_be(false); - } -}); - -describe('Binding: If', { - before_each: prepareTestNode, - - 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - ko.applyBindings({ someItem: null }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should leave descendant nodes in the document (and bind them) if the value is truey, independently of the active template engine': function() { - ko.setTemplateEngine(new ko.templateEngine()); // This template engine will just throw errors if you try to use it - testNode.innerHTML = "
"; - value_of(testNode.childNodes.length).should_be(1); - ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' } }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - }, - - 'Should toggle the presence and bindedness of descendant nodes according to the truthiness of the value': function() { - var someItem = ko.observable(undefined); - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: someItem }, testNode); - - // First it's not there - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - - // Then it's there - someItem({ occasionallyExistentChildProp: 'Child prop value' }); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - - // Then it's gone again - someItem(null); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should not interfere with binding context': function() { - testNode.innerHTML = "
Parents:
"; - ko.applyBindings({ }, testNode); - value_of(testNode.childNodes[0]).should_contain_text("Parents: 0"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).should_be(0); - }, - - 'Should be able to define an \"if\" region using a containerless template': function() { - var someitem = ko.observable(undefined); - testNode.innerHTML = "hello goodbye"; - ko.applyBindings({ someitem: someitem }, testNode); - - // First it's not there - value_of(testNode).should_contain_html("hello goodbye"); - - // Then it's there - someitem({ occasionallyexistentchildprop: 'child prop value' }); - value_of(testNode).should_contain_html("hello child prop value goodbye"); - - // Then it's gone again - someitem(null); - value_of(testNode).should_contain_html("hello goodbye"); - }, - - 'Should be able to nest \"if\" regions defined by containerless templates': function() { - var condition1 = ko.observable(false); - var condition2 = ko.observable(false); - testNode.innerHTML = "hello First is trueBoth are true"; - ko.applyBindings({ condition1: condition1, condition2: condition2 }, testNode); - - // First neither are there - value_of(testNode).should_contain_html("hello "); - - // Make outer appear - condition1(true); - value_of(testNode).should_contain_html("hello first is true"); - - // Make inner appear - condition2(true); - value_of(testNode).should_contain_html("hello first is trueboth are true"); - } -}); - -describe('Binding: Ifnot', { - before_each: prepareTestNode, - - 'Should remove descendant nodes from the document (and not bind them) if the value is truey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - ko.applyBindings({ someItem: null, condition: true }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should leave descendant nodes in the document (and bind them) if the value is falsey, independently of the active template engine': function() { - ko.setTemplateEngine(new ko.templateEngine()); // This template engine will just throw errors if you try to use it - testNode.innerHTML = "
"; - value_of(testNode.childNodes.length).should_be(1); - ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' }, condition: false }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - }, - - 'Should toggle the presence and bindedness of descendant nodes according to the falsiness of the value': function() { - var someItem = ko.observable(undefined); - var condition = ko.observable(true); - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: someItem, condition: condition }, testNode); - - // First it's not there - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - - // Then it's there - someItem({ occasionallyExistentChildProp: 'Child prop value' }); - condition(false); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - - // Then it's gone again - condition(true); - someItem(null); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should not interfere with binding context': function() { - testNode.innerHTML = "
Parents:
"; - ko.applyBindings({ }, testNode); - value_of(testNode.childNodes[0]).should_contain_text("Parents: 0"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).should_be(0); - } -}); - -describe('Binding: With Light', { - before_each: prepareTestNode, - - 'Should leave descendant nodes in the document (and bind them in the context of the supplied value) if the value is truey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes.length).should_be(1); - ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' } }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - }, - - 'Should not bind the same elements more than once even if the supplied value notifies a change': function() { - var countedClicks = 0; - var someItem = ko.observable({ - childProp: ko.observable('Hello'), - handleClick: function() { countedClicks++ } + + describe('Binding: Options', { + before_each: prepareTestNode, + after_each: restore, + // Todo: when the options list is populated, this should trigger a change event so that observers are notified of the new value (i.e., the default selection) + + 'Should only be applicable to SELECT nodes': function () { + var threw = false; + testNode.innerHTML = ""; + try { ko.applyBindings({}, testNode); } + catch (ex) { threw = true; } + value_of(threw).should_be(true); + }, + + 'Should set the SELECT node\'s options set to match the model value': function () { + var observable = new ko.observableArray(["A", "B", "C"]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: observable }, testNode); + var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); + value_of(displayedOptions).should_be(["A", "B", "C"]); + }, + + 'Should accept optionsText and optionsValue params to display subproperties of the model values': function () { + var modelValues = new ko.observableArray([ + { name: 'bob', id: ko.observable(6) }, // Note that subproperties can be observable + {name: ko.observable('frank'), id: 13 } + ]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: modelValues }, testNode); + var displayedText = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); + var displayedValues = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.value; }); + value_of(displayedText).should_be(["bob", "frank"]); + value_of(displayedValues).should_be([6, 13]); + }, + + 'Should accept function in optionsText param to display subproperties of the model values': function () { + var modelValues = new ko.observableArray([ + { name: 'bob', job: 'manager' }, + { name: 'frank', job: 'coder & tester' } + ]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: modelValues }, testNode); + var displayedText = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerText || node.textContent; }); + value_of(displayedText).should_be(["bob (manager)", "frank (coder & tester)"]); + }, + + 'Should update the SELECT node\'s options if the model changes': function () { + var observable = new ko.observableArray(["A", "B", "C"]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: observable }, testNode); + observable.splice(1, 1); + var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); + value_of(displayedOptions).should_be(["A", "C"]); + }, + + 'Should retain as much selection as possible when changing the SELECT node\'s options': function () { + var observable = new ko.observableArray(["A", "B", "C"]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: observable }, testNode); + value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be(["B"]); + }, + + 'Should place a caption at the top of the options list and display it when the model value is undefined': function () { + testNode.innerHTML = ""; + ko.applyBindings({}, testNode); + var displayedOptions = ko.utils.arrayMap(testNode.childNodes[0].childNodes, function (node) { return node.innerHTML; }); + value_of(displayedOptions).should_be(["Select one...", "A", "B"]); + } + }); + + describe('Binding: Selected Options', { + before_each: prepareTestNode, + after_each: restore, + 'Should only be applicable to SELECT nodes': function () { + var threw = false; + testNode.innerHTML = ""; + try { ko.applyBindings({}, testNode); } + catch (ex) { threw = true; } + value_of(threw).should_be(true); + }, + + 'Should set selection in the SELECT node to match the model': function () { + var bObject = {}; + var values = new ko.observableArray(["A", bObject, "C"]); + var selection = new ko.observableArray([bObject]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: values, mySelection: selection }, testNode); + + value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be([bObject]); + selection.push("C"); + value_of(getSelectedValuesFromSelectNode(testNode.childNodes[0])).should_be([bObject, "C"]); + }, + + 'Should update the model when selection in the SELECT node changes': function () { + function setMultiSelectOptionSelectionState(optionElement, state) { + // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) + if (/MSIE 6/i.test(navigator.userAgent)) + optionElement.setAttribute('selected', state); + else + optionElement.selected = state; + } + + var cObject = {}; + var values = new ko.observableArray(["A", "B", cObject]); + var selection = new ko.observableArray(["B"]); + testNode.innerHTML = ""; + ko.applyBindings({ myValues: values, mySelection: selection }, testNode); + + value_of(selection()).should_be(["B"]); + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0], true); + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[1], false); + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[2], true); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + + value_of(selection()).should_be(["A", cObject]); + value_of(selection()[1] === cObject).should_be(true); // Also check with strict equality, because we don't want to falsely accept [object Object] == cObject + }, + + 'Should update the model when selection in the SELECT node inside an optgroup changes': function () { + function setMultiSelectOptionSelectionState(optionElement, state) { + // Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html) + if (/MSIE 6/i.test(navigator.userAgent)) + optionElement.setAttribute('selected', state); + else + optionElement.selected = state; + } + + var selection = new ko.observableArray([]); + testNode.innerHTML = ""; + ko.applyBindings({ mySelection: selection }, testNode); + + value_of(selection()).should_be([]); + + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[0], true); + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[1], false); + setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[2], true); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + + value_of(selection()).should_be(['a', 'c']); + } + }); + + describe('Binding: Submit', { + before_each: prepareTestNode, + after_each: restore, + 'Should invoke the supplied function on submit and prevent default action, using model as \'this\' param and the form node as a param to the handler': function () { + var firstParamStored; + var model = { wasCalled: false, doCall: function (firstParam) { this.wasCalled = true; firstParamStored = firstParam; } }; + testNode.innerHTML = ""; + var formNode = testNode.childNodes[0]; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "submit"); + value_of(model.wasCalled).should_be(true); + value_of(firstParamStored).should_be(formNode); + } + }); + + describe('Binding: Event', { + before_each: prepareTestNode, + after_each: restore, + 'Should invoke the supplied function when the event occurs, using model as \'this\' param and first arg, and event as second arg': function () { + var model = { + firstWasCalled: false, + firstHandler: function (passedModel, evt) { + value_of(evt.type).should_be("click"); + value_of(this).should_be(model); + value_of(passedModel).should_be(model); + + value_of(model.firstWasCalled).should_be(false); + model.firstWasCalled = true; + }, + + secondWasCalled: false, + secondHandler: function (passedModel, evt) { + value_of(evt.type).should_be("mouseover"); + value_of(this).should_be(model); + value_of(passedModel).should_be(model); + + value_of(model.secondWasCalled).should_be(false); + model.secondWasCalled = true; + } + }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.firstWasCalled).should_be(true); + value_of(model.secondWasCalled).should_be(false); + ko.utils.triggerEvent(testNode.childNodes[0], "mouseover"); + value_of(model.secondWasCalled).should_be(true); + ko.utils.triggerEvent(testNode.childNodes[0], "mouseout"); // Shouldn't do anything (specifically, shouldn't throw) + }, + + 'Should prevent default action': function () { + testNode.innerHTML = "hey"; + ko.applyBindings(null, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + // Assuming we haven't been redirected to http://www.example.com/, this spec has now passed + }, + + 'Should let bubblable events bubble to parent elements by default': function () { + var model = { + innerWasCalled: false, innerDoCall: function () { this.innerWasCalled = true; }, + outerWasCalled: false, outerDoCall: function () { this.outerWasCalled = true; } + }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(model.innerWasCalled).should_be(true); + value_of(model.outerWasCalled).should_be(true); + }, + + 'Should be able to prevent bubbling of bubblable events using the (eventname)Bubble:false option': function () { + var model = { + innerWasCalled: false, innerDoCall: function () { this.innerWasCalled = true; }, + outerWasCalled: false, outerDoCall: function () { this.outerWasCalled = true; } + }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(model.innerWasCalled).should_be(true); + value_of(model.outerWasCalled).should_be(false); + }, + + 'Should be able to supply event type as event.type': function () { + var model = { clickCalled: false }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.clickCalled).should_be(true); + }, + + 'Should call function in correct context if using \'eventHandlersUseObjectForThis\' option': function () { + var model = { subModel: { wasCalled: false, clickFunc: function () { this.wasCalled = true } } }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode, { eventHandlersUseObjectForThis: true }); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.subModel.wasCalled).should_be(true); + }, + + 'Should be able to supply handler params using "bind" helper': function () { + // Using "bind" like this just eliminates the function literal wrapper - it's purely stylistic + var didCallHandler = false, someObj = {}; + var myHandler = function () { + value_of(this).should_be(someObj); + value_of(arguments.length).should_be(5); + + // First x args will be the ones you bound + value_of(arguments[0]).should_be(123); + value_of(arguments[1]).should_be("another"); + value_of(arguments[2].something).should_be(true); + + // Then you get the args we normally pass to handlers, i.e., the model then the event + value_of(arguments[3]).should_be(viewModel); + value_of(arguments[4].type).should_be("mouseover"); + + didCallHandler = true; + }; + testNode.innerHTML = ""; + var viewModel = { myHandler: myHandler, someObj: someObj }; + ko.applyBindings(viewModel, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "mouseover"); + value_of(didCallHandler).should_be(true); + } + }); + + describe('Binding: Click', { + // This is just a special case of the "event" binding, so not necessary to respecify all its behaviours + before_each: prepareTestNode, + after_each: restore, + 'Should invoke the supplied function on click, using model as \'this\' param and first arg, and event as second arg': function () { + var model = { + wasCalled: false, + doCall: function (arg1, arg2) { + this.wasCalled = true; + value_of(arg1).should_be(model); + value_of(arg2.type).should_be("click"); + } + }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.wasCalled).should_be(true); + } + }); + + describe('Binding: CSS class name', { + before_each: prepareTestNode, + after_each: restore, + 'Should give the element the specific CSS class only when the specified value is true': function () { + var observable1 = new ko.observable(); + var observable2 = new ko.observable(true); + testNode.innerHTML = "
Hallo
"; + ko.applyBindings({ someModelProperty: observable1, anotherModelProperty: observable2 }, testNode); + + value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 anotherRule"); + observable1(true); + value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 anotherRule myRule"); + observable2(false); + value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 myRule"); + }, + + 'Should give the element a single CSS class without a leading space when the specified value is true': function () { + var observable1 = new ko.observable(); + testNode.innerHTML = "
Hallo
"; + ko.applyBindings({ someModelProperty: observable1 }, testNode); + + value_of(testNode.childNodes[0].className).should_be(""); + observable1(true); + value_of(testNode.childNodes[0].className).should_be("myRule"); + } + }); + + describe('Binding: CSS style', { + before_each: prepareTestNode, + after_each: restore, + 'Should give the element the specified CSS style value': function () { + var myObservable = new ko.observable("red"); + testNode.innerHTML = "
Hallo
"; + ko.applyBindings({ colorValue: myObservable }, testNode); + + value_of(testNode.childNodes[0].style.backgroundColor).should_be_one_of(["red", "#ff0000"]); // Opera returns style color values in #rrggbb notation, unlike other browsers + myObservable("green"); + value_of(testNode.childNodes[0].style.backgroundColor).should_be_one_of(["green", "#008000"]); + myObservable(undefined); + value_of(testNode.childNodes[0].style.backgroundColor).should_be(""); + } + }); + + describe('Binding: Unique Name', { + before_each: prepareTestNode, + after_each: restore, + 'Should apply a different name to each element': function () { + testNode.innerHTML = "
"; + ko.applyBindings({}, testNode); + + value_of(testNode.childNodes[0].name.length > 0).should_be(true); + value_of(testNode.childNodes[1].name.length > 0).should_be(true); + value_of(testNode.childNodes[0].name == testNode.childNodes[1].name).should_be(false); + }, + + 'Should work without a value': function () { + testNode.innerHTML = "
"; + ko.applyBindings({}, testNode); + + value_of(testNode.childNodes[0].name.length > 0).should_be(true); + value_of(testNode.childNodes[1].name.length > 0).should_be(true); + value_of(testNode.childNodes[0].name == testNode.childNodes[1].name).should_be(false); + } + }); + + describe('Binding: Checked', { + before_each: prepareTestNode, + after_each: restore, + 'Triggering a click should toggle a checkbox\'s checked state before the event handler fires': function () { + // This isn't strictly to do with the checked binding, but if this doesn't work, the rest of the specs aren't meaningful + testNode.innerHTML = ""; + var clickHandlerFireCount = 0, expectedCheckedStateInHandler; + ko.utils.registerEventHandler(testNode.childNodes[0], "click", function () { + clickHandlerFireCount++; + value_of(testNode.childNodes[0].checked).should_be(expectedCheckedStateInHandler); + }) + value_of(testNode.childNodes[0].checked).should_be(false); + expectedCheckedStateInHandler = true; + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(testNode.childNodes[0].checked).should_be(true); + value_of(clickHandlerFireCount).should_be(1); + + expectedCheckedStateInHandler = false; + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(testNode.childNodes[0].checked).should_be(false); + value_of(clickHandlerFireCount).should_be(2); + }, + + 'Should be able to control a checkbox\'s checked state': function () { + var myobservable = new ko.observable(true); + testNode.innerHTML = ""; + + ko.applyBindings({ someProp: myobservable }, testNode); + value_of(testNode.childNodes[0].checked).should_be(true); + + myobservable(false); + value_of(testNode.childNodes[0].checked).should_be(false); + }, + + 'Should update observable properties on the underlying model when the checkbox click event fires': function () { + var myobservable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(myobservable()).should_be(true); + }, + + 'Should only notify observable properties on the underlying model *once* even if the checkbox change events fire multiple times': function () { + var myobservable = new ko.observable(); + var timesNotified = 0; + myobservable.subscribe(function () { timesNotified++ }); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + // Multiple events only cause one notification... + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(timesNotified).should_be(1); + + // ... until the checkbox value actually changes + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(timesNotified).should_be(2); + }, + + 'Should update non-observable properties on the underlying model when the checkbox click event fires': function () { + var model = { someProp: false }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.someProp).should_be(true); + }, + + 'Should update observable properties on the underlying model when the checkbox is clicked': function () { + var myobservable = new ko.observable(false); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(myobservable()).should_be(true); + }, + + 'Should update non-observable properties on the underlying model when the checkbox is clicked': function () { + var model = { someProp: false }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.someProp).should_be(true); + }, + + 'Should make a radio button checked if and only if its value matches the bound model property': function () { + var myobservable = new ko.observable("another value"); + testNode.innerHTML = ""; + + ko.applyBindings({ someProp: myobservable }, testNode); + value_of(testNode.childNodes[0].checked).should_be(false); + + myobservable("This Radio Button Value"); + value_of(testNode.childNodes[0].checked).should_be(true); + }, + + 'Should set an observable model property to this radio button\'s value when checked': function () { + var myobservable = new ko.observable("another value"); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + value_of(myobservable()).should_be("another value"); + testNode.childNodes[0].click(); + value_of(myobservable()).should_be("this radio button value"); + }, + + 'Should only notify observable properties on the underlying model *once* even if the radio button change/click events fire multiple times': function () { + var myobservable = new ko.observable("original value"); + var timesNotified = 0; + myobservable.subscribe(function () { timesNotified++ }); + testNode.innerHTML = ""; + ko.applyBindings({ someProp: myobservable }, testNode); + + // Multiple events only cause one notification... + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + ko.utils.triggerEvent(testNode.childNodes[0], "change"); + value_of(timesNotified).should_be(1); + + // ... until you click something with a different value + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + ko.utils.triggerEvent(testNode.childNodes[1], "change"); + value_of(timesNotified).should_be(2); + }, + + 'Should set a non-observable model property to this radio button\'s value when checked': function () { + var model = { someProp: "another value" }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "click"); + value_of(model.someProp).should_be("this radio button value"); + }, + + 'When a checkbox is bound to an array, the checkbox should control whether its value is in that array': function () { + var model = { myArray: ["Existing value", "Unrelated value"] }; + testNode.innerHTML = "" + + ""; + ko.applyBindings(model, testNode); + + value_of(model.myArray).should_be(["Existing value", "Unrelated value"]); + + // Checkbox initial state is determined by whether the value is in the array + value_of(testNode.childNodes[0].checked).should_be(true); + value_of(testNode.childNodes[1].checked).should_be(false); + // Checking the checkbox puts it in the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + value_of(testNode.childNodes[1].checked).should_be(true); + value_of(model.myArray).should_be(["Existing value", "Unrelated value", "New value"]); + // Unchecking the checkbox removes it from the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + value_of(testNode.childNodes[1].checked).should_be(false); + value_of(model.myArray).should_be(["Existing value", "Unrelated value"]); + }, + + 'When a checkbox is bound to an observable array, the checkbox checked state responds to changes in the array': function () { + var model = { myObservableArray: ko.observableArray(["Unrelated value"]) }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + value_of(testNode.childNodes[0].checked).should_be(false); + + // Put the value in the array; observe the checkbox reflect this + model.myObservableArray.push("My value"); + value_of(testNode.childNodes[0].checked).should_be(true); + + // Remove the value from the array; observe the checkbox reflect this + model.myObservableArray.remove("My value"); + value_of(testNode.childNodes[0].checked).should_be(false); + } + }); + + describe('Binding: Attr', { + before_each: prepareTestNode, + after_each: restore, + 'Should be able to set arbitrary attribute values': function () { + var model = { myValue: "first value" }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].getAttribute("firstAttribute")).should_be("first value"); + value_of(testNode.childNodes[0].getAttribute("second-attribute")).should_be("true"); + }, + + 'Should be able to set arbitrary attribute values (without quotes)': function () { + var model = { myValue: "first value" }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].getAttribute("firstAttribute")).should_be("first value"); + value_of(testNode.childNodes[0].getAttribute("second-attribute")).should_be("true"); + }, + + 'Should respond to changes in an observable value': function () { + var model = { myprop: ko.observable("initial value") }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("initial value"); + + // Change the observable; observe it reflected in the DOM + model.myprop("new value"); + value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("new value"); + }, + + 'Should remove the attribute if the value is strictly false, null, or undefined': function () { + var model = { myprop: ko.observable() }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + ko.utils.arrayForEach([false, null, undefined], function (testValue) { + model.myprop("nonempty value"); + value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be("nonempty value"); + model.myprop(testValue); + value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be(null); + }); + }, + + 'Should be able to set class attribute and access it using className property': function () { + var model = { myprop: ko.observable("newClass") }; + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].className).should_be("oldClass"); + ko.applyBindings(model, testNode); + value_of(testNode.childNodes[0].className).should_be("newClass"); + // Should be able to clear class also + model.myprop(undefined); + value_of(testNode.childNodes[0].className).should_be(""); + value_of(testNode.childNodes[0].getAttribute("class")).should_be(null); + } }); - - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: someItem }, testNode); - - // Initial state is one subscriber, one click handler - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Hello"); - value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(countedClicks).should_be(1); - - // Force "update" binding handler to fire, then check we still have one subscriber... - someItem.valueHasMutated(); - value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); - - // ... and one click handler - countedClicks = 0; - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(countedClicks).should_be(1); - }, - - 'Should be able to access parent binding context via $parent': function() { - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: { }, parentProp: 'Parent prop value' }, testNode); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Parent prop value"); - }, - - 'Should be able to access all parent binding contexts via $parents, and root context via $root': function() { - testNode.innerHTML = "
" + - "
" + - "
" + - "" + - "" + - "" + - "" + - "" + + + describe('Binding: Hasfocus', { + before_each: prepareTestNode, + after_each: restore, + 'Should respond to changes on an observable value by blurring or focusing the element': function () { + var currentState; + var model = { myVal: ko.observable() } + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + ko.utils.registerEventHandler(testNode.childNodes[0], "focusin", function () { currentState = true }); + ko.utils.registerEventHandler(testNode.childNodes[0], "focusout", function () { currentState = false }); + + // When the value becomes true, we focus + model.myVal(true); + value_of(currentState).should_be(true); + + // When the value becomes false, we blur + model.myVal(false); + value_of(currentState).should_be(false); + }, + + 'Should set an observable value to be true on focus and false on blur': function () { + var model = { myVal: ko.observable() } + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + // Need to raise "focusin" and "focusout" manually, because simply calling ".focus()" and ".blur()" + // in IE doesn't reliably trigger the "focus" and "blur" events synchronously + + ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); + value_of(model.myVal()).should_be(true); + + // Move the focus elsewhere + ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); + value_of(model.myVal()).should_be(false); + }, + + 'Should set a non-observable value to be true on focus and false on blur': function () { + var model = { myVal: null } + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + ko.utils.triggerEvent(testNode.childNodes[0], "focusin"); + value_of(model.myVal).should_be(true); + + // Move the focus elsewhere + ko.utils.triggerEvent(testNode.childNodes[0], "focusout"); + value_of(model.myVal).should_be(false); + } + }); + + describe('Binding: If', { + before_each: prepareTestNode, + after_each: restore, + 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + ko.applyBindings({ someItem: null }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should leave descendant nodes in the document (and bind them) if the value is truey, independently of the active template engine': function () { + ko.setTemplateEngine(new ko.templateEngine()); // This template engine will just throw errors if you try to use it + testNode.innerHTML = "
"; + value_of(testNode.childNodes.length).should_be(1); + ko.applyBindings({ someItem: { existentChildProp: 'Child prop value'} }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + }, + + 'Should toggle the presence and bindedness of descendant nodes according to the truthiness of the value': function () { + var someItem = ko.observable(undefined); + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: someItem }, testNode); + + // First it's not there + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + + // Then it's there + someItem({ occasionallyExistentChildProp: 'Child prop value' }); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + + // Then it's gone again + someItem(null); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should not interfere with binding context': function () { + testNode.innerHTML = "
Parents:
"; + ko.applyBindings({}, testNode); + value_of(testNode.childNodes[0]).should_contain_text("Parents: 0"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).should_be(0); + }, + + 'Should be able to define an \"if\" region using a containerless template': function () { + var someitem = ko.observable(undefined); + testNode.innerHTML = "hello goodbye"; + ko.applyBindings({ someitem: someitem }, testNode); + + // First it's not there + value_of(testNode).should_contain_html("hello goodbye"); + + // Then it's there + someitem({ occasionallyexistentchildprop: 'child prop value' }); + value_of(testNode).should_contain_html("hello child prop value goodbye"); + + // Then it's gone again + someitem(null); + value_of(testNode).should_contain_html("hello goodbye"); + }, + + 'Should be able to nest \"if\" regions defined by containerless templates': function () { + var condition1 = ko.observable(false); + var condition2 = ko.observable(false); + testNode.innerHTML = "hello First is trueBoth are true"; + ko.applyBindings({ condition1: condition1, condition2: condition2 }, testNode); + + // First neither are there + value_of(testNode).should_contain_html("hello "); + + // Make outer appear + condition1(true); + value_of(testNode).should_contain_html("hello first is true"); + + // Make inner appear + condition2(true); + value_of(testNode).should_contain_html("hello first is trueboth are true"); + } + }); + + describe('Binding: Ifnot', { + before_each: prepareTestNode, + after_each: restore, + 'Should remove descendant nodes from the document (and not bind them) if the value is truey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + ko.applyBindings({ someItem: null, condition: true }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should leave descendant nodes in the document (and bind them) if the value is falsey, independently of the active template engine': function () { + ko.setTemplateEngine(new ko.templateEngine()); // This template engine will just throw errors if you try to use it + testNode.innerHTML = "
"; + value_of(testNode.childNodes.length).should_be(1); + ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' }, condition: false }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + }, + + 'Should toggle the presence and bindedness of descendant nodes according to the falsiness of the value': function () { + var someItem = ko.observable(undefined); + var condition = ko.observable(true); + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: someItem, condition: condition }, testNode); + + // First it's not there + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + + // Then it's there + someItem({ occasionallyExistentChildProp: 'Child prop value' }); + condition(false); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + + // Then it's gone again + condition(true); + someItem(null); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should not interfere with binding context': function () { + testNode.innerHTML = "
Parents:
"; + ko.applyBindings({}, testNode); + value_of(testNode.childNodes[0]).should_contain_text("Parents: 0"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).should_be(0); + } + }); + + describe('Binding: With Light', { + before_each: prepareTestNode, + after_each: restore, + 'Should leave descendant nodes in the document (and bind them in the context of the supplied value) if the value is truey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes.length).should_be(1); + ko.applyBindings({ someItem: { existentChildProp: 'Child prop value'} }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + }, + + 'Should not bind the same elements more than once even if the supplied value notifies a change': function () { + var countedClicks = 0; + var someItem = ko.observable({ + childProp: ko.observable('Hello'), + handleClick: function () { countedClicks++ } + }); + + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: someItem }, testNode); + + // Initial state is one subscriber, one click handler + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Hello"); + value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(countedClicks).should_be(1); + + // Force "update" binding handler to fire, then check we still have one subscriber... + someItem.valueHasMutated(); + value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); + + // ... and one click handler + countedClicks = 0; + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(countedClicks).should_be(1); + }, + + 'Should be able to access parent binding context via $parent': function () { + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: {}, parentProp: 'Parent prop value' }, testNode); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Parent prop value"); + }, + + 'Should be able to access all parent binding contexts via $parents, and root context via $root': function () { + testNode.innerHTML = "
" + + "
" + + "
" + + "" + + "" + + "" + + "" + + "" + "
" + "
" + "
"; - ko.applyBindings({ - name: 'outer', - topItem: { - name: 'top', - middleItem: { - name: 'middle', - bottomItem: { - name: "bottom" + ko.applyBindings({ + name: 'outer', + topItem: { + name: 'top', + middleItem: { + name: 'middle', + bottomItem: { + name: "bottom" + } + } } - } + }, testNode); + var finalContainer = testNode.childNodes[0].childNodes[0].childNodes[0]; + value_of(finalContainer.childNodes[0]).should_contain_text("bottom"); + value_of(finalContainer.childNodes[1]).should_contain_text("middle"); + value_of(finalContainer.childNodes[2]).should_contain_text("top"); + value_of(finalContainer.childNodes[3]).should_contain_text("outer"); + value_of(finalContainer.childNodes[4]).should_contain_text("outer"); + + // Also check that, when we later retrieve the binding contexts, we get consistent results + value_of(ko.contextFor(testNode).$data.name).should_be("outer"); + value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("outer"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("top"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("middle"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("bottom"); + var firstSpan = testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]; + value_of(firstSpan.tagName).should_be("SPAN"); + value_of(ko.contextFor(firstSpan).$data.name).should_be("bottom"); + value_of(ko.contextFor(firstSpan).$root.name).should_be("outer"); + value_of(ko.contextFor(firstSpan).$parents[1].name).should_be("top"); + }, + + 'Should be able to define a \"withlight\" region using a containerless binding': function () { + var someitem = ko.observable({ someItem: 'first value' }); + testNode.innerHTML = "xxx "; + ko.applyBindings({ someitem: someitem }, testNode); + + value_of(testNode).should_contain_text("xxx first value"); + + someitem({ someItem: 'second value' }); + value_of(testNode).should_contain_text("xxx second value"); + }, + + 'Should be able to use \"withlight\" within an observable top-level view model': function () { + var vm = ko.observable({ someitem: ko.observable({ someItem: 'first value' }) }); + testNode.innerHTML = "xxx "; + ko.applyBindings(vm, testNode); + + value_of(testNode).should_contain_text("xxx first value"); + + vm({ someitem: ko.observable({ someItem: 'second value' }) }); + value_of(testNode).should_contain_text("xxx second value"); + }, + + 'Should be able to nest a containerless template within \"withlight\"': function () { + testNode.innerHTML = "
text" + + "
"; + + var childprop = ko.observableArray([]); + var someitem = ko.observable({ childprop: childprop }); + var viewModel = { someitem: someitem }; + ko.applyBindings(viewModel, testNode); + + // First it's not there (by template) + var container = testNode.childNodes[0]; + value_of(container).should_contain_html("text"); + + // Then it's there + childprop.push('me') + value_of(container).should_contain_html("textme"); + + // Then there's a second one + childprop.push('me2') + value_of(container).should_contain_html("textmeme2"); + + // Then it changes + someitem({ childprop: ['notme'] }); + container = testNode.childNodes[0]; + value_of(container).should_contain_html("textnotme"); } - }, testNode); - var finalContainer = testNode.childNodes[0].childNodes[0].childNodes[0]; - value_of(finalContainer.childNodes[0]).should_contain_text("bottom"); - value_of(finalContainer.childNodes[1]).should_contain_text("middle"); - value_of(finalContainer.childNodes[2]).should_contain_text("top"); - value_of(finalContainer.childNodes[3]).should_contain_text("outer"); - value_of(finalContainer.childNodes[4]).should_contain_text("outer"); - - // Also check that, when we later retrieve the binding contexts, we get consistent results - value_of(ko.contextFor(testNode).$data.name).should_be("outer"); - value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("outer"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("top"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("middle"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("bottom"); - var firstSpan = testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]; - value_of(firstSpan.tagName).should_be("SPAN"); - value_of(ko.contextFor(firstSpan).$data.name).should_be("bottom"); - value_of(ko.contextFor(firstSpan).$root.name).should_be("outer"); - value_of(ko.contextFor(firstSpan).$parents[1].name).should_be("top"); - }, - - 'Should be able to define a \"withlight\" region using a containerless binding': function() { - var someitem = ko.observable({someItem: 'first value'}); - testNode.innerHTML = "xxx "; - ko.applyBindings({ someitem: someitem }, testNode); - - value_of(testNode).should_contain_text("xxx first value"); - - someitem({ someItem: 'second value' }); - value_of(testNode).should_contain_text("xxx second value"); - }, - - 'Should be able to use \"withlight\" within an observable top-level view model': function() { - var vm = ko.observable({someitem: ko.observable({someItem: 'first value'})}); - testNode.innerHTML = "xxx "; - ko.applyBindings(vm, testNode); - - value_of(testNode).should_contain_text("xxx first value"); - - vm({someitem: ko.observable({ someItem: 'second value' })}); - value_of(testNode).should_contain_text("xxx second value"); - }, - - 'Should be able to nest a containerless template within \"withlight\"': function() { - testNode.innerHTML = "
text" + - "
"; - - var childprop = ko.observableArray([]); - var someitem = ko.observable({childprop: childprop}); - var viewModel = {someitem: someitem}; - ko.applyBindings(viewModel, testNode); - - // First it's not there (by template) - var container = testNode.childNodes[0]; - value_of(container).should_contain_html("text"); - - // Then it's there - childprop.push('me') - value_of(container).should_contain_html("textme"); - - // Then there's a second one - childprop.push('me2') - value_of(container).should_contain_html("textmeme2"); - - // Then it changes - someitem({childprop: ['notme']}); - container = testNode.childNodes[0]; - value_of(container).should_contain_html("textnotme"); - } -}); - -describe('Binding: With', { - before_each: prepareTestNode, - - 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - ko.applyBindings({ someItem: null }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should leave descendant nodes in the document (and bind them in the context of the supplied value) if the value is truey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes.length).should_be(1); - ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' } }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - }, - - 'Should toggle the presence and bindedness of descendant nodes according to the truthiness of the value, performing binding in the context of the value': function() { - var someItem = ko.observable(undefined); - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: someItem }, testNode); - - // First it's not there - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - - // Then it's there - someItem({ occasionallyExistentChildProp: 'Child prop value' }); - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); - - // Then it's gone again - someItem(null); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should not bind the same elements more than once even if the supplied value notifies a change': function() { - var countedClicks = 0; - var someItemContents = { - childProp: ko.observable('Hello'), - handleClick: function() { countedClicks++ } - }; - var someItem = ko.observable(someItemContents); - - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: someItem }, testNode); - - // Initial state is one subscriber, one click handler - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Hello"); - value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(countedClicks).should_be(1); - - // Force "update" binding handler to fire, then check we still have one subscriber... - someItem.valueHasMutated(); - value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); - - // ... and one click handler - countedClicks = 0; - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(countedClicks).should_be(1); - - // Clear and restore data, then check we still have one subscriber... - someItem(null); - someItem(someItemContents); - value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); - - // ... and one click handler - countedClicks = 0; - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); - value_of(countedClicks).should_be(1); - }, - - 'Should be able to access parent binding context via $parent': function() { - testNode.innerHTML = "
"; - ko.applyBindings({ someItem: { }, parentProp: 'Parent prop value' }, testNode); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Parent prop value"); - }, - - 'Should be able to access all parent binding contexts via $parents, and root context via $root': function() { - testNode.innerHTML = "
" + - "
" + - "
" + - "" + - "" + - "" + - "" + - "" + + }); + + describe('Binding: With', { + before_each: prepareTestNode, + after_each: restore, + 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + ko.applyBindings({ someItem: null }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should leave descendant nodes in the document (and bind them in the context of the supplied value) if the value is truey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes.length).should_be(1); + ko.applyBindings({ someItem: { existentChildProp: 'Child prop value'} }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + }, + + 'Should toggle the presence and bindedness of descendant nodes according to the truthiness of the value, performing binding in the context of the value': function () { + var someItem = ko.observable(undefined); + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: someItem }, testNode); + + // First it's not there + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + + // Then it's there + someItem({ occasionallyExistentChildProp: 'Child prop value' }); + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Child prop value"); + + // Then it's gone again + someItem(null); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should not bind the same elements more than once even if the supplied value notifies a change': function () { + var countedClicks = 0; + var someItemContents = { + childProp: ko.observable('Hello'), + handleClick: function () { countedClicks++ } + }; + var someItem = ko.observable(someItemContents); + + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: someItem }, testNode); + + // Initial state is one subscriber, one click handler + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Hello"); + value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(countedClicks).should_be(1); + + // Force "update" binding handler to fire, then check we still have one subscriber... + someItem.valueHasMutated(); + value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); + + // ... and one click handler + countedClicks = 0; + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(countedClicks).should_be(1); + + // Clear and restore data, then check we still have one subscriber... + someItem(null); + someItem(someItemContents); + value_of(someItem().childProp.getSubscriptionsCount()).should_be(1); + + // ... and one click handler + countedClicks = 0; + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + value_of(countedClicks).should_be(1); + }, + + 'Should be able to access parent binding context via $parent': function () { + testNode.innerHTML = "
"; + ko.applyBindings({ someItem: {}, parentProp: 'Parent prop value' }, testNode); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Parent prop value"); + }, + + 'Should be able to access all parent binding contexts via $parents, and root context via $root': function () { + testNode.innerHTML = "
" + + "
" + + "
" + + "" + + "" + + "" + + "" + + "" + "
" + "
" + "
"; - ko.applyBindings({ - name: 'outer', - topItem: { - name: 'top', - middleItem: { - name: 'middle', - bottomItem: { - name: "bottom" + ko.applyBindings({ + name: 'outer', + topItem: { + name: 'top', + middleItem: { + name: 'middle', + bottomItem: { + name: "bottom" + } + } } - } + }, testNode); + var finalContainer = testNode.childNodes[0].childNodes[0].childNodes[0]; + value_of(finalContainer.childNodes[0]).should_contain_text("bottom"); + value_of(finalContainer.childNodes[1]).should_contain_text("middle"); + value_of(finalContainer.childNodes[2]).should_contain_text("top"); + value_of(finalContainer.childNodes[3]).should_contain_text("outer"); + value_of(finalContainer.childNodes[4]).should_contain_text("outer"); + + // Also check that, when we later retrieve the binding contexts, we get consistent results + value_of(ko.contextFor(testNode).$data.name).should_be("outer"); + value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("outer"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("top"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("middle"); + value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("bottom"); + var firstSpan = testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]; + value_of(firstSpan.tagName).should_be("SPAN"); + value_of(ko.contextFor(firstSpan).$data.name).should_be("bottom"); + value_of(ko.contextFor(firstSpan).$root.name).should_be("outer"); + value_of(ko.contextFor(firstSpan).$parents[1].name).should_be("top"); + }, + + 'Should be able to define a \"with\" region using a containerless binding': function () { + var someitem = ko.observable(undefined); + testNode.innerHTML = "hello goodbye"; + ko.applyBindings({ someitem: someitem }, testNode); + + // First it's not there + value_of(testNode).should_contain_html("hello goodbye"); + + // Then it's there + someitem({ occasionallyexistentchildprop: 'child prop value' }); + value_of(testNode).should_contain_html("hello child prop value goodbye"); + + // Then it's gone again + someitem(null); + value_of(testNode).should_contain_html("hello goodbye"); + }, + + 'Should be able to nest \"with\" regions defined by containerless templates': function () { + testNode.innerHTML = "hello " + + "Got top: " + + "" + + "Got child: " + + "" + + ""; + var viewModel = { topitem: ko.observable(null) }; + ko.applyBindings(viewModel, testNode); + + // First neither are there + value_of(testNode).should_contain_html("hello "); + + // Make top appear + viewModel.topitem({ topprop: 'property of top', childitem: ko.observable() }); + value_of(testNode).should_contain_html("hello got top: property of top"); + + // Make child appear + viewModel.topitem().childitem({ childprop: 'property of child' }); + value_of(testNode).should_contain_html("hello got top: property of topgot child: property of child"); + + // Make top disappear + viewModel.topitem(null); + value_of(testNode).should_contain_html("hello "); + }, + + 'Should be able to nest a containerless template within \"with\"': function () { + testNode.innerHTML = "
text" + + "
"; + + var childprop = ko.observableArray([]); + var someitem = ko.observable({ childprop: childprop }); + var viewModel = { someitem: someitem }; + ko.applyBindings(viewModel, testNode); + + // First it's not there (by template) + var container = testNode.childNodes[0]; + value_of(container).should_contain_html("text"); + + // Then it's there + childprop.push('me') + value_of(container).should_contain_html("textme"); + + // Then there's a second one + childprop.push('me2') + value_of(container).should_contain_html("textmeme2"); + + // Then it's not there (by with) + someitem(null); + value_of(testNode).should_contain_html('
'); + + // Then it changes + someitem({ childprop: ['notme'] }); + container = testNode.childNodes[0]; + value_of(container).should_contain_html("textnotme"); } - }, testNode); - var finalContainer = testNode.childNodes[0].childNodes[0].childNodes[0]; - value_of(finalContainer.childNodes[0]).should_contain_text("bottom"); - value_of(finalContainer.childNodes[1]).should_contain_text("middle"); - value_of(finalContainer.childNodes[2]).should_contain_text("top"); - value_of(finalContainer.childNodes[3]).should_contain_text("outer"); - value_of(finalContainer.childNodes[4]).should_contain_text("outer"); - - // Also check that, when we later retrieve the binding contexts, we get consistent results - value_of(ko.contextFor(testNode).$data.name).should_be("outer"); - value_of(ko.contextFor(testNode.childNodes[0]).$data.name).should_be("outer"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).should_be("top"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("middle"); - value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]).$data.name).should_be("bottom"); - var firstSpan = testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]; - value_of(firstSpan.tagName).should_be("SPAN"); - value_of(ko.contextFor(firstSpan).$data.name).should_be("bottom"); - value_of(ko.contextFor(firstSpan).$root.name).should_be("outer"); - value_of(ko.contextFor(firstSpan).$parents[1].name).should_be("top"); - }, - - 'Should be able to define a \"with\" region using a containerless binding': function() { - var someitem = ko.observable(undefined); - testNode.innerHTML = "hello goodbye"; - ko.applyBindings({ someitem: someitem }, testNode); - - // First it's not there - value_of(testNode).should_contain_html("hello goodbye"); - - // Then it's there - someitem({ occasionallyexistentchildprop: 'child prop value' }); - value_of(testNode).should_contain_html("hello child prop value goodbye"); - - // Then it's gone again - someitem(null); - value_of(testNode).should_contain_html("hello goodbye"); - }, - - 'Should be able to nest \"with\" regions defined by containerless templates': function() { - testNode.innerHTML = "hello " - + "Got top: " - + "" - + "Got child: " - + "" - + ""; - var viewModel = { topitem: ko.observable(null) }; - ko.applyBindings(viewModel, testNode); - - // First neither are there - value_of(testNode).should_contain_html("hello "); - - // Make top appear - viewModel.topitem({ topprop: 'property of top', childitem: ko.observable() }); - value_of(testNode).should_contain_html("hello got top: property of top"); - - // Make child appear - viewModel.topitem().childitem({ childprop: 'property of child' }); - value_of(testNode).should_contain_html("hello got top: property of topgot child: property of child"); - - // Make top disappear - viewModel.topitem(null); - value_of(testNode).should_contain_html("hello "); - }, - - 'Should be able to nest a containerless template within \"with\"': function() { - testNode.innerHTML = "
text" + - "
"; - - var childprop = ko.observableArray([]); - var someitem = ko.observable({childprop: childprop}); - var viewModel = {someitem: someitem}; - ko.applyBindings(viewModel, testNode); - - // First it's not there (by template) - var container = testNode.childNodes[0]; - value_of(container).should_contain_html("text"); - - // Then it's there - childprop.push('me') - value_of(container).should_contain_html("textme"); - - // Then there's a second one - childprop.push('me2') - value_of(container).should_contain_html("textmeme2"); - - // Then it's not there (by with) - someitem(null); - value_of(testNode).should_contain_html('
'); - - // Then it changes - someitem({childprop: ['notme']}); - container = testNode.childNodes[0]; - value_of(container).should_contain_html("textnotme"); - } -}); - -describe('Binding: Foreach', { - before_each: prepareTestNode, - - 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - ko.applyBindings({ someItem: null }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should remove descendant nodes from the document (and not bind them) if the value is undefined': function() { - testNode.innerHTML = "
"; - value_of(testNode.childNodes[0].childNodes.length).should_be(1); - ko.applyBindings({ someItem: undefined }, testNode); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - 'Should duplicate descendant nodes for each value in the array value (and bind them in the context of that supplied value)': function() { - testNode.innerHTML = "
"; - var someItems = [ + }); + + describe('Binding: Foreach', { + before_each: prepareTestNode, + after_each: restore, + 'Should remove descendant nodes from the document (and not bind them) if the value is falsey': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + ko.applyBindings({ someItem: null }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should remove descendant nodes from the document (and not bind them) if the value is undefined': function () { + testNode.innerHTML = "
"; + value_of(testNode.childNodes[0].childNodes.length).should_be(1); + ko.applyBindings({ someItem: undefined }, testNode); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + 'Should duplicate descendant nodes for each value in the array value (and bind them in the context of that supplied value)': function () { + testNode.innerHTML = "
"; + var someItems = [ { childProp: 'first child' }, { childProp: 'second child' } ]; - ko.applyBindings({ someItems: someItems }, testNode); - value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); - }, - - 'Should be able to use $data to reference each array item being bound': function() { - testNode.innerHTML = "
"; - var someItems = ['alpha', 'beta']; - ko.applyBindings({ someItems: someItems }, testNode); - value_of(testNode.childNodes[0]).should_contain_html('alphabeta'); - }, - - - 'Should add and remove nodes to match changes in the bound array': function() { - testNode.innerHTML = "
"; - var someItems = ko.observableArray([ + ko.applyBindings({ someItems: someItems }, testNode); + value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); + }, + + 'Should be able to use $data to reference each array item being bound': function () { + testNode.innerHTML = "
"; + var someItems = ['alpha', 'beta']; + ko.applyBindings({ someItems: someItems }, testNode); + value_of(testNode.childNodes[0]).should_contain_html('alphabeta'); + }, + + + 'Should add and remove nodes to match changes in the bound array': function () { + testNode.innerHTML = "
"; + var someItems = ko.observableArray([ { childProp: 'first child' }, { childProp: 'second child' } ]); - ko.applyBindings({ someItems: someItems }, testNode); - value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); - - // Add items at the beginning... - someItems.unshift({ childProp: 'zeroth child' }); - value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childsecond child'); - - // ... middle - someItems.splice(2, 0, { childProp: 'middle child' }); - value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childmiddle childsecond child'); - - // ... and end - someItems.push({ childProp: 'last child' }); - value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childmiddle childsecond childlast child'); - - // Also remove from beginning... - someItems.shift(); - value_of(testNode.childNodes[0]).should_contain_html('first childmiddle childsecond childlast child'); - - // ... and middle - someItems.splice(1, 1); - value_of(testNode.childNodes[0]).should_contain_html('first childsecond childlast child'); - - // ... and end - someItems.pop(); - value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); - - // Also, marking as "destroy" should eliminate the item from display - someItems.destroy(someItems()[0]); - value_of(testNode.childNodes[0]).should_contain_html('second child'); - }, - - 'Should remove all nodes corresponding to a removed array item, even if they were generated via containerless templates': function() { - // Represents issue https://github.com/SteveSanderson/knockout/issues/185 - testNode.innerHTML = "
ab
"; - var someitems = ko.observableArray([1,2]); - ko.applyBindings({ someitems: someitems }, testNode); - value_of(testNode).should_contain_html('
abab
'); - - // Now remove items, and check the corresponding child nodes vanished - someitems.splice(1, 1); - value_of(testNode).should_contain_html('
ab
'); - }, - - 'Should update all nodes corresponding to a changed array item, even if they were generated via containerless templates': function() { - testNode.innerHTML = "
"; - var someitems = [ ko.observable('A'), ko.observable('B') ]; - ko.applyBindings({ someitems: someitems }, testNode); - value_of(testNode).should_contain_text('AB'); - - // Now update an item - someitems[0]('A2'); - value_of(testNode).should_contain_text('A2B'); - }, - - 'Should be able to supply show "_destroy"ed items via includeDestroyed option': function() { - testNode.innerHTML = "
"; - var someItems = ko.observableArray([ + ko.applyBindings({ someItems: someItems }, testNode); + value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); + + // Add items at the beginning... + someItems.unshift({ childProp: 'zeroth child' }); + value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childsecond child'); + + // ... middle + someItems.splice(2, 0, { childProp: 'middle child' }); + value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childmiddle childsecond child'); + + // ... and end + someItems.push({ childProp: 'last child' }); + value_of(testNode.childNodes[0]).should_contain_html('zeroth childfirst childmiddle childsecond childlast child'); + + // Also remove from beginning... + someItems.shift(); + value_of(testNode.childNodes[0]).should_contain_html('first childmiddle childsecond childlast child'); + + // ... and middle + someItems.splice(1, 1); + value_of(testNode.childNodes[0]).should_contain_html('first childsecond childlast child'); + + // ... and end + someItems.pop(); + value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); + + // Also, marking as "destroy" should eliminate the item from display + someItems.destroy(someItems()[0]); + value_of(testNode.childNodes[0]).should_contain_html('second child'); + }, + + 'Should remove all nodes corresponding to a removed array item, even if they were generated via containerless templates': function () { + // Represents issue https://github.com/SteveSanderson/knockout/issues/185 + testNode.innerHTML = "
ab
"; + var someitems = ko.observableArray([1, 2]); + ko.applyBindings({ someitems: someitems }, testNode); + value_of(testNode).should_contain_html('
abab
'); + + // Now remove items, and check the corresponding child nodes vanished + someitems.splice(1, 1); + value_of(testNode).should_contain_html('
ab
'); + }, + + 'Should update all nodes corresponding to a changed array item, even if they were generated via containerless templates': function () { + testNode.innerHTML = "
'; + var someitems = [ko.observable('A'), ko.observable('B')]; + ko.applyBindings({ someitems: someitems }, testNode); + value_of(testNode).should_contain_text('AB'); + + // Now update an item + someitems[0]('A2'); + value_of(testNode).should_contain_text('A2B'); + }, + + 'Should be able to supply show "_destroy"ed items via includeDestroyed option': function () { + testNode.innerHTML = "
"; + var someItems = ko.observableArray([ { childProp: 'first child' }, { childProp: 'second child', _destroy: true } ]); - ko.applyBindings({ someItems: someItems }, testNode); - value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); - }, - - 'Should be able to supply afterAdd and beforeRemove callbacks': function() { - testNode.innerHTML = "
"; - var someItems = ko.observableArray([{ childprop: 'first child' }]); - var afterAddCallbackData = [], beforeRemoveCallbackData = []; - ko.applyBindings({ - someItems: someItems, - myAfterAdd: function(elem, index, value) { afterAddCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) }, - myBeforeRemove: function(elem, index, value) { beforeRemoveCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) } - }, testNode); - - value_of(testNode.childNodes[0]).should_contain_html('first child'); - - // Try adding - someItems.push({ childprop: 'added child'}); - value_of(testNode.childNodes[0]).should_contain_html('first childadded child'); - value_of(afterAddCallbackData.length).should_be(1); - value_of(afterAddCallbackData[0].elem).should_be(testNode.childNodes[0].childNodes[1]); - value_of(afterAddCallbackData[0].index).should_be(1); - value_of(afterAddCallbackData[0].value.childprop).should_be("added child"); - value_of(afterAddCallbackData[0].currentParentClone).should_contain_html('first childadded child'); - - // Try removing - someItems.shift(); - value_of(beforeRemoveCallbackData.length).should_be(1); - value_of(beforeRemoveCallbackData[0].elem).should_contain_text("first child"); - value_of(beforeRemoveCallbackData[0].index).should_be(0); - value_of(beforeRemoveCallbackData[0].value.childprop).should_be("first child"); - // Note that when using "beforeRemove", we *don't* remove the node from the doc - it's up to the beforeRemove callback to do it. So, check it's still there. - value_of(beforeRemoveCallbackData[0].currentParentClone).should_contain_html('first childadded child'); - value_of(testNode.childNodes[0]).should_contain_html('first childadded child'); - }, - - 'Should be able to nest foreaches and access binding contexts both during and after binding': function() { - testNode.innerHTML = "
" - + "
" - + "(Val: , Parents: , Rootval: )" + ko.applyBindings({ someItems: someItems }, testNode); + value_of(testNode.childNodes[0]).should_contain_html('first childsecond child'); + }, + + 'Should be able to supply afterAdd and beforeRemove callbacks': function () { + testNode.innerHTML = "
"; + var someItems = ko.observableArray([{ childprop: 'first child'}]); + var afterAddCallbackData = [], beforeRemoveCallbackData = []; + ko.applyBindings({ + someItems: someItems, + myAfterAdd: function (elem, index, value) { afterAddCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) }, + myBeforeRemove: function (elem, index, value) { beforeRemoveCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) } + }, testNode); + + value_of(testNode.childNodes[0]).should_contain_html('first child'); + + // Try adding + someItems.push({ childprop: 'added child' }); + value_of(testNode.childNodes[0]).should_contain_html('first childadded child'); + value_of(afterAddCallbackData.length).should_be(1); + value_of(afterAddCallbackData[0].elem).should_be(testNode.childNodes[0].childNodes[1]); + value_of(afterAddCallbackData[0].index).should_be(1); + value_of(afterAddCallbackData[0].value.childprop).should_be("added child"); + value_of(afterAddCallbackData[0].currentParentClone).should_contain_html('first childadded child'); + + // Try removing + someItems.shift(); + value_of(beforeRemoveCallbackData.length).should_be(1); + value_of(beforeRemoveCallbackData[0].elem).should_contain_text("first child"); + value_of(beforeRemoveCallbackData[0].index).should_be(0); + value_of(beforeRemoveCallbackData[0].value.childprop).should_be("first child"); + // Note that when using "beforeRemove", we *don't* remove the node from the doc - it's up to the beforeRemove callback to do it. So, check it's still there. + value_of(beforeRemoveCallbackData[0].currentParentClone).should_contain_html('first childadded child'); + value_of(testNode.childNodes[0]).should_contain_html('first childadded child'); + }, + + 'Should be able to nest foreaches and access binding contexts both during and after binding': function () { + testNode.innerHTML = "
" + + "
" + + "(Val: , Parents: , Rootval: )" + "
" - + "
"; - var viewModel = { - rootVal: 'ROOTVAL', - items: ko.observableArray([ + + "
"; + var viewModel = { + rootVal: 'ROOTVAL', + items: ko.observableArray([ { children: ko.observableArray(['A1', 'A2', 'A3']) }, { children: ko.observableArray(['B1', 'B2']) } ]) - }; - ko.applyBindings(viewModel, testNode); - - // Verify we can access binding contexts during binding - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("(Val: A1, Parents: 2, Rootval: ROOTVAL)(Val: A2, Parents: 2, Rootval: ROOTVAL)(Val: A3, Parents: 2, Rootval: ROOTVAL)"); - value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("(Val: B1, Parents: 2, Rootval: ROOTVAL)(Val: B2, Parents: 2, Rootval: ROOTVAL)"); - - // Verify we can access them later - var firstInnerTextNode = testNode.childNodes[0].childNodes[0].childNodes[1]; - value_of(firstInnerTextNode.nodeType).should_be(1); // The first span associated with A1 - value_of(ko.dataFor(firstInnerTextNode)).should_be("A1"); - value_of(ko.contextFor(firstInnerTextNode).$parent.children()[2]).should_be("A3"); - value_of(ko.contextFor(firstInnerTextNode).$parents[1].items()[1].children()[1]).should_be("B2"); - value_of(ko.contextFor(firstInnerTextNode).$root.rootVal).should_be("ROOTVAL"); - }, - - 'Should be able to define a \'foreach\' region using a containerless template': function() { - testNode.innerHTML = "hi "; - var someitems = [ + }; + ko.applyBindings(viewModel, testNode); + + // Verify we can access binding contexts during binding + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("(Val: A1, Parents: 2, Rootval: ROOTVAL)(Val: A2, Parents: 2, Rootval: ROOTVAL)(Val: A3, Parents: 2, Rootval: ROOTVAL)"); + value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("(Val: B1, Parents: 2, Rootval: ROOTVAL)(Val: B2, Parents: 2, Rootval: ROOTVAL)"); + + // Verify we can access them later + var firstInnerTextNode = testNode.childNodes[0].childNodes[0].childNodes[1]; + value_of(firstInnerTextNode.nodeType).should_be(1); // The first span associated with A1 + value_of(ko.dataFor(firstInnerTextNode)).should_be("A1"); + value_of(ko.contextFor(firstInnerTextNode).$parent.children()[2]).should_be("A3"); + value_of(ko.contextFor(firstInnerTextNode).$parents[1].items()[1].children()[1]).should_be("B2"); + value_of(ko.contextFor(firstInnerTextNode).$root.rootVal).should_be("ROOTVAL"); + }, + + 'Should be able to define a \'foreach\' region using a containerless template': function () { + testNode.innerHTML = "hi "; + var someitems = [ { childprop: 'first child' }, { childprop: 'second child' } ]; - ko.applyBindings({ someitems: someitems }, testNode); - value_of(testNode).should_contain_html('hi first childsecond child'); - - // Check we can recover the binding contexts - value_of(ko.dataFor(testNode.childNodes[3]).childprop).should_be("second child"); - value_of(ko.contextFor(testNode.childNodes[3]).$parent.someitems.length).should_be(2); - }, - - 'Should be able to nest \'foreach\' regions defined using containerless templates' : function() { - var innerContents = document.createElement("DIV"); - testNode.innerHTML = ""; - testNode.appendChild(document.createComment("ko foreach: items")); - testNode.appendChild(document.createComment( "ko foreach: children")); - innerContents.innerHTML = "(Val: , Parents: , Rootval: )"; - while (innerContents.firstChild) - testNode.appendChild(innerContents.firstChild); - testNode.appendChild(document.createComment( "/ko")); - testNode.appendChild(document.createComment("/ko")); - - var viewModel = { - rootVal: 'ROOTVAL', - items: ko.observableArray([ + ko.applyBindings({ someitems: someitems }, testNode); + value_of(testNode).should_contain_html('hi first childsecond child'); + + // Check we can recover the binding contexts + value_of(ko.dataFor(testNode.childNodes[3]).childprop).should_be("second child"); + value_of(ko.contextFor(testNode.childNodes[3]).$parent.someitems.length).should_be(2); + }, + + 'Should be able to nest \'foreach\' regions defined using containerless templates': function () { + var innerContents = document.createElement("DIV"); + testNode.innerHTML = ""; + testNode.appendChild(document.createComment(virtualElementTag + " foreach: items")); + testNode.appendChild(document.createComment(virtualElementTag + " foreach: children")); + innerContents.innerHTML = "(Val: , Parents: , Rootval: )"; + while (innerContents.firstChild) + testNode.appendChild(innerContents.firstChild); + testNode.appendChild(document.createComment("/" + virtualElementTag)); + testNode.appendChild(document.createComment("/" + virtualElementTag)); + + var viewModel = { + rootVal: 'ROOTVAL', + items: ko.observableArray([ { children: ko.observableArray(['A1', 'A2', 'A3']) }, { children: ko.observableArray(['B1', 'B2']) } ]) - }; - ko.applyBindings(viewModel, testNode); - - // Verify we can access binding contexts during binding - value_of(testNode).should_contain_text("(Val: A1, Parents: 2, Rootval: ROOTVAL)(Val: A2, Parents: 2, Rootval: ROOTVAL)(Val: A3, Parents: 2, Rootval: ROOTVAL)(Val: B1, Parents: 2, Rootval: ROOTVAL)(Val: B2, Parents: 2, Rootval: ROOTVAL)"); - - // Verify we can access them later - var firstInnerSpan = testNode.childNodes[3]; - value_of(firstInnerSpan).should_contain_text("A1"); // It is the first span bound in the context of A1 - value_of(ko.dataFor(firstInnerSpan)).should_be("A1"); - value_of(ko.contextFor(firstInnerSpan).$parent.children()[2]).should_be("A3"); - value_of(ko.contextFor(firstInnerSpan).$parents[1].items()[1].children()[1]).should_be("B2"); - value_of(ko.contextFor(firstInnerSpan).$root.rootVal).should_be("ROOTVAL"); - }, - - 'Should be able to nest \'if\' inside \'foreach\' defined using containerless templates' : function() { - testNode.innerHTML = "
    "; - testNode.childNodes[0].appendChild(document.createComment("ko foreach: items")); - testNode.childNodes[0].appendChild(document.createElement("li")); - testNode.childNodes[0].childNodes[1].innerHTML = ""; - testNode.childNodes[0].childNodes[1].insertBefore(document.createComment("ko if: childval"), testNode.childNodes[0].childNodes[1].firstChild); - testNode.childNodes[0].childNodes[1].appendChild(document.createComment("/ko")); - testNode.childNodes[0].appendChild(document.createComment("/ko")); - - var viewModel = { - items: [ - { childval: {childprop: 123 } }, + }; + ko.applyBindings(viewModel, testNode); + + // Verify we can access binding contexts during binding + value_of(testNode).should_contain_text("(Val: A1, Parents: 2, Rootval: ROOTVAL)(Val: A2, Parents: 2, Rootval: ROOTVAL)(Val: A3, Parents: 2, Rootval: ROOTVAL)(Val: B1, Parents: 2, Rootval: ROOTVAL)(Val: B2, Parents: 2, Rootval: ROOTVAL)"); + + // Verify we can access them later + var firstInnerSpan = testNode.childNodes[3]; + value_of(firstInnerSpan).should_contain_text("A1"); // It is the first span bound in the context of A1 + value_of(ko.dataFor(firstInnerSpan)).should_be("A1"); + value_of(ko.contextFor(firstInnerSpan).$parent.children()[2]).should_be("A3"); + value_of(ko.contextFor(firstInnerSpan).$parents[1].items()[1].children()[1]).should_be("B2"); + value_of(ko.contextFor(firstInnerSpan).$root.rootVal).should_be("ROOTVAL"); + }, + + 'Should be able to nest \'if\' inside \'foreach\' defined using containerless templates': function () { + testNode.innerHTML = "
      "; + testNode.childNodes[0].appendChild(document.createComment(virtualElementTag + " foreach: items")); + testNode.childNodes[0].appendChild(document.createElement("li")); + testNode.childNodes[0].childNodes[1].innerHTML = ""; + testNode.childNodes[0].childNodes[1].insertBefore(document.createComment(virtualElementTag + " if: childval"), testNode.childNodes[0].childNodes[1].firstChild); + testNode.childNodes[0].childNodes[1].appendChild(document.createComment("/" + virtualElementTag)); + testNode.childNodes[0].appendChild(document.createComment("/" + virtualElementTag)); + + var viewModel = { + items: [ + { childval: { childprop: 123} }, { childval: null }, - { childval: {childprop: 456 } } + { childval: { childprop: 456} } ] - }; - ko.applyBindings(viewModel, testNode); + }; + ko.applyBindings(viewModel, testNode); - value_of(testNode).should_contain_html('
        ' - + '' + value_of(testNode).should_contain_html('
          ' + + '' + '
        • ' - + '' - + '123' - + '' + + '' + + '123' + + '' + '
        • ' + '
        • ' - + '' - + '' - + '
        • ' + + '' + + '' + + '' + '
        • ' - + '' - + '456' - + '' + + '' + + '456' + + '' + '
        • ' - + '' + + '' + '
        '); - }, - - 'Should be able to use containerless templates directly inside UL elements even when closing LI tags are omitted' : function() { - // Represents issue https://github.com/SteveSanderson/knockout/issues/155 - // Certain closing tags, including are optional (http://www.w3.org/TR/html5/syntax.html#syntax-tag-omission) - // Most browsers respect your positioning of closing tags, but IE <= 7 doesn't, and treats your markup - // as if it was written as below: - - // Your actual markup: "
        • Header item
        "; - // How IE <= 8 treats it: - testNode.innerHTML = "
        • Header item
        "; - var viewModel = { - someitems: [ 'Alpha', 'Beta' ] - }; - ko.applyBindings(viewModel, testNode); - - // Either of the following two results are acceptable. - try { - // Modern browsers implicitly re-add the closing tags - value_of(testNode).should_contain_html('
        • header item
        • alpha
        • beta
        '); - } catch(ex) { - // ... but IE < 8 doesn't add ones that immediately precede a
      • - value_of(testNode).should_contain_html('
        • header item
        • alpha
        • beta
        '); - } - }, - - 'Should be able to nest containerless templates directly inside UL elements, even on IE < 8 with its bizarre HTML parsing/formatting' : function() { - // Represents https://github.com/SteveSanderson/knockout/issues/212 - // This test starts with the following DOM structure: - //
          - // - // - //
        • - // - // - //
        • - //
        - // Note that: - // 1. The closing comments are inside the
      • to simulate IE<8's weird parsing - // 2. We have to build this with manual DOM operations, otherwise IE<8 will deform it in a different weird way - // It would be a more authentic test if we could set up the scenario using .innerHTML and then let the browser do whatever parsing it does normally, - // but unfortunately IE varies its weirdness according to whether it's really parsing an HTML doc, or whether you're using .innerHTML. - - testNode.innerHTML = ""; - testNode.appendChild(document.createElement("ul")); - testNode.firstChild.appendChild(document.createComment("ko foreach: ['A', 'B']")); - testNode.firstChild.appendChild(document.createComment("ko if: $data == 'B'")); - testNode.firstChild.appendChild(document.createElement("li")); - testNode.firstChild.lastChild.setAttribute("data-bind", "text: $data"); - testNode.firstChild.lastChild.appendChild(document.createComment("/ko")); - testNode.firstChild.lastChild.appendChild(document.createComment("/ko")); - - ko.applyBindings(null, testNode); - value_of(testNode).should_contain_text("B"); - }, - - 'Should be able to output HTML5 elements (even on IE<9, as long as you reference either innershiv.js or jQuery1.7+Modernizr)': function() { - // Represents https://github.com/SteveSanderson/knockout/issues/194 - ko.utils.setHtml(testNode, "
        "); - var viewModel = { - someitems: [ 'Alpha', 'Beta' ] - }; - ko.applyBindings(viewModel, testNode); - value_of(testNode).should_contain_html('
        alpha
        beta
        '); - }, - - 'Should be able to output HTML5 elements within container-less templates (same as above)': function() { - // Represents https://github.com/SteveSanderson/knockout/issues/194 - ko.utils.setHtml(testNode, "
        "); - var viewModel = { - someitems: [ 'Alpha', 'Beta' ] - }; - ko.applyBindings(viewModel, testNode); - value_of(testNode).should_contain_html('
        alpha
        beta
        '); + }, + + 'Should be able to use containerless templates directly inside UL elements even when closing LI tags are omitted': function () { + // Represents issue https://github.com/SteveSanderson/knockout/issues/155 + // Certain closing tags, including
      • are optional (http://www.w3.org/TR/html5/syntax.html#syntax-tag-omission) + // Most browsers respect your positioning of closing tags, but IE <= 7 doesn't, and treats your markup + // as if it was written as below: + + // Your actual markup: "
        • Header item
        "; + // How IE <= 8 treats it: + testNode.innerHTML = "
        • Header item
        "; + var viewModel = { + someitems: ['Alpha', 'Beta'] + }; + ko.applyBindings(viewModel, testNode); + + // Either of the following two results are acceptable. + try { + // Modern browsers implicitly re-add the closing tags + value_of(testNode).should_contain_html('
        • header item
        • alpha
        • beta
        '); + } catch (ex) { + // ... but IE < 8 doesn't add ones that immediately precede a
      • + value_of(testNode).should_contain_html('
        • header item
        • alpha
        • beta
        '); + } + }, + + 'Should be able to nest containerless templates directly inside UL elements, even on IE < 8 with its bizarre HTML parsing/formatting': function () { + // Represents https://github.com/SteveSanderson/knockout/issues/212 + // This test starts with the following DOM structure: + //
          + // + // + //
        • + // + // + //
        • + //
        + // Note that: + // 1. The closing comments are inside the
      • to simulate IE<8's weird parsing + // 2. We have to build this with manual DOM operations, otherwise IE<8 will deform it in a different weird way + // It would be a more authentic test if we could set up the scenario using .innerHTML and then let the browser do whatever parsing it does normally, + // but unfortunately IE varies its weirdness according to whether it's really parsing an HTML doc, or whether you're using .innerHTML. + + testNode.innerHTML = ""; + testNode.appendChild(document.createElement("ul")); + testNode.firstChild.appendChild(document.createComment(virtualElementTag + " foreach: ['A', 'B']")); + testNode.firstChild.appendChild(document.createComment(virtualElementTag + " if: $data == 'B'")); + testNode.firstChild.appendChild(document.createElement("li")); + testNode.firstChild.lastChild.setAttribute(bindingAttribute, "text: $data"); + testNode.firstChild.lastChild.appendChild(document.createComment("/" + virtualElementTag)); + testNode.firstChild.lastChild.appendChild(document.createComment("/" + virtualElementTag)); + + ko.applyBindings(null, testNode); + value_of(testNode).should_contain_text("B"); + }, + + 'Should be able to output HTML5 elements (even on IE<9, as long as you reference either innershiv.js or jQuery1.7+Modernizr)': function () { + // Represents https://github.com/SteveSanderson/knockout/issues/194 + ko.utils.setHtml(testNode, "
        "); + var viewModel = { + someitems: ['Alpha', 'Beta'] + }; + ko.applyBindings(viewModel, testNode); + value_of(testNode).should_contain_html('
        alpha
        beta
        '); + }, + + 'Should be able to output HTML5 elements within container-less templates (same as above)': function () { + // Represents https://github.com/SteveSanderson/knockout/issues/194 + ko.utils.setHtml(testNode, "
        "); + var viewModel = { + someitems: ['Alpha', 'Beta'] + }; + ko.applyBindings(viewModel, testNode); + value_of(testNode).should_contain_html('
        alpha
        beta
        '); + } + }); } -}); \ No newline at end of file + defaultBindingsBehaviors() + defaultBindingsBehaviors({ name: "alternative configuration", virtualElementTag: "ko1", bindingAttribute: "data-bind1" }); \ No newline at end of file diff --git a/spec/templatingBehaviors.js b/spec/templatingBehaviors.js index 7984e6ae6..b60261649 100644 --- a/spec/templatingBehaviors.js +++ b/spec/templatingBehaviors.js @@ -1,689 +1,723 @@ +var templatingBehaviors = + function (testConfiguration) { + var existingBindingProvider = ko.bindingProvider.instance; + var myConfiguration = testConfiguration; -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; + + var dummyTemplateEngine = function (templates) { + var inMemoryTemplates = templates || {}; + var inMemoryTemplateData = {}; + + function dummyTemplateSource(id) { + this.id = id; } - 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()); + 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); + var res = result; } } + } + + if (options.bypassDomNodeWrap) + return ko.utils.parseHtmlFragment(result); + else { + var node = document.createElement("div"); - // Dummy [[js:...]] syntax (in case you need to use square brackets inside the expression) - result = result.replace(/\[\[js\:([\s\S]*?)\]\]/g, evalHandler); + // Annoyingly, IE strips out comment nodes unless they are contained between other nodes, so put some dummy nodes around the HTML, then remove them after parsing. + node.innerHTML = "
        a
        " + result + "
        a
        "; + node.removeChild(node.firstChild); + node.removeChild(node.lastChild); - // Dummy [js:...] syntax - result = result.replace(/\[js\:([\s\S]*?)\]/g, evalHandler); + return [node]; } - } - } + }; + + this.rewriteTemplate = function (template, rewriterCallback) { + // Only rewrite if the template isn't a function (can't rewrite those) + var templateSource = 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 ' + (myConfiguration ? myConfiguration.name : "default provider settings"), { + before_each: function () { + ko.setTemplateEngine(new ko.nativeTemplateEngine()); + var existingNode = document.getElementById("templatingTarget"); + if (existingNode != null) + existingNode.parentNode.removeChild(existingNode); + testNode = document.createElement("div"); + testNode.id = "templatingTarget"; + document.body.appendChild(testNode); + ko.bindingProvider["instance"] = new ko.bindingProvider(myConfiguration); + virtualElementTag = ko.bindingProvider["instance"].configuration.virtualElementTag; + bindingAttribute = ko.bindingProvider["instance"].configuration.bindingAttribute; + }, - if (options.bypassDomNodeWrap) - return ko.utils.parseHtmlFragment(result); - else { - var node = document.createElement("div"); + after_each: function () { + ko.bindingProvider.instance = existingBindingProvider; - // Annoyingly, IE strips out comment nodes unless they are contained between other nodes, so put some dummy nodes around the HTML, then remove them after parsing. - node.innerHTML = "
        a
        " + result + "
        a
        "; - node.removeChild(node.firstChild); - node.removeChild(node.lastChild); + }, - return [node]; - } - }; + '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, { bypassDomNodeWrap: true }); + }, - this.rewriteTemplate = function (template, rewriterCallback) { - // Only rewrite if the template isn't a function (can't rewrite those) - var templateSource = 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', { - before_each: function () { - ko.setTemplateEngine(new ko.nativeTemplateEngine()); - var existingNode = document.getElementById("templatingTarget"); - if (existingNode != null) - existingNode.parentNode.removeChild(existingNode); - testNode = document.createElement("div"); - testNode.id = "templatingTarget"; - document.body.appendChild(testNode); - }, - - '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, { bypassDomNodeWrap: true }); - }, - - '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 } - value_of(threw).should_be(true); - }, - - '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); - value_of(testNode.childNodes.length).should_be(1); - value_of(testNode.childNodes[0].innerHTML).should_be("ABC"); - }, - - 'Should be able to access newly rendered/inserted elements in \'afterRender\' callaback': function () { - var passedElement, passedDataItem; - var myCallback = function(elementsArray, dataItem) { - value_of(elementsArray.length).should_be(1); - passedElement = elementsArray[0]; - passedDataItem = dataItem; - } - var myModel = {}; - ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" })); - ko.renderTemplate("someTemplate", myModel, { afterRender: myCallback }, testNode); - value_of(passedElement.innerHTML).should_be("ABC"); - value_of(passedDataItem).should_be(myModel); - }, - - '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); - value_of(testNode.childNodes.length).should_be(1); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); - - dependency("B"); - value_of(testNode.childNodes.length).should_be(1); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = B"); - }, - - '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); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); - - observable("B"); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = B"); - }, - - '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); - value_of(testNode.childNodes.length).should_be(1); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); - - testNode.parentNode.removeChild(testNode); - dependency("B"); - value_of(testNode.childNodes.length).should_be(1); - value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        template output
        "); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        result = 123
        "); - }, - - '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); - - value_of(innerObservable.getSubscriptionsCount()).should_be(1); - ko.cleanAndRemoveNode(testNode.childNodes[0]); - value_of(innerObservable.getSubscriptionsCount()).should_be(0); - }, - - 'Should be able to pick template as a function of the data item using data-bind syntax': function () { - var templatePicker = function(dataItem) { - return dataItem.myTemplate; - }; - ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" })); - testNode.innerHTML = "
        "; - ko.applyBindings({ someProp: { childProp: 123, myTemplate: "someTemplate" }, templateSelectorFunction: templatePicker }, testNode); - value_of(testNode.childNodes[0]).should_contain_html("
        result = 123
        "); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        inner template output 123
        "); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        abc
        "); - value_of(timesRenderedOuter).should_be(1); - value_of(timesRenderedInner).should_be(1); - - observable("DEF"); - value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        def
        "); - value_of(timesRenderedOuter).should_be(1); - value_of(timesRenderedInner).should_be(2); - }, - - '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); - - value_of(innerObservable.getSubscriptionsCount()).should_be(1); - ko.cleanAndRemoveNode(document.getElementById('innerTemplateOutput')); - value_of(innerObservable.getSubscriptionsCount()).should_be(0); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0].value).should_be("Hi"); - }, - - 'Should handle data-bind attributes that include newlines from inside templates': function () { - ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" })); - ko.renderTemplate("someTemplate", null, null, testNode); - value_of(testNode.childNodes[0].childNodes[0].value).should_be("Hi"); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0].value).should_be("hello"); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0].value).should_be("goodbye"); - }, - - '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].childNodes[0]; - value_of(buttonNode.tagName).should_be("BUTTON"); // Be sure we're clicking the right thing - buttonNode.click(); - value_of(viewModel.didCallMyFunction).should_be(true); - }, - - '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); - } - }; + '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 + } + value_of(threw).should_be(true); + }, - 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); - value_of(model.numBindings).should_be(2); - value_of(testNode.childNodes[0]).should_contain_html("outer
        inner via inline binding: 2inner via external binding: 1
        "); - - ko.bindingProvider.instance = originalBindingProvider; - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        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" }); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is frank
        the item is steve
        "); - value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode); - value_of(testNode.childNodes[0].childNodes[1]).should_be(originalFrankNode); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is frank
        "); - }, - - '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); - value_of(initCalls).should_be(3); // 3 because there were 3 items in myCollection - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is undefined
        the item is null
        "); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is steve
        the item is another
        "); - var originalBobNode = testNode.childNodes[0].childNodes[0]; - - myObservable("Steve2"); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is steve2
        the item is another
        "); - value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode); - - // Ensure we can still remove the corresponding nodes (even though they've changed), and that doing so causes the subscription to be disposed - value_of(myObservable.getSubscriptionsCount()).should_be(1); - myArray.splice(1, 1); - value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is another
        "); - myObservable("Something else"); // Re-evaluating the observable causes the orphaned subscriptions to be disposed - value_of(myObservable.getSubscriptionsCount()).should_be(0); - }, - - '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); - value_of(testNode.childNodes[0].childNodes.length).should_be(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); - value_of(testNode.childNodes[0].childNodes.length).should_be(0); - }, - - '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); - value_of(innerObservable.getSubscriptionsCount()).should_be(2); - - ko.cleanAndRemoveNode(testNode.childNodes[0]); - value_of(innerObservable.getSubscriptionsCount()).should_be(0); - }, - - '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); - value_of(innerObservable.getSubscriptionsCount()).should_be(2); - - myArray.splice(1, 1); - value_of(innerObservable.getSubscriptionsCount()).should_be(1); - myArray([]); - value_of(innerObservable.getSubscriptionsCount()).should_be(0); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        someprop=1
        someprop=3
        someprop=4
        "); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_html("
        someprop=1
        someprop=2
        someprop=3
        "); - }, - - '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 - value_of(testNode.childNodes[0]).should_contain_text("Value: abc"); - - // Causing the condition to become false causes the output to be removed - viewModel.myProp(null); - value_of(testNode.childNodes[0]).should_contain_text(""); - - // Causing the condition to become true causes the output to reappear - viewModel.myProp({ childProp: 'def' }); - value_of(testNode.childNodes[0]).should_contain_text("Value: def"); - }, - - '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) - value_of(testNode.childNodes[0]).should_contain_text(""); - - // Causing the condition to become false causes the output to be displayed - viewModel.shouldHide(false); - value_of(testNode.childNodes[0]).should_contain_text("Hello"); - - // Causing the condition to become true causes the output to disappear - viewModel.shouldHide(true); - value_of(testNode.childNodes[0]).should_contain_text(""); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Value: abc"); - value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("Value: abc"); - value_of(testNode.childNodes[0].childNodes[2]).should_contain_text("Value: abc"); - - // Causing the condition to become false causes the output to be removed - viewModel.myProp(null); - value_of(testNode.childNodes[0]).should_contain_text(""); - - // Causing the condition to become true causes the output to reappear - viewModel.myProp({ childProp: 'def' }); - value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Value: def"); - value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("Value: def"); - value_of(testNode.childNodes[0].childNodes[2]).should_contain_text("Value: def"); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0].checked).should_be(true); - }, - - '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); - value_of(testNode.childNodes[0].childNodes[0].checked).should_be(true); - }, - - 'Should be able to render a different template for each array entry by passing a function as template name': 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) { - return dataItem.preferredTemplate == 1 ? 'firstTemplate' : 'secondTemplate'; - }; - ko.applyBindings({ myCollection: myArray, getTemplateModelProperty: getTemplate }, testNode); - value_of(testNode.childNodes[0]).should_contain_html("
        template1output, firstitemvalue
        template2output, seconditemvalue
        "); - }, - - '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 = "
        "; + '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); + value_of(testNode.childNodes.length).should_be(1); + value_of(testNode.childNodes[0].innerHTML).should_be("ABC"); + }, - ko.applyBindings(myModel, testNode); - value_of(testNode.childNodes[0]).should_contain_html("
        person alpha has additional property someadditionalvalue
        person beta has additional property someadditionalvalue
        "); - }, - - 'If the template binding is updated, should dispose any template subscriptions previously associated with the element': function() { - var myModel = { - myObservable: ko.observable("some value"), - unrelatedObservable: ko.observable() - }; - 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 - value_of(testNode.childNodes[0]).should_contain_html("
        the value is some value
        "); - value_of(myModel.myObservable.getSubscriptionsCount()).should_be(1); - - // By changing unrelatedObservable, we force the data-bind value to be re-evaluated, setting up a new template subscription, - // so there have now existed two subscriptions on myObservable... - myModel.unrelatedObservable("any value"); - - // ...but, because the old subscription should have been disposed automatically, there should only be one left - value_of(myModel.myObservable.getSubscriptionsCount()).should_be(1); - }, - - '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); - - value_of(testNode.childNodes[0]).should_contain_text("Alternative output"); - }, - - '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); - value_of(testNode.childNodes[0]).should_contain_text("ValueLiteral: Hello, ValueBound: Hello"); - }, - - '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" } + 'Should be able to access newly rendered/inserted elements in \'afterRender\' callaback': function () { + var passedElement, passedDataItem; + var myCallback = function (elementsArray, dataItem) { + value_of(elementsArray.length).should_be(1); + passedElement = elementsArray[0]; + passedDataItem = dataItem; } + var myModel = {}; + ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "ABC" })); + ko.renderTemplate("someTemplate", myModel, { afterRender: myCallback }, testNode); + value_of(passedElement.innerHTML).should_be("ABC"); + value_of(passedDataItem).should_be(myModel); + }, + + '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); + value_of(testNode.childNodes.length).should_be(1); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); + + dependency("B"); + value_of(testNode.childNodes.length).should_be(1); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = B"); + }, + + '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); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); + + observable("B"); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = B"); + }, + + '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); + value_of(testNode.childNodes.length).should_be(1); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); + + testNode.parentNode.removeChild(testNode); + dependency("B"); + value_of(testNode.childNodes.length).should_be(1); + value_of(testNode.childNodes[0].innerHTML).should_be("Value = A"); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        template output
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        result = 123
        "); + }, + + '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); + + value_of(innerObservable.getSubscriptionsCount()).should_be(1); + ko.cleanAndRemoveNode(testNode.childNodes[0]); + value_of(innerObservable.getSubscriptionsCount()).should_be(0); + }, + + 'Should be able to pick template as a function of the data item using data-bind syntax': function () { + var templatePicker = function (dataItem) { + return dataItem.myTemplate; + }; + ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" })); + testNode.innerHTML = "
        "; + ko.applyBindings({ someProp: { childProp: 123, myTemplate: "someTemplate" }, templateSelectorFunction: templatePicker }, testNode); + value_of(testNode.childNodes[0]).should_contain_html("
        result = 123
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        inner template output 123
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        abc
        "); + value_of(timesRenderedOuter).should_be(1); + value_of(timesRenderedInner).should_be(1); + + observable("DEF"); + value_of(testNode.childNodes[0]).should_contain_html("
        outer template output,
        def
        "); + value_of(timesRenderedOuter).should_be(1); + value_of(timesRenderedInner).should_be(2); + }, + + '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); + + value_of(innerObservable.getSubscriptionsCount()).should_be(1); + ko.cleanAndRemoveNode(document.getElementById('innerTemplateOutput')); + value_of(innerObservable.getSubscriptionsCount()).should_be(0); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].value).should_be("Hi"); + }, + + 'Should handle data-bind attributes that include newlines from inside templates': function () { + ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" })); + ko.renderTemplate("someTemplate", null, null, testNode); + value_of(testNode.childNodes[0].childNodes[0].value).should_be("Hi"); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].value).should_be("hello"); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].value).should_be("goodbye"); + }, + + '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].childNodes[0]; + value_of(buttonNode.tagName).should_be("BUTTON"); // Be sure we're clicking the right thing + buttonNode.click(); + value_of(viewModel.didCallMyFunction).should_be(true); + }, + + '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); + }, + configuration : originalBindingProvider.configuration //???!!! + }; + + 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); + value_of(model.numBindings).should_be(2); + value_of(testNode.childNodes[0]).should_contain_html("outer
        inner via inline binding: 2inner via external binding: 1
        "); + + ko.bindingProvider.instance = originalBindingProvider; + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        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" }); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is frank
        the item is steve
        "); + value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode); + value_of(testNode.childNodes[0].childNodes[1]).should_be(originalFrankNode); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is frank
        "); + }, + + '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); + value_of(initCalls).should_be(3); // 3 because there were 3 items in myCollection + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is undefined
        the item is null
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is steve
        the item is another
        "); + var originalBobNode = testNode.childNodes[0].childNodes[0]; + + myObservable("Steve2"); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is steve2
        the item is another
        "); + value_of(testNode.childNodes[0].childNodes[0]).should_be(originalBobNode); + + // Ensure we can still remove the corresponding nodes (even though they've changed), and that doing so causes the subscription to be disposed + value_of(myObservable.getSubscriptionsCount()).should_be(1); + myArray.splice(1, 1); + value_of(testNode.childNodes[0]).should_contain_html("
        the item is bob
        the item is another
        "); + myObservable("Something else"); // Re-evaluating the observable causes the orphaned subscriptions to be disposed + value_of(myObservable.getSubscriptionsCount()).should_be(0); + }, + + '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); + value_of(testNode.childNodes[0].childNodes.length).should_be(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); + value_of(testNode.childNodes[0].childNodes.length).should_be(0); + }, + + '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); + value_of(innerObservable.getSubscriptionsCount()).should_be(2); + + ko.cleanAndRemoveNode(testNode.childNodes[0]); + value_of(innerObservable.getSubscriptionsCount()).should_be(0); + }, + + '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); + value_of(innerObservable.getSubscriptionsCount()).should_be(2); + + myArray.splice(1, 1); + value_of(innerObservable.getSubscriptionsCount()).should_be(1); + myArray([]); + value_of(innerObservable.getSubscriptionsCount()).should_be(0); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        someprop=1
        someprop=3
        someprop=4
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        someprop=1
        someprop=2
        someprop=3
        "); + }, + + '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 + value_of(testNode.childNodes[0]).should_contain_text("Value: abc"); + + // Causing the condition to become false causes the output to be removed + viewModel.myProp(null); + value_of(testNode.childNodes[0]).should_contain_text(""); + + // Causing the condition to become true causes the output to reappear + viewModel.myProp({ childProp: 'def' }); + value_of(testNode.childNodes[0]).should_contain_text("Value: def"); + }, + + '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) + value_of(testNode.childNodes[0]).should_contain_text(""); + + // Causing the condition to become false causes the output to be displayed + viewModel.shouldHide(false); + value_of(testNode.childNodes[0]).should_contain_text("Hello"); + + // Causing the condition to become true causes the output to disappear + viewModel.shouldHide(true); + value_of(testNode.childNodes[0]).should_contain_text(""); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Value: abc"); + value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("Value: abc"); + value_of(testNode.childNodes[0].childNodes[2]).should_contain_text("Value: abc"); + + // Causing the condition to become false causes the output to be removed + viewModel.myProp(null); + value_of(testNode.childNodes[0]).should_contain_text(""); + + // Causing the condition to become true causes the output to reappear + viewModel.myProp({ childProp: 'def' }); + value_of(testNode.childNodes[0].childNodes[0]).should_contain_text("Value: def"); + value_of(testNode.childNodes[0].childNodes[1]).should_contain_text("Value: def"); + value_of(testNode.childNodes[0].childNodes[2]).should_contain_text("Value: def"); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].checked).should_be(true); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].checked).should_be(true); + }, + + 'Should be able to render a different template for each array entry by passing a function as template name': 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) { + return dataItem.preferredTemplate == 1 ? 'firstTemplate' : 'secondTemplate'; + }; + ko.applyBindings({ myCollection: myArray, getTemplateModelProperty: getTemplate }, testNode); + value_of(testNode.childNodes[0]).should_contain_html("
        template1output, firstitemvalue
        template2output, seconditemvalue
        "); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_html("
        person alpha has additional property someadditionalvalue
        person beta has additional property someadditionalvalue
        "); + }, + + 'If the template binding is updated, should dispose any template subscriptions previously associated with the element': function () { + var myModel = { + myObservable: ko.observable("some value"), + unrelatedObservable: ko.observable() + }; + 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 + value_of(testNode.childNodes[0]).should_contain_html("
        the value is some value
        "); + value_of(myModel.myObservable.getSubscriptionsCount()).should_be(1); + + // By changing unrelatedObservable, we force the data-bind value to be re-evaluated, setting up a new template subscription, + // so there have now existed two subscriptions on myObservable... + myModel.unrelatedObservable("any value"); + + // ...but, because the old subscription should have been disposed automatically, there should only be one left + value_of(myModel.myObservable.getSubscriptionsCount()).should_be(1); + }, + + '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); + + value_of(testNode.childNodes[0]).should_contain_text("Alternative output"); + }, + + '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); + value_of(testNode.childNodes[0]).should_contain_text("ValueLiteral: Hello, ValueBound: Hello"); + }, + + '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); + value_of(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]).should_contain_text("(Data:INNER, Parent:MIDDLE, Grandparent:OUTER, Root:ROOT, Depth:3)"); + }, + + '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; + value_of(ex.message).should_be("This template engine does not support anonymous templates nested within its templates"); + } + value_of(didThrow).should_be(true); + }, + + '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; + try { + ko.applyBindings({ someData: { childProp: 'abc'} }, testNode) + } catch (ex) { + didThrow = true; + value_of(ex.message).should_be("This template engine does not support the '" + bindingName + "' binding within its templates"); + } + if (!didThrow) + throw new Error("Did not prevent use of " + bindingName); + }); + }, + + '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); + value_of(testNode).should_contain_html("start
        childprop: abc
        end"); + }, + + '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); + value_of(testNode).should_contain_html('start
        hellohello
        '); + }, + + '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); + value_of(initCalls).should_be(1); } - }, testNode); - value_of(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]).should_contain_text("(Data:INNER, Parent:MIDDLE, Grandparent:OUTER, Root:ROOT, Depth:3)"); - }, - - '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; - value_of(ex.message).should_be("This template engine does not support anonymous templates nested within its templates"); - } - value_of(didThrow).should_be(true); - }, - - '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; - try { ko.applyBindings({ someData: { childProp: 'abc' } }, testNode) } - catch (ex) { - didThrow = true; - value_of(ex.message).should_be("This template engine does not support the '" + bindingName + "' binding within its templates"); - } - if (!didThrow) - throw new Error("Did not prevent use of " + bindingName); - }); - }, + }) + }; + + templatingBehaviors(); + templatingBehaviors({ name: "alternative configuration", virtualElementTag: "ko1", bindingAttribute: "data-bind1" }); - '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); - value_of(testNode).should_contain_html("start
        childprop: abc
        end"); - }, - - '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); - value_of(testNode).should_contain_html('start
        hellohello
        '); - }, - - '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); - value_of(initCalls).should_be(1); - } -}) \ No newline at end of file + \ No newline at end of file diff --git a/src/templating/templateEngine.js b/src/templating/templateEngine.js index f23d4e84e..de5a22341 100644 --- a/src/templating/templateEngine.js +++ b/src/templating/templateEngine.js @@ -8,7 +8,7 @@ // // - bindingContext.$data is the data you should pass into the template // // - you might also want to make bindingContext.$parent, bindingContext.$parents, // // and bindingContext.$root available in the template too -// // - options gives you access to any other properties set on "data-bind: { template: options }" +// // - options gives you access to any other properties set on "data-bind: { template: options } (where data-bind is the configured binding attribute)" // // // // Return value: an array of DOM nodes // } @@ -20,7 +20,7 @@ // // For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }' // } // -// This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables. +// This is only necessary if you want to allow binding attributes to reference arbitrary template variables. // If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does) // and then you don't need to override 'createJavaScriptEvaluatorBlock'. From 4516c1553a92c51c0c49816d0fb3fa20cfd3dc70 Mon Sep 17 00:00:00 2001 From: GilesBradshaw Date: Fri, 16 Mar 2012 11:19:31 +0000 Subject: [PATCH 4/7] Build --- build/output/knockout-latest.debug.js | 1976 +++++++++++++++---------- build/output/knockout-latest.js | 169 ++- 2 files changed, 1307 insertions(+), 838 deletions(-) diff --git a/build/output/knockout-latest.debug.js b/build/output/knockout-latest.debug.js index f91f9c9af..1247043a0 100644 --- a/build/output/knockout-latest.debug.js +++ b/build/output/knockout-latest.debug.js @@ -1,9 +1,11 @@ -// Knockout JavaScript library v2.1.0pre +// Knockout JavaScript library v2.1.0pre+mbest/smart-binding/beta.3 // (c) Steven Sanderson - http://knockoutjs.com/ // License: MIT (http://www.opensource.org/licenses/mit-license.php) -(function(window,document,navigator,undefined){ -!function(factory) { +(function(){ +var DEBUG=true; +(function(window,document,navigator,undefined){function ko_throw(e){throw Error(e)} +(function(factory) { // Support three module loading scenarios if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { // [1] CommonJS/Node.js @@ -16,7 +18,7 @@ // [3] No module loader (plain