From 9d230788cd1ebdb8cbc0e1210163b53c24e1e1ea Mon Sep 17 00:00:00 2001 From: Ryan Niemeyer Date: Fri, 13 Jun 2014 22:33:18 -0500 Subject: [PATCH 1/2] Make "value" binding act like "checkedValue" when on a checkbox or radio --- spec/defaultBindings/checkedBehaviors.js | 383 ++++++++++++++++------- spec/defaultBindings/valueBehaviors.js | 38 +++ src/binding/defaultBindings/checked.js | 15 +- src/binding/defaultBindings/value.js | 12 +- 4 files changed, 323 insertions(+), 125 deletions(-) diff --git a/spec/defaultBindings/checkedBehaviors.js b/spec/defaultBindings/checkedBehaviors.js index 5f7cd10f6..ba31be9bd 100644 --- a/spec/defaultBindings/checkedBehaviors.js +++ b/spec/defaultBindings/checkedBehaviors.js @@ -176,148 +176,293 @@ describe('Binding: Checked', function() { expect(testNode.childNodes[0].checked).toEqual(false); }); - it('When a \'checkedValue\' is specified, should use that as the checkbox value in the array', function() { - var model = { myArray: ko.observableArray([1,3]) }; - testNode.innerHTML = "" - + ""; - ko.applyBindings(model, testNode); + describe("With \'checkedValue\'", function() { + it('When a \'checkedValue\' is specified, should use that as the checkbox value in the array', function() { + var model = { myArray: ko.observableArray([1,3]) }; + testNode.innerHTML = "" + + ""; + ko.applyBindings(model, testNode); + + expect(model.myArray()).toEqual([1,3]); // initial value is unchanged + + // Checkbox initial state is determined by whether the value is in the array + expect(testNode.childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[1].checked).toEqual(false); + + // Verify that checkedValue sets element value + expect(testNode.childNodes[0].value).toEqual('1'); + expect(testNode.childNodes[1].value).toEqual('2'); + + // Checking the checkbox puts it in the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + expect(testNode.childNodes[1].checked).toEqual(true); + expect(model.myArray()).toEqual([1,3,2]); + + // Unchecking the checkbox removes it from the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + expect(testNode.childNodes[1].checked).toEqual(false); + expect(model.myArray()).toEqual([1,3]); + + // Put the value in the array; observe the checkbox reflect this + model.myArray.push(2); + expect(testNode.childNodes[1].checked).toEqual(true); + + // Remove the value from the array; observe the checkbox reflect this + model.myArray.remove(1); + expect(testNode.childNodes[0].checked).toEqual(false); + }); - expect(model.myArray()).toEqual([1,3]); // initial value is unchanged + it('Should be able to use objects as value of checkboxes using \'checkedValue\'', function() { + var object1 = {x:1}, + object2 = {y:1}, + model = { values: [object1], choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + // Checkbox initial state is determined by whether the value is in the array + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Checking the checkbox puts it in the array + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + expect(model.values).toEqual([object1, object2]); + + // Unchecking the checkbox removes it from the array + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + expect(model.values).toEqual([object1]); + }); - // Checkbox initial state is determined by whether the value is in the array - expect(testNode.childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[1].checked).toEqual(false); + it('Should be able to use observables as value of checkboxes using \'checkedValue\'', function() { + var object1 = {id:ko.observable(1)}, + object2 = {id:ko.observable(2)}, + model = { values: [1], choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + expect(model.values).toEqual([1]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the checked item; should update the selected values and leave checked values unchanged + object1.id(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + expect(model.values).toEqual([3]); + + // Update the value observable of the unchecked item; should do nothing + object2.id(4); + expect(model.values).toEqual([3]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item to the current model value; should set to checked + object2.id(3); + expect(model.values).toEqual([3]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + }); - // Verify that checkedValue sets element value - expect(testNode.childNodes[0].value).toEqual('1'); - expect(testNode.childNodes[1].value).toEqual('2'); + it('When a \'checkedValue\' is specified, should use that as the radio button\'s value', function () { + var myobservable = new ko.observable(false); + testNode.innerHTML = "" + + ""; + ko.applyBindings({ someProp: myobservable }, testNode); - // Checking the checkbox puts it in the array - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - expect(testNode.childNodes[1].checked).toEqual(true); - expect(model.myArray()).toEqual([1,3,2]); + expect(myobservable()).toEqual(false); - // Unchecking the checkbox removes it from the array - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - expect(testNode.childNodes[1].checked).toEqual(false); - expect(model.myArray()).toEqual([1,3]); + // Check initial state + expect(testNode.childNodes[0].checked).toEqual(false); + expect(testNode.childNodes[1].checked).toEqual(true); - // Put the value in the array; observe the checkbox reflect this - model.myArray.push(2); - expect(testNode.childNodes[1].checked).toEqual(true); + // Update observable; verify elements + myobservable(true); + expect(testNode.childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[1].checked).toEqual(false); - // Remove the value from the array; observe the checkbox reflect this - model.myArray.remove(1); - expect(testNode.childNodes[0].checked).toEqual(false); + // "Click" a button; verify observable and elements + testNode.childNodes[1].click(); + expect(myobservable()).toEqual(false); + expect(testNode.childNodes[0].checked).toEqual(false); + expect(testNode.childNodes[1].checked).toEqual(true); + }); + + it('Should be able to use observables as value of radio buttons using \'checkedValue\'', function () { + var object1 = {id:ko.observable(1)}, + object2 = {id:ko.observable(2)}, + model = { value: 1, choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + expect(model.value).toEqual(1); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the checked item; should update the selected value and leave checked values unchanged + object1.id(3); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item; should do nothing + object2.id(4); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item to the current model value; should set to checked + object2.id(3); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + }); }); - it('Should be able to use objects as value of checkboxes using \'checkedValue\'', function() { - var object1 = {x:1}, - object2 = {y:1}, - model = { values: [object1], choices: [object1, object2] }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); + describe('\'value\' treated like \'checkedValue\' when used with \'checked\'.', function() { + it('When a \'value\' is specified, should use that as the checkbox value in the array', function() { + var model = { myArray: ko.observableArray([1,3]) }; + testNode.innerHTML = "" + + ""; + ko.applyBindings(model, testNode); - // Checkbox initial state is determined by whether the value is in the array - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + expect(model.myArray()).toEqual([1,3]); // initial value is unchanged - // Checking the checkbox puts it in the array - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); - expect(model.values).toEqual([object1, object2]); + // Checkbox initial state is determined by whether the value is in the array + expect(testNode.childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[1].checked).toEqual(false); - // Unchecking the checkbox removes it from the array - ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - expect(model.values).toEqual([object1]); - }); + // Verify that checkedValue sets element value + expect(testNode.childNodes[0].value).toEqual('1'); + expect(testNode.childNodes[1].value).toEqual('2'); - it('Should be able to use observables as value of checkboxes using \'checkedValue\'', function() { - var object1 = {id:ko.observable(1)}, - object2 = {id:ko.observable(2)}, - model = { values: [1], choices: [object1, object2] }; - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); + // Checking the checkbox puts it in the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + expect(testNode.childNodes[1].checked).toEqual(true); + expect(model.myArray()).toEqual([1,3,2]); - expect(model.values).toEqual([1]); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - - // Update the value observable of the checked item; should update the selected values and leave checked values unchanged - object1.id(3); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - expect(model.values).toEqual([3]); - - // Update the value observable of the unchecked item; should do nothing - object2.id(4); - expect(model.values).toEqual([3]); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - - // Update the value observable of the unchecked item to the current model value; should set to checked - object2.id(3); - expect(model.values).toEqual([3]); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); - }); + // Unchecking the checkbox removes it from the array + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + expect(testNode.childNodes[1].checked).toEqual(false); + expect(model.myArray()).toEqual([1,3]); - it('When a \'checkedValue\' is specified, should use that as the radio button\'s value', function () { - var myobservable = new ko.observable(false); - testNode.innerHTML = "" + - ""; - ko.applyBindings({ someProp: myobservable }, testNode); + // Put the value in the array; observe the checkbox reflect this + model.myArray.push(2); + expect(testNode.childNodes[1].checked).toEqual(true); - expect(myobservable()).toEqual(false); + // Remove the value from the array; observe the checkbox reflect this + model.myArray.remove(1); + expect(testNode.childNodes[0].checked).toEqual(false); + }); - // Check initial state - expect(testNode.childNodes[0].checked).toEqual(false); - expect(testNode.childNodes[1].checked).toEqual(true); + it('Should be able to use objects as value of checkboxes using \'value\'', function() { + var object1 = {x:1}, + object2 = {y:1}, + model = { values: [object1], choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + // Checkbox initial state is determined by whether the value is in the array + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Checking the checkbox puts it in the array + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + expect(model.values).toEqual([object1, object2]); + + // Unchecking the checkbox removes it from the array + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click"); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + expect(model.values).toEqual([object1]); + }); - // Update observable; verify elements - myobservable(true); - expect(testNode.childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[1].checked).toEqual(false); + it('Should be able to use observables as value of checkboxes using \'value\'', function() { + var object1 = {id:ko.observable(1)}, + object2 = {id:ko.observable(2)}, + model = { values: [1], choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + expect(model.values).toEqual([1]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the checked item; should update the selected values and leave checked values unchanged + object1.id(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + expect(model.values).toEqual([3]); + + // Update the value observable of the unchecked item; should do nothing + object2.id(4); + expect(model.values).toEqual([3]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item to the current model value; should set to checked + object2.id(3); + expect(model.values).toEqual([3]); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + }); - // "Click" a button; verify observable and elements - testNode.childNodes[1].click(); - expect(myobservable()).toEqual(false); - expect(testNode.childNodes[0].checked).toEqual(false); - expect(testNode.childNodes[1].checked).toEqual(true); - }); + it('When a \'value\' is specified, should use that as the radio button\'s value', function () { + var myobservable = new ko.observable(false); + testNode.innerHTML = "" + + ""; + ko.applyBindings({ someProp: myobservable }, testNode); - it('Should be able to use observables as value of radio buttons using \'checkedValue\'', function () { - var object1 = {id:ko.observable(1)}, - object2 = {id:ko.observable(2)}, - model = { value: 1, choices: [object1, object2] }; - var myobservable = new ko.observable(false); - testNode.innerHTML = "
"; - ko.applyBindings(model, testNode); + expect(myobservable()).toEqual(false); + + // Check initial state + expect(testNode.childNodes[0].checked).toEqual(false); + expect(testNode.childNodes[1].checked).toEqual(true); - expect(model.value).toEqual(1); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - - // Update the value observable of the checked item; should update the selected value and leave checked values unchanged - object1.id(3); - expect(model.value).toEqual(3); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - - // Update the value observable of the unchecked item; should do nothing - object2.id(4); - expect(model.value).toEqual(3); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); - - // Update the value observable of the unchecked item to the current model value; should set to checked - object2.id(3); - expect(model.value).toEqual(3); - expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); - expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + // Update observable; verify elements + myobservable(true); + expect(testNode.childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[1].checked).toEqual(false); + + // "Click" a button; verify observable and elements + testNode.childNodes[1].click(); + expect(myobservable()).toEqual(false); + expect(testNode.childNodes[0].checked).toEqual(false); + expect(testNode.childNodes[1].checked).toEqual(true); + }); + + it('Should be able to use observables as value of radio buttons using \'value\'', function () { + var object1 = {id:ko.observable(1)}, + object2 = {id:ko.observable(2)}, + model = { value: 1, choices: [object1, object2] }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + + expect(model.value).toEqual(1); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the checked item; should update the selected value and leave checked values unchanged + object1.id(3); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item; should do nothing + object2.id(4); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false); + + // Update the value observable of the unchecked item to the current model value; should set to checked + object2.id(3); + expect(model.value).toEqual(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); + }); }); + it('When the bound observable is updated in a subscription in response to a radio click, view and model should stay in sync', function() { // This test failed when jQuery was included before the changes made in #1191 testNode.innerHTML = '' + diff --git a/spec/defaultBindings/valueBehaviors.js b/spec/defaultBindings/valueBehaviors.js index f7d3f7036..7c05cc581 100644 --- a/spec/defaultBindings/valueBehaviors.js +++ b/spec/defaultBindings/valueBehaviors.js @@ -595,4 +595,42 @@ describe('Binding: Value', function() { }); }); }); + + describe('Acts like \'checkedValue\' on a checkbox or radio', function() { + it('Should update value, but not respond to events when on a checkbox', function() { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + + var checkbox = testNode.childNodes[0]; + expect(checkbox.value).toEqual('B'); + + observable('C'); + expect(checkbox.value).toEqual('C'); + + checkbox.value = 'D'; + ko.utils.triggerEvent(checkbox, "change"); + + // observable does not update, as we are not handling events when on a checkbox/radio + expect(observable()).toEqual('C'); + }); + + it('Should update value, but not respond to events when on a radio', function() { + var observable = new ko.observable('B'); + testNode.innerHTML = ""; + ko.applyBindings({ myObservable: observable }, testNode); + + var radio = testNode.childNodes[0]; + expect(radio.value).toEqual('B'); + + observable('C'); + expect(radio.value).toEqual('C'); + + radio.value = 'D'; + ko.utils.triggerEvent(radio, "change"); + + // observable does not update, as we are not handling events when on a checkbox/radio + expect(observable()).toEqual('C'); + }); + }); }); \ No newline at end of file diff --git a/src/binding/defaultBindings/checked.js b/src/binding/defaultBindings/checked.js index 5ed3ecec1..49f9e482e 100755 --- a/src/binding/defaultBindings/checked.js +++ b/src/binding/defaultBindings/checked.js @@ -3,11 +3,16 @@ ko.bindingHandlers['checked'] = { 'after': ['value', 'attr'], 'init': function (element, valueAccessor, allBindings) { - function checkedValue() { - return allBindings['has']('checkedValue') - ? ko.utils.unwrapObservable(allBindings.get('checkedValue')) - : element.value; - } + var checkedValue = ko.pureComputed(function() { + // Treat "value" like "checkedValue" when it is included with "checked" binding + if (allBindings['has']('checkedValue')) { + return ko.utils.unwrapObservable(allBindings.get('checkedValue')); + } else if (allBindings['has']('value')) { + return ko.utils.unwrapObservable(allBindings.get('value')); + } + + return element.value; + }, null, { disposeWhenNodeIsRemoved: element }); function updateModel() { // This updates the model value from the view value. diff --git a/src/binding/defaultBindings/value.js b/src/binding/defaultBindings/value.js index ca92403aa..73e13a20a 100755 --- a/src/binding/defaultBindings/value.js +++ b/src/binding/defaultBindings/value.js @@ -7,6 +7,15 @@ ko.bindingHandlers['value'] = { var propertyChangedFired = false; var elementValueBeforeEvent = null; + // If the value binding is placed on a radio/checkbox, then just pass through to checkedValue and quit + if (element.tagName.toLowerCase() == "input" && (element.type == "checkbox" || element.type == "radio")) { + ko.computed(function () { + ko.bindingHandlers['checkedValue']['update'](element, valueAccessor); + }, this, { disposeWhenNodeIsRemoved: element }); + + return; + } + if (requestedEventsToCatch) { if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names requestedEventsToCatch = [requestedEventsToCatch]; @@ -94,6 +103,7 @@ ko.bindingHandlers['value'] = { }; ko.computed(updateFromModel, null, { disposeWhenNodeIsRemoved: element }); - } + }, + 'update': function() {} // Keep for backwards compatibility with code that may have wrapped value binding }; ko.expressionRewriting.twoWayBindings['value'] = true; From 0d140b9c46b3c7c15914533db09cbcf1532c80fb Mon Sep 17 00:00:00 2001 From: Michael Best Date: Sat, 14 Jun 2014 11:09:06 -1000 Subject: [PATCH 2/2] Use ko.applyBindingAccessorsToNode in value to wrap checkedValue; ko.pureComputed doesn't need disposeWhenNodeIsRemoved option. --- spec/defaultBindings/checkedBehaviors.js | 75 ++++++++++++++++-------- src/binding/defaultBindings/checked.js | 2 +- src/binding/defaultBindings/value.js | 15 ++--- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/spec/defaultBindings/checkedBehaviors.js b/spec/defaultBindings/checkedBehaviors.js index ba31be9bd..b712d314e 100644 --- a/spec/defaultBindings/checkedBehaviors.js +++ b/spec/defaultBindings/checkedBehaviors.js @@ -176,6 +176,28 @@ describe('Binding: Checked', function() { expect(testNode.childNodes[0].checked).toEqual(false); }); + it('When the bound observable is updated in a subscription in response to a radio click, view and model should stay in sync', function() { + // This test failed when jQuery was included before the changes made in #1191 + testNode.innerHTML = '' + + '' + + ''; + var choice = ko.observable('1'); + choice.subscribe(function(newValue) { + if (newValue == '3') // don't allow item 3 to be selected; revert to item 1 + choice('1'); + }); + ko.applyBindings({choice: choice}, testNode); + expect(testNode.childNodes[0].checked).toEqual(true); + + // Click on item 2; verify it's selected + ko.utils.triggerEvent(testNode.childNodes[1], "click"); + expect(testNode.childNodes[1].checked).toEqual(true); + + // Click on item 3; verify item 1 is selected + ko.utils.triggerEvent(testNode.childNodes[2], "click"); + expect(testNode.childNodes[0].checked).toEqual(true); + }); + describe("With \'checkedValue\'", function() { it('When a \'checkedValue\' is specified, should use that as the checkbox value in the array', function() { var model = { myArray: ko.observableArray([1,3]) }; @@ -265,7 +287,7 @@ describe('Binding: Checked', function() { }); it('When a \'checkedValue\' is specified, should use that as the radio button\'s value', function () { - var myobservable = new ko.observable(false); + var myobservable = ko.observable(false); testNode.innerHTML = "" + ""; ko.applyBindings({ someProp: myobservable }, testNode); @@ -288,6 +310,19 @@ describe('Binding: Checked', function() { expect(testNode.childNodes[1].checked).toEqual(true); }); + it('When node is removed, subscription to observable bound to \'checkedValue\' is disposed', function () { + var model = { values: [1], checkedValue: ko.observable(1) }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + expect(model.values).toEqual([1]); + expect(testNode.childNodes[0].checked).toEqual(true); + expect(model.checkedValue.getSubscriptionsCount()).toBeGreaterThan(0); + + ko.removeNode(testNode.childNodes[0]); + expect(model.checkedValue.getSubscriptionsCount()).toEqual(0); + }); + it('Should be able to use observables as value of radio buttons using \'checkedValue\'', function () { var object1 = {id:ko.observable(1)}, object2 = {id:ko.observable(2)}, @@ -408,7 +443,7 @@ describe('Binding: Checked', function() { }); it('When a \'value\' is specified, should use that as the radio button\'s value', function () { - var myobservable = new ko.observable(false); + var myobservable = ko.observable(false); testNode.innerHTML = "" + ""; ko.applyBindings({ someProp: myobservable }, testNode); @@ -431,6 +466,19 @@ describe('Binding: Checked', function() { expect(testNode.childNodes[1].checked).toEqual(true); }); + it('When node is removed, subscription to observable bound to \'value\' is disposed', function () { + var model = { values: [1], checkedValue: ko.observable(1) }; + testNode.innerHTML = ""; + ko.applyBindings(model, testNode); + + expect(model.values).toEqual([1]); + expect(testNode.childNodes[0].checked).toEqual(true); + expect(model.checkedValue.getSubscriptionsCount()).toBeGreaterThan(0); + + ko.removeNode(testNode.childNodes[0]); + expect(model.checkedValue.getSubscriptionsCount()).toEqual(0); + }); + it('Should be able to use observables as value of radio buttons using \'value\'', function () { var object1 = {id:ko.observable(1)}, object2 = {id:ko.observable(2)}, @@ -461,27 +509,4 @@ describe('Binding: Checked', function() { expect(testNode.childNodes[0].childNodes[1].checked).toEqual(true); }); }); - - - it('When the bound observable is updated in a subscription in response to a radio click, view and model should stay in sync', function() { - // This test failed when jQuery was included before the changes made in #1191 - testNode.innerHTML = '' + - '' + - ''; - var choice = ko.observable('1'); - choice.subscribe(function(newValue) { - if (newValue == '3') // don't allow item 3 to be selected; revert to item 1 - choice('1'); - }); - ko.applyBindings({choice: choice}, testNode); - expect(testNode.childNodes[0].checked).toEqual(true); - - // Click on item 2; verify it's selected - ko.utils.triggerEvent(testNode.childNodes[1], "click"); - expect(testNode.childNodes[1].checked).toEqual(true); - - // Click on item 3; verify item 1 is selected - ko.utils.triggerEvent(testNode.childNodes[2], "click"); - expect(testNode.childNodes[0].checked).toEqual(true); - }); }); \ No newline at end of file diff --git a/src/binding/defaultBindings/checked.js b/src/binding/defaultBindings/checked.js index 49f9e482e..f4d0b08a4 100755 --- a/src/binding/defaultBindings/checked.js +++ b/src/binding/defaultBindings/checked.js @@ -12,7 +12,7 @@ ko.bindingHandlers['checked'] = { } return element.value; - }, null, { disposeWhenNodeIsRemoved: element }); + }); function updateModel() { // This updates the model value from the view value. diff --git a/src/binding/defaultBindings/value.js b/src/binding/defaultBindings/value.js index 73e13a20a..ef6f832f8 100755 --- a/src/binding/defaultBindings/value.js +++ b/src/binding/defaultBindings/value.js @@ -1,21 +1,18 @@ ko.bindingHandlers['value'] = { 'after': ['options', 'foreach'], 'init': function (element, valueAccessor, allBindings) { + // If the value binding is placed on a radio/checkbox, then just pass through to checkedValue and quit + if (element.tagName.toLowerCase() == "input" && (element.type == "checkbox" || element.type == "radio")) { + ko.applyBindingAccessorsToNode(element, { 'checkedValue': valueAccessor }); + return; + } + // Always catch "change" event; possibly other events too if asked var eventsToCatch = ["change"]; var requestedEventsToCatch = allBindings.get("valueUpdate"); var propertyChangedFired = false; var elementValueBeforeEvent = null; - // If the value binding is placed on a radio/checkbox, then just pass through to checkedValue and quit - if (element.tagName.toLowerCase() == "input" && (element.type == "checkbox" || element.type == "radio")) { - ko.computed(function () { - ko.bindingHandlers['checkedValue']['update'](element, valueAccessor); - }, this, { disposeWhenNodeIsRemoved: element }); - - return; - } - if (requestedEventsToCatch) { if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names requestedEventsToCatch = [requestedEventsToCatch];