javascriptknockout.jsknockout-3.0computed-observable

knockout.js how to set selected option after confirm() cancelled within a ko.computed write


i have a selector element with options and default text:

self._selected = ko.observable();
self.option = ko.computed({
    read:function(){
        return self._selected;
    },
    write: function(data){
        if(data){
            if(confirm('are you sure?')){
                self._selected(data);
            }else{
                //reset
            }
        }
    }
});

<select data-bind="options: options, value:option, optionsCaption: 'choose ...'"></select>

the problem this:

it should be "choose ..."

jsbin here, it was tested on chrome only


Solution

  • There is an asymmetry here:

    When you change the value of a select box, the DOM gets updated immediately and knockout afterwards (of course, knockout depends on the DOM change event). So when your code asks "Are you sure?", the DOM already has the new value.

    Now, when you do not write that value to the observable bound to value:, the viewmodel's state does not change. And knockout only updates the DOM when an observable changes. So the DOM stays at the selected value, and the bound value in your viewmodel is different.


    The easiest way out of this is to save the old value in a variable, always write the new value to the observable, and simply restore the old value if the user clicks "no". This way the asymmetry is broken and the DOM and the viewmodel stay in sync.

    var AppData = function(params) {
        var self = {};
        var selected = ko.observable();
        
        self.options = ko.observableArray(params.options);  
        self.option = ko.computed({
            read: selected,
            write: function(value) {
                var oldValue = selected();
                selected(value);
                if (value !== oldValue && !confirm('are you sure?')) {
                    selected(oldValue);
                }
            }
        });
      
        return self;
    };
    
    // ----------------------------------------------------------------------
    ko.applyBindings(new AppData({
      options: ['one','two','three']
    }));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <select data-bind="options: options, value: option, optionsCaption: 'Select...'"></select>
    
    <hr>
    <pre data-bind="text: ko.toJSON($root, null, 2)"></pre>


    This is a perfect candidate for a knockout extender that asks for value change confirmation. This way we can re-use it for different observables and keep the viewmodel clean.

    ko.extenders.confirmChange = function (target, message) {
        return ko.pureComputed({
            read: target,
            write: function(newValue) {
                var oldValue = target();
                target(newValue);
                if (newValue !== oldValue && !confirm(message)){
                    target(oldValue);
                }
            }
        });
    };
    
    // ----------------------------------------------------------------------
    var AppData = function(params) {
        var self = this;
        
        self.options = ko.observableArray(params.options);  
        self.option = ko.observable().extend({confirmChange: 'are you sure?'});
    };
    
    // ----------------------------------------------------------------------
    ko.applyBindings(new AppData({
      options: ['one','two','three']
    }));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <select data-bind="options: options, value: option, optionsCaption: 'Select...'"></select>
    
    <hr>
    <pre data-bind="text: ko.toJSON($root, null, 2)"></pre>