From 4db92498ff293d57b2626254b9c33d580f098bea Mon Sep 17 00:00:00 2001 From: Michael Best Date: Tue, 5 Mar 2013 15:17:26 -1000 Subject: [PATCH] Add checkedValue binding that works with the checked binding to allow for arbitrary values for checkboxes and radio buttons. --- spec/defaultBindings/checkedBehaviors.js | 103 +++++++++++++++++++++++ src/binding/defaultBindings/checked.js | 32 ++++--- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/spec/defaultBindings/checkedBehaviors.js b/spec/defaultBindings/checkedBehaviors.js index 1b654ba5f..e069f731c 100644 --- a/spec/defaultBindings/checkedBehaviors.js +++ b/spec/defaultBindings/checkedBehaviors.js @@ -173,4 +173,107 @@ describe('Binding: Checked', function() { model.myObservableArray.remove("My value"); 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); + + 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); + }); + + 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]); + }); + + 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; should update that checkbox + object1.id(3); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(false); + expect(model.values).toEqual([1]); // Represents current behavior, that the array is unchanged, although this might be confusing to some + + // Checking the checkbox adds it to the array + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click"); + expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true); + expect(model.values).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); + + expect(myobservable()).toEqual(false); + + // Check initial state + expect(testNode.childNodes[0].checked).toEqual(false); + 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); + + // "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); + }); }); \ No newline at end of file diff --git a/src/binding/defaultBindings/checked.js b/src/binding/defaultBindings/checked.js index e6125c93b..31128491b 100755 --- a/src/binding/defaultBindings/checked.js +++ b/src/binding/defaultBindings/checked.js @@ -1,3 +1,11 @@ +(function() { + +function checkedValue(element, allBindings) { + return 'checkedValue' in allBindings + ? ko.utils.unwrapObservable(allBindings['checkedValue']) + : element.value; +} + ko.bindingHandlers['checked'] = { 'init': function (element, valueAccessor, allBindingsAccessor) { var updateHandler = function() { @@ -5,7 +13,7 @@ ko.bindingHandlers['checked'] = { if (element.type == "checkbox") { valueToWrite = element.checked; } else if ((element.type == "radio") && (element.checked)) { - valueToWrite = element.value; + valueToWrite = checkedValue(element, allBindingsAccessor()); } else { return; // "checked" binding only responds to checkboxes and selected radio buttons } @@ -14,11 +22,7 @@ ko.bindingHandlers['checked'] = { if ((element.type == "checkbox") && (unwrappedValue instanceof Array)) { // For checkboxes bound to an array, we add/remove the checkbox value to that array // This works for both observable and non-observable arrays - var existingEntryIndex = ko.utils.arrayIndexOf(unwrappedValue, element.value); - if (element.checked && (existingEntryIndex < 0)) - modelValue.push(element.value); - else if ((!element.checked) && (existingEntryIndex >= 0)) - modelValue.splice(existingEntryIndex, 1); + ko.utils.addOrRemoveItem(modelValue, checkedValue(element, allBindingsAccessor()), element.checked); } else { ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true); } @@ -29,19 +33,27 @@ ko.bindingHandlers['checked'] = { if ((element.type == "radio") && !element.name) ko.bindingHandlers['uniqueName']['init'](element, function() { return true }); }, - 'update': function (element, valueAccessor) { + 'update': function (element, valueAccessor, allBindingsAccessor) { var value = ko.utils.unwrapObservable(valueAccessor()); if (element.type == "checkbox") { if (value instanceof Array) { // When bound to an array, the checkbox being checked represents its value being present in that array - element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0; + element.checked = ko.utils.arrayIndexOf(value, checkedValue(element, allBindingsAccessor())) >= 0; } else { - // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish + // When bound to any other value (not an array), the checkbox being checked represents the value being trueish element.checked = value; } } else if (element.type == "radio") { - element.checked = (element.value == value); + element.checked = (checkedValue(element, allBindingsAccessor()) === value); } } }; + +ko.bindingHandlers['checkedValue'] = { + 'update': function (element, valueAccessor) { + element.value = ko.utils.unwrapObservable(valueAccessor()); + } +}; + +})(); \ No newline at end of file