2

I have a checkbox which previously was bound directly to an observable property on my view model. I use a generic dirty flag on all view models which watches all observable properties on the view model for changes.

Now for one property I wish to present a confirmation alert if the user attempts to uncheck the checkbox before unsetting the underlying observable. I have tried to implement this as a writeable computed observable which wraps the observable like so:

function VM() {
    var self = this;

    self.SelectedInternal = ko.observable(false);

    self.selected= ko.computed({
        read: function () {
            return self.SelectedInternal();
        },
        write: function (value) {
            alert('Clicked'); // To demonstrate problem
            if (value === false) {
                if (confirm('Are you sure you wish to deselect this option?')) {
                    self.SelectedInternal(value);
                }          
            }
        }
    });
}

var vm = new VM();

ko.applyBindings(vm);

What I'm seeing (in both Firefox and IE) is that when I default the SelectedInternal observable value to false as above, then the "selected" write function only fires each time that I check the checkbox, not when I uncheck it. If I default the SelectedInternal observable value to true, then the first time I uncheck it the write setter executes, but not on subsequent unchecks.

Here's a fiddle to demonstrate:

http://jsfiddle.net/xHqsZ/18/

What is happening here? Is there a better way to achieve this?

UPDATE: This approach probably isn't going to work anyway because I can't get a hook into the original click to return false on, and resetting the observable to true if the user chose Cancel in the confirm box doesn't seem to take effect. But I'd still like to know why the computed setter itself isn't behaving as expected.

Tom W Hall
  • 5,273
  • 4
  • 29
  • 35

4 Answers4

2

I have so far achieved this thus (properties renamed for simplicity):

<input type="checkbox" data-bind="checked: vm.optionComputed, click: vm.toggleOption" />

And in the view model:

self.optionComputed = ko.computed({
    read: function () {
        return self.Option();
    },
    write: function (value) {
    }
});

self.toggleOption = function (vm, event) {
    var checkBox = $(event.target);
    var isChecked = checkBox.is(':checked');

    if (!(!isChecked && !confirm('Are you sure?'))) {
        self.Option(isChecked);
    }    
};

There's a slight glitchiness in that when you choose OK to unselect, the checkbox (which has already been blanked out by the click) briefly appears checked again before finally unchecking. But the behaviour in terms of preventing the observable changing until confirmation is correct.

Tom W Hall
  • 5,273
  • 4
  • 29
  • 35
  • That is one approach. This is a situation where a custom binding will work better. I have a partially working answer, at http://jsfiddle.net/photo_tom/xHqsZ/31/. I need to spend a little more time on it, I have to leave for my day job. I'll have a working sample for you later today. – photo_tom Dec 12 '12 at 12:24
1

Take a look at documentation at http://knockoutjs.com/documentation/computedObservables.html.

The write function is called when the value from the read function is changed. See Example #1 in the documentation.

One thing that I've done in the write function, is to set other observable values. For example, one check box to clear all others in a group. I did this by just updating those observables inside the write function.


Edit

I've put together a fiddle showing how to do what is described in last paragraph - http://jsfiddle.net/photo_tom/xHqsZ/22/. The computed function that makes it work is

self.clearAll = ko.computed({
    read: function() {
        return !(self.option1() || self.option2() || self.option3());
    },
    write: function(value) {
        alert('Clicked');
        self.option1(false);
        self.option2(false);
        self.option3(false);

    }
});

Edit #2 To answer comment about wanting to manually confirm false to true checked state question.

The cleanest way to handle this is with a custom binder. The key section is registering a custom changed event handler. Inside of that function, you can then ask if user wants to really set checkbox to true.

ko.bindingHandlers.checkConfirm = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function() {
            // get the current observable value.
            var value = valueAccessor();
            var valueUnwrapped = ko.utils.unwrapObservable(value);

            if (!valueUnwrapped && element.checked) {
                var r = confirm("Confirm Setting value to true");
                if (r) {
                    value(true);
                    return;
                } else {
                    // not okayed, so clear cb.
                    element.checked = false;
                }
            }
            value(false);
        });
    },
    update: function(element, valueAccessor) {
        // use default ko code to update checkbox
        ko.bindingHandlers.checked.update(element, valueAccessor);
    }
};

Updated fiddle is at http://jsfiddle.net/photo_tom/xHqsZ/32/

photo_tom
  • 7,292
  • 14
  • 68
  • 116
  • Thanks, but could you please fork that fiddle to demonstrate how you'd achieve a confirm alert with this technique? I have been experimenting with a separate click binding as well as the checked binding, but I haven't been able to get it right yet. – Tom W Hall Dec 10 '12 at 02:21
  • Thanks, but that doesn't address my scenario - I have only one checkbox and it's when that checkbox is unchecked that I want to seek confirmation, before changing the underlying observable. – Tom W Hall Dec 11 '12 at 20:18
  • See my answer. I'm open to anything nicer :-) – Tom W Hall Dec 12 '12 at 02:43
0

I ran up against this same issue, although under different circumstances. (I was attempting to do a mass update on a list with the check box.) After looking at the answers here I decided to abandon the computed checked binding, and instead I used an observable for the binding, and a subscription to do my update:

$(document).ready(function() {
    function VM() {
        var self = this;
        self.items = ko.observableArray([
            { name: "Foo", active: ko.observable(true) },
            { name: "Bar", active: ko.observable(true) },
            { name: "Bas", active: ko.observable(true) }
        ]);
        self.allActive = ko.observable(true);
        self.allActive.subscribe(function(value) {
            if(self.allActiveCanceled) {
                self.allActiveCanceled = false;
                return;
            }
            if(!confirm('Really?')) {
                window.setTimeout(function() {
                    self.allActiveCanceled = true;
                    self.allActive(!value);
                }, 1);
                return;
            }
            var items = self.items();
            for(var i = 0, l = items.length; i < l; i++) {
                items[i].active(value);   
            }
        });
        self.allActiveCanceled = false;
    }
    var vm = new VM();
    ko.applyBindings(vm);
});

Here is a fiddle with the associated markup: http://jsfiddle.net/makeblake/dWNLA/

The setTimeout and cancelled flag feels like a bit of a hack, but it gets the job done.

Blake Mitchell
  • 2,647
  • 1
  • 24
  • 22
0

I would also suggest using a custom knockout binding, but make sure to re-use / inherit the complete ko.bindingHandlers.checked binding functionality to profit from it's legacy browser's compatibility handling:

ko.bindingHandlers.confirmedChecked = {
    'after': ['value', 'attr'],
    'init': function (element, valueAccessor, allBindings)
    {
      ko.utils.registerEventHandler(
        element,
        'click',
        function(event)
        {
            if (
                element.checked &&
                !confirm('Are you sure you want to enable this setting?')
            )
            {
                if (event.stopImmediatePropagation)
                {
                    event.stopImmediatePropagation();
                }

                element.checked = false;
            }
        }
    );

    ko.bindingHandlers.checked.init(element, valueAccessor, allBindings);
    }
};
Stefan
  • 1,028
  • 1
  • 9
  • 7