Skip to content

Commit 07ff098

Browse files
committed
Change checked binding to use checkedValue, if present, for checkboxes when not bound to an array.
1 parent 10571c3 commit 07ff098

File tree

2 files changed

+110
-17
lines changed

2 files changed

+110
-17
lines changed

spec/defaultBindings/checkedBehaviors.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,86 @@ describe('Binding: Checked', function() {
391391
expect(model.value()).toEqual(2);
392392
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);
393393
});
394+
395+
it('Should use that value as the checkbox\'s value when not bound to an array', function () {
396+
var myobservable = ko.observable('random value');
397+
testNode.innerHTML = "<input type='checkbox' data-bind='checked:someProp, " + binding + ":true' />" +
398+
"<input type='checkbox' data-bind='checked:someProp, " + binding + ":false' />";
399+
ko.applyBindings({ someProp: myobservable }, testNode);
400+
401+
expect(myobservable()).toEqual('random value');
402+
403+
// Check initial state: both are unchecked because neither has a matching value
404+
expect(testNode).toHaveCheckedStates([false, false]);
405+
406+
// Update observable; verify element states
407+
myobservable(false);
408+
expect(testNode).toHaveCheckedStates([false, true]);
409+
myobservable(true);
410+
expect(testNode).toHaveCheckedStates([true, false]);
411+
412+
// "check" a box; verify observable and elements
413+
testNode.childNodes[1].click();
414+
expect(myobservable()).toEqual(false);
415+
expect(testNode).toHaveCheckedStates([false, true]);
416+
417+
// "uncheck" a box; verify observable and elements
418+
testNode.childNodes[1].click();
419+
expect(myobservable()).toEqual(undefined);
420+
expect(testNode).toHaveCheckedStates([false, false]);
421+
});
422+
423+
it('Should be able to use observables as value of checkboxes when not bound to an array', function() {
424+
var object1 = {id:ko.observable(1)},
425+
object2 = {id:ko.observable(2)},
426+
model = { value: ko.observable(1), choices: [object1, object2] };
427+
testNode.innerHTML = "<div data-bind='foreach: choices'><input type='checkbox' data-bind='" + binding + ":id, checked:$parent.value' /></div>";
428+
ko.applyBindings(model, testNode);
429+
430+
expect(model.value()).toEqual(1);
431+
expect(testNode.childNodes[0]).toHaveCheckedStates([true, false]);
432+
433+
// Update the value observable of the checked item; should update the selected values and leave checked values unchanged
434+
object1.id(3);
435+
expect(model.value()).toEqual(3);
436+
expect(testNode.childNodes[0]).toHaveCheckedStates([true, false]);
437+
438+
// Update the value observable of the unchecked item; should do nothing
439+
object2.id(4);
440+
expect(model.value()).toEqual(3);
441+
expect(testNode.childNodes[0]).toHaveCheckedStates([true, false]);
442+
443+
// Update the value observable of the unchecked item to the current model value; should set to checked
444+
object2.id(3);
445+
expect(model.value()).toEqual(3);
446+
expect(testNode.childNodes[0]).toHaveCheckedStates([true, true]);
447+
448+
// Update the value again; should leave checked and replace selected value (other button should be unchecked)
449+
object2.id(4);
450+
expect(model.value()).toEqual(4);
451+
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);
452+
453+
// Revert to original value; should update selected value
454+
object2.id(2);
455+
expect(model.value()).toEqual(2);
456+
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);
457+
});
458+
459+
it('Should ignore \'undefined\' value for checkbox', function () {
460+
var myobservable = new ko.observable(true);
461+
testNode.innerHTML = "<input type='checkbox' data-bind='checked: someProp, " + binding + ":undefined' />";
462+
ko.applyBindings({ someProp: myobservable }, testNode);
463+
464+
// ignores 'undefined' value and treats checkbox value as true/false
465+
expect(testNode.childNodes[0].checked).toEqual(true);
466+
myobservable(false);
467+
expect(testNode.childNodes[0].checked).toEqual(false);
468+
469+
ko.utils.triggerEvent(testNode.childNodes[0], "click");
470+
expect(myobservable()).toEqual(true);
471+
ko.utils.triggerEvent(testNode.childNodes[0], "click");
472+
expect(myobservable()).toEqual(false);
473+
});
394474
});
395475
});
396476
});

src/binding/defaultBindings/checked.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,65 +11,78 @@ ko.bindingHandlers['checked'] = {
1111
return ko.utils.unwrapObservable(allBindings.get('value'));
1212
}
1313

14-
return element.value;
14+
return useElementValue ? element.value : undefined;
1515
});
1616

1717
function updateModel() {
1818
// This updates the model value from the view value.
1919
// It runs in response to DOM events (click) and changes in checkedValue.
2020
var isChecked = element.checked,
21-
elemValue = useCheckedValue ? checkedValue() : isChecked;
21+
elemValue = checkedValue();
2222

2323
// When we're first setting up this computed, don't change any model state.
2424
if (ko.computedContext.isInitial()) {
2525
return;
2626
}
2727

2828
// We can ignore unchecked radio buttons, because some other radio
29-
// button will be getting checked, and that one can take care of updating state.
30-
if (isRadio && !isChecked) {
29+
// button will be checked, and that one can take care of updating state.
30+
// Also ignore value changes to an already unchecked checkbox.
31+
if (!isChecked && (isRadio || ko.computedContext.getDependenciesCount())) {
3132
return;
3233
}
3334

3435
var modelValue = ko.dependencyDetection.ignore(valueAccessor);
3536
if (valueIsArray) {
36-
var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue;
37-
if (oldElemValue !== elemValue) {
37+
var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue,
38+
saveOldValue = oldElemValue;
39+
oldElemValue = elemValue;
40+
41+
if (saveOldValue !== elemValue) {
3842
// When we're responding to the checkedValue changing, and the element is
3943
// currently checked, replace the old elem value with the new elem value
4044
// in the model array.
4145
if (isChecked) {
4246
ko.utils.addOrRemoveItem(writableValue, elemValue, true);
43-
ko.utils.addOrRemoveItem(writableValue, oldElemValue, false);
47+
ko.utils.addOrRemoveItem(writableValue, saveOldValue, false);
4448
}
45-
46-
oldElemValue = elemValue;
4749
} else {
4850
// When we're responding to the user having checked/unchecked a checkbox,
4951
// add/remove the element value to the model array.
5052
ko.utils.addOrRemoveItem(writableValue, elemValue, isChecked);
5153
}
54+
5255
if (rawValueIsNonArrayObservable && ko.isWriteableObservable(modelValue)) {
5356
modelValue(writableValue);
5457
}
5558
} else {
59+
if (isCheckbox) {
60+
if (elemValue === undefined) {
61+
elemValue = isChecked;
62+
} else if (!isChecked) {
63+
elemValue = undefined;
64+
}
65+
}
5666
ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
5767
}
5868
};
5969

6070
function updateView() {
6171
// This updates the view value from the model value.
6272
// It runs in response to changes in the bound (checked) value.
63-
var modelValue = ko.utils.unwrapObservable(valueAccessor());
73+
var modelValue = ko.utils.unwrapObservable(valueAccessor()),
74+
elemValue = checkedValue();
6475

6576
if (valueIsArray) {
6677
// When a checkbox is bound to an array, being checked represents its value being present in that array
67-
element.checked = ko.utils.arrayIndexOf(modelValue, checkedValue()) >= 0;
68-
} else if (isCheckbox) {
69-
// When a checkbox is bound to any other value (not an array), being checked represents the value being trueish
70-
element.checked = modelValue;
78+
element.checked = ko.utils.arrayIndexOf(modelValue, elemValue) >= 0;
79+
oldElemValue = elemValue;
80+
} else if (isCheckbox && elemValue === undefined) {
81+
// When a checkbox is bound to any other value (not an array) and "checkedValue" is not defined,
82+
// being checked represents the value being trueish
83+
element.checked = !!modelValue;
7184
} else {
72-
// For radio buttons, being checked means that the radio button's value corresponds to the model value
85+
// Otherwise, being checked means that the checkbox or radio button's value corresponds to the model value
7386
element.checked = (checkedValue() === modelValue);
7487
}
7588
};
@@ -85,8 +98,8 @@ ko.bindingHandlers['checked'] = {
8598
var rawValue = valueAccessor(),
8699
valueIsArray = isCheckbox && (ko.utils.unwrapObservable(rawValue) instanceof Array),
87100
rawValueIsNonArrayObservable = !(valueIsArray && rawValue.push && rawValue.splice),
88-
oldElemValue = valueIsArray ? checkedValue() : undefined,
89-
useCheckedValue = isRadio || valueIsArray;
101+
useElementValue = isRadio || valueIsArray,
102+
oldElemValue = valueIsArray ? checkedValue() : undefined;
90103

91104
// IE 6 won't allow radio buttons to be selected unless they have a name
92105
if (isRadio && !element.name)

0 commit comments

Comments
 (0)