Question

Let's put it short: This is my knockout custom binding for putting a checkbox in indeterminate state.

ko.bindingHandlers.nullableChecked = {
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (value == null) element.indeterminate = true;
        ko.bindingHandlers.checked.update(element, function () { return value; });
    }
};

If the initial value is null everything works fine and checkbox is put in indeterminate state but when I click the checkbox it doesn't seem to update the bound property's value to false/true accordingly. Am I missing something?

Was it helpful?

Solution 3

You're not calling Init.

Simply proxy the init function for checked in your nullableChecked init function like you did in the update.

ko.bindingHandlers.nullableChecked = {
    init: function(element, valueAccessor) {
          ko.bindingHandlers.checked.init(element, valueAccessor);
    },
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (value == null) element.indeterminate = true;
        ko.bindingHandlers.checked.update(element, valueAccessor);
    }
};

Without the init, it's never actually setting up a "click" binding on the checkbox to tell knockout that something has changed. If you look at the debug code (http://knockoutjs.com/downloads/knockout-2.2.1.debug.js), you'll see that init uses jQuery to set up a 'click' event on the checkbox to update the observable when the value changes.

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;
        } else {
            return; // "checked" binding only responds to checkboxes and selected radio buttons
        }

        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
            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);
        } else {
            ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, 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 });
},

Edit: Here's a Fiddle: http://jsfiddle.net/cclose/NFfVn/

OTHER TIPS

If anyone is here looking to have a select all type checkbox show as intermediate if not all of its sub items are checked, you can use a simple binding like this

ko.bindingHandlers.indeterminateValue = {
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        element.indeterminate = value;
    }
};

then you can use it like this

 <input type="checkbox" data-bind="checked: selectedAllProduce, indeterminateValue: selectedProduce().length > 0 && selectedProduce().length < produce.length" title="Select all/none"/>

Here is a fiddle showing it working on the computed observable select all example https://jsfiddle.net/deannorth/crqwac12/

The answer by @Fodagus works with Knockout <= 2.3.0, but in Knockout >= 3.0.0, ko.bindingHandlers.checked.update is undefined.

We found that this works for us in Knockout 3.1.0:

    ko.bindingHandlers.nullableChecked = {
        init: function (element, valueAccessor) {
            ko.bindingHandlers.checked.init(element, valueAccessor);
        },
        update: function (element, valueAccessor) {
            var value = ko.utils.unwrapObservable(valueAccessor());
            if (value == null) {
                element.indeterminate = true;
            }
            else {
                element.indeterminate = false;
            }
        }
    };

Are we missing anything?

In the new versions of knockout 3.0.0 and above. The update method in the checkbox binding handler is removed. Therefore you should create a click event in the init function to trigger the update method whenever the checkbox is clicked. Below is an example of a binding handler to transform a jquery checkbox into a uniform checkbox.

ko.bindingHandlers.checkedUniform =
{
    init: function (element, valueAccessor) {
        ko.bindingHandlers.checked.init(element, valueAccessor);
        $(element).uniform().on('click', ko.bindingHandlers.checkedUniform.update);
    },
    update: function (element, valueAccessor) 
    {
        ko.bindingHandlers.checked.update(element, valueAccessor);
        $.uniform.update($(element));
    }
};

The checked handler receives an accessor to the value that was stored, not the observable that was holding the value. So the observable never gets any value changes caused by the handler. I think you should be able to just pass in the value accessor as is.

ko.bindingHandlers.nullableChecked = {
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (value == null) element.indeterminate = true;
        ko.bindingHandlers.checked.update(element, valueAccessor);
    }
};
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top