Skip to content

Commit

Permalink
Change checked binding to use checkedValue, if present, for checkboxe…
Browse files Browse the repository at this point in the history
…s when not bound to an array.
  • Loading branch information
mbest committed Oct 7, 2015
1 parent 10571c3 commit 07ff098
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 17 deletions.
80 changes: 80 additions & 0 deletions spec/defaultBindings/checkedBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,86 @@ describe('Binding: Checked', function() {
expect(model.value()).toEqual(2);
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);
});

it('Should use that value as the checkbox\'s value when not bound to an array', function () {
var myobservable = ko.observable('random value');
testNode.innerHTML = "<input type='checkbox' data-bind='checked:someProp, " + binding + ":true' />" +
"<input type='checkbox' data-bind='checked:someProp, " + binding + ":false' />";
ko.applyBindings({ someProp: myobservable }, testNode);

expect(myobservable()).toEqual('random value');

// Check initial state: both are unchecked because neither has a matching value
expect(testNode).toHaveCheckedStates([false, false]);

// Update observable; verify element states
myobservable(false);
expect(testNode).toHaveCheckedStates([false, true]);
myobservable(true);
expect(testNode).toHaveCheckedStates([true, false]);

// "check" a box; verify observable and elements
testNode.childNodes[1].click();
expect(myobservable()).toEqual(false);
expect(testNode).toHaveCheckedStates([false, true]);

// "uncheck" a box; verify observable and elements
testNode.childNodes[1].click();
expect(myobservable()).toEqual(undefined);
expect(testNode).toHaveCheckedStates([false, false]);
});

it('Should be able to use observables as value of checkboxes when not bound to an array', function() {
var object1 = {id:ko.observable(1)},
object2 = {id:ko.observable(2)},
model = { value: ko.observable(1), choices: [object1, object2] };
testNode.innerHTML = "<div data-bind='foreach: choices'><input type='checkbox' data-bind='" + binding + ":id, checked:$parent.value' /></div>";
ko.applyBindings(model, testNode);

expect(model.value()).toEqual(1);
expect(testNode.childNodes[0]).toHaveCheckedStates([true, false]);

// Update the value observable of the checked item; should update the selected values and leave checked values unchanged
object1.id(3);
expect(model.value()).toEqual(3);
expect(testNode.childNodes[0]).toHaveCheckedStates([true, false]);

// Update the value observable of the unchecked item; should do nothing
object2.id(4);
expect(model.value()).toEqual(3);
expect(testNode.childNodes[0]).toHaveCheckedStates([true, 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]).toHaveCheckedStates([true, true]);

// Update the value again; should leave checked and replace selected value (other button should be unchecked)
object2.id(4);
expect(model.value()).toEqual(4);
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);

// Revert to original value; should update selected value
object2.id(2);
expect(model.value()).toEqual(2);
expect(testNode.childNodes[0]).toHaveCheckedStates([false, true]);
});

it('Should ignore \'undefined\' value for checkbox', function () {
var myobservable = new ko.observable(true);
testNode.innerHTML = "<input type='checkbox' data-bind='checked: someProp, " + binding + ":undefined' />";
ko.applyBindings({ someProp: myobservable }, testNode);

// ignores 'undefined' value and treats checkbox value as true/false
expect(testNode.childNodes[0].checked).toEqual(true);
myobservable(false);
expect(testNode.childNodes[0].checked).toEqual(false);

ko.utils.triggerEvent(testNode.childNodes[0], "click");
expect(myobservable()).toEqual(true);
ko.utils.triggerEvent(testNode.childNodes[0], "click");
expect(myobservable()).toEqual(false);
});
});
});
});
47 changes: 30 additions & 17 deletions src/binding/defaultBindings/checked.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,65 +11,78 @@ ko.bindingHandlers['checked'] = {
return ko.utils.unwrapObservable(allBindings.get('value'));
}

return element.value;
return useElementValue ? element.value : undefined;
});

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

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

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

var modelValue = ko.dependencyDetection.ignore(valueAccessor);
if (valueIsArray) {
var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue;
if (oldElemValue !== elemValue) {
var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue,
saveOldValue = oldElemValue;
oldElemValue = elemValue;

if (saveOldValue !== elemValue) {
// When we're responding to the checkedValue changing, and the element is
// currently checked, replace the old elem value with the new elem value
// in the model array.
if (isChecked) {
ko.utils.addOrRemoveItem(writableValue, elemValue, true);
ko.utils.addOrRemoveItem(writableValue, oldElemValue, false);
ko.utils.addOrRemoveItem(writableValue, saveOldValue, false);
}

oldElemValue = elemValue;
} else {
// When we're responding to the user having checked/unchecked a checkbox,
// add/remove the element value to the model array.
ko.utils.addOrRemoveItem(writableValue, elemValue, isChecked);
}

if (rawValueIsNonArrayObservable && ko.isWriteableObservable(modelValue)) {
modelValue(writableValue);
}
} else {
if (isCheckbox) {
if (elemValue === undefined) {
elemValue = isChecked;
} else if (!isChecked) {
elemValue = undefined;
}
}
ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
}
};

function updateView() {
// This updates the view value from the model value.
// It runs in response to changes in the bound (checked) value.
var modelValue = ko.utils.unwrapObservable(valueAccessor());
var modelValue = ko.utils.unwrapObservable(valueAccessor()),
elemValue = checkedValue();

if (valueIsArray) {
// When a checkbox is bound to an array, being checked represents its value being present in that array
element.checked = ko.utils.arrayIndexOf(modelValue, checkedValue()) >= 0;
} else if (isCheckbox) {
// When a checkbox is bound to any other value (not an array), being checked represents the value being trueish
element.checked = modelValue;
element.checked = ko.utils.arrayIndexOf(modelValue, elemValue) >= 0;
oldElemValue = elemValue;
} else if (isCheckbox && elemValue === undefined) {
// When a checkbox is bound to any other value (not an array) and "checkedValue" is not defined,
// being checked represents the value being trueish
element.checked = !!modelValue;
} else {
// For radio buttons, being checked means that the radio button's value corresponds to the model value
// Otherwise, being checked means that the checkbox or radio button's value corresponds to the model value
element.checked = (checkedValue() === modelValue);
}
};
Expand All @@ -85,8 +98,8 @@ ko.bindingHandlers['checked'] = {
var rawValue = valueAccessor(),
valueIsArray = isCheckbox && (ko.utils.unwrapObservable(rawValue) instanceof Array),
rawValueIsNonArrayObservable = !(valueIsArray && rawValue.push && rawValue.splice),
oldElemValue = valueIsArray ? checkedValue() : undefined,
useCheckedValue = isRadio || valueIsArray;
useElementValue = isRadio || valueIsArray,
oldElemValue = valueIsArray ? checkedValue() : undefined;

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

0 comments on commit 07ff098

Please sign in to comment.