Skip to content

Commit

Permalink
Add checkedValue binding that works with the checked binding to allow…
Browse files Browse the repository at this point in the history
… for arbitrary values for checkboxes and radio buttons.
  • Loading branch information
mbest committed Mar 6, 2013
1 parent 4684034 commit 4db9249
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 10 deletions.
103 changes: 103 additions & 0 deletions spec/defaultBindings/checkedBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<input type='checkbox' data-bind='checked:myArray, checkedValue:1' />"
+ "<input value='off' type='checkbox' data-bind='checked:myArray, checkedValue:2' />";
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 = "<div data-bind='foreach: choices'><input type='checkbox' data-bind='checked:$parent.values, checkedValue:$data' /></div>";
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 = "<div data-bind='foreach: choices'><input type='checkbox' data-bind='checked:$parent.values, checkedValue:id' /></div>";
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 = "<input type='radio' data-bind='checked:someProp, checkedValue:true' />" +
"<input type='radio' data-bind='checked:someProp, checkedValue:false' />";
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);
});
});
32 changes: 22 additions & 10 deletions src/binding/defaultBindings/checked.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
(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() {
var valueToWrite;
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
}
Expand All @@ -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);
}
Expand All @@ -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());
}
};

})();

0 comments on commit 4db9249

Please sign in to comment.