Protected Observable

A knockout extension for providing "transactional" properties for observables.

by Areson

HTML

<script src="https://github.com/SteveSanderson/knockout/raw/master/build/output/knockout-latest.debug.js"></script>
<script src="https://raw.github.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.debug.js"></script>
<input type="text" data-bind="value: observe"></input>
<button data-bind="click: observe.reset">Reset</button>
<button data-bind="click: observe.commit">Commit</button>


The value is <span data-bind="text: observe"></span>

<hr/>
<h3>List Example</h3>

<input data-bind="value: listAdd"></input>
<button data-bind="click: addToList">Add to List</button>
<button data-bind="click: observeArray.reset">Reset</button>
<button data-bind="click: observeArray.commit">Commit</button>
<button data-bind="click: emptyArray">Remove All</button>

<table data-bind="foreach: observeArray()">
    <tr>
        <td data-bind="text: $data"></td>
    </tr>
</table>
    
<hr/>
Complex Observable Example
<label>ID: <input data-bind="value: complexObserve().id"/></label>
<label>Value: <input data-bind="value: complexObserve().value"/></label>

<button data-bind="click: complexObserve.reset">Reset</button>
<button data-bind="click: complexObserve.commit">Commit</button>
<br/>
ID: <span data-bind="text: complexObserve().id"></span>
<br/>Value: <span data-bind="text: complexObserve().value"></span>

JavaScript

ko.extenders.protected = function(target, options) {
    var _actual = target;
    var _deepObjects = [];

    //Setup how we will "copy" the values 
    //back and forth
    var copyValue = function(value) {
        if (_actual.removeAll) 
            return ($.extend(true, [], value));
        else if ($.isPlainObject(_actual()))
            return $.extend({}, value);
        else return (value);
    }

    //Are we doing "deep" protection?
    if(options.deep)
    {
        //Get the underlying object
        var object = _actual();
        
        //Iterate over all observables and protect them
        for(var name in object)
        {
            if(ko.isObservable(object[name]))
            {
                //Make the observable protected
                object[name] = ko.extenders.protected(object[name], options);
                //Save the observable to our "protected" array
                _deepObjects.push(object[name])
            }
        }
    }
    
    //Hold onto the intermediate value in between commits
    var _cachedValue = copyValue(target());

    //Setup a dependent variable to control
    //access to our observable
    var result = ko.computed({
        read: function() {
            return (_actual());
        },

        write: function(value) {
            _cachedValue = value;
        }
    });

    var _deepCommit = function() {
        for(var i in _deepObjects)
            _deepObjects[i].commit();
    }
    
    var _deepReset = function() {
        for(var i in _deepObjects)
            _deepObjects[i].reset();        
    }
        
    //Commit the changes to the observable
    result.commit = function() {
        if($.isPlainObject(_actual()))
        {
            if(options.deep && _deepObjects.length > 0)
                _deepCommit();
            else
                _actual(copyValue(_cachedValue));
        }
        else
            _actual(copyValue(_cachedValue));
    }

    result.reset = function() {
       ...