Skip to content

Commit

Permalink
Merge pull request knockout#954 from knockout/954-checkedValue-update
Browse files Browse the repository at this point in the history
Change checkedValue/checked bindings to update value of selection instead of selection
  • Loading branch information
mbest committed May 27, 2013
2 parents d17b1f3 + ffd5c56 commit f3b0932
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 53 deletions.
44 changes: 28 additions & 16 deletions spec/defaultBindings/checkedBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,23 @@ describe('Binding: Checked', function() {
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
// 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]);

// Represents current behavior, that the array is unchanged and the checkbox is unchecked
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(false);
expect(model.values).toEqual([1]);
// 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);

// But the correct behavior might be to keep it checked and update the array
// Implementing this correct behavior will probably require independent bindings (#321) and/or binding ordering
//expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
//expect(model.values).toEqual([3]);
// 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);
});

it('When a \'checkedValue\' is specified, should use that as the radio button\'s value', function () {
Expand Down Expand Up @@ -293,17 +299,23 @@ describe('Binding: Checked', function() {
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
expect(testNode.childNodes[0].childNodes[1].checked).toEqual(false);

// Update the value observable
// 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);

// The current behavior is to uncheck the radio button
expect(testNode.childNodes[0].childNodes[0].checked).toEqual(false);
expect(model.value).toEqual(1);
// 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);

// But the correct behavior might be to keep it checked and update the model "value"
// Implementing this correct behavior will probably require independent bindings (#321) and/or binding ordering
//expect(testNode.childNodes[0].childNodes[0].checked).toEqual(true);
//expect(model.value).toEqual(3);
// 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);
});

});
116 changes: 80 additions & 36 deletions src/binding/defaultBindings/checked.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,97 @@
(function() {

function checkedValue(element, allBindings) {
return allBindings['has']('checkedValue')
? ko.utils.unwrapObservable(allBindings.get('checkedValue'))
: element.value;
}

ko.bindingHandlers['checked'] = {
'after': ['value', 'attr'],
'init': function (element, valueAccessor, allBindings) {
var updateHandler = function() {
var valueToWrite;
if (element.type == "checkbox") {
valueToWrite = element.checked;
} else if ((element.type == "radio") && (element.checked)) {
valueToWrite = checkedValue(element, allBindings);
} else {
return; // "checked" binding only responds to checkboxes and selected radio buttons
function checkedValue() {
return allBindings['has']('checkedValue')
? ko.utils.unwrapObservable(allBindings.get('checkedValue'))
: element.value;
}

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;

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

var modelValue = valueAccessor(), unwrappedValue = ko.utils.unwrapObservable(modelValue);
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
ko.utils.addOrRemoveItem(modelValue, checkedValue(element, allBindings), element.checked);
// 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) {
return;
}

var modelValue = ko.dependencyDetection.ignore(valueAccessor);
if (isValueArray) {
if (oldElemValue !== 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(modelValue, elemValue, true);
ko.utils.addOrRemoveItem(modelValue, oldElemValue, 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(modelValue, elemValue, isChecked);
}
} else {
ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', valueToWrite, true);
ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
}
};
ko.utils.registerEventHandler(element, "click", updateHandler);

// IE 6 won't allow radio buttons to be selected unless they have a name
if ((element.type == "radio") && !element.name)
ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
},
'update': function (element, valueAccessor, allBindings) {
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, checkedValue(element, allBindings)) >= 0;
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());

if (isValueArray) {
// 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;
} else {
// When bound to any other value (not an array), the checkbox being checked represents the value being trueish
element.checked = value;
// For radio buttons, being checked means that the radio button's value corresponds to the model value
element.checked = (checkedValue() === modelValue);
}
} else if (element.type == "radio") {
element.checked = (checkedValue(element, allBindings) === value);
};

var isCheckbox = element.type == "checkbox",
isRadio = element.type == "radio";

// Only bind to check boxes and radio buttons
if (!isCheckbox && !isRadio) {
return;
}

var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array),
oldElemValue = isValueArray ? checkedValue() : undefined,
useCheckedValue = isRadio || isValueArray,
shouldSet = false;

// IE 6 won't allow radio buttons to be selected unless they have a name
if (isRadio && !element.name)
ko.bindingHandlers['uniqueName']['init'](element, function() { return true });

// Set up two computeds to update the binding:

// The first responds to changes in the checkedValue value and to element clicks
ko.dependentObservable(updateModel, null, { disposeWhenNodeIsRemoved: element });
ko.utils.registerEventHandler(element, "click", updateModel);

// The second responds to changes in the model value (the one associated with the checked binding)
ko.dependentObservable(updateView, null, { disposeWhenNodeIsRemoved: element });

shouldSet = true;
}
};
ko.expressionRewriting.twoWayBindings['checked'] = true;
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ ko.utils = (function () {
},

addOrRemoveItem: function(array, value, included) {
var existingEntryIndex = array.indexOf ? array.indexOf(value) : ko.utils.arrayIndexOf(array, value);
var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.peekObservable(array), value);
if (existingEntryIndex < 0) {
if (included)
array.push(value);
Expand Down

0 comments on commit f3b0932

Please sign in to comment.