Creating a smart, dirty flag

http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html

by craigcav

HTML

<script src="http://rniemeyer.github.com/KnockMeOut/Scripts/jquery.tmpl.js"></script>
<script src="http://rniemeyer.github.com/KnockMeOut/Scripts/knockout-latest.debug.js"></script>
<ul data-bind="template: { name: 'itemsTmpl', foreach: items }"></ul>
<hr/>

<script id="itemsTmpl" type="text/html">
    <li data-bind="css: { dirty: dirtyFlag.isDirty }">
        <span data-bind="text: id"></span>
        <input data-bind="value: name" />
        <button data-bind="enable: dirtyFlag.isDirty, click: dirtyFlag.reset">Reset</button>
        <button data-bind="enable: dirtyFlag.hasRollback, click: dirtyFlag.rollback">Rollback</button>
    </li>
</script>
<button data-bind="enable: isDirty, click: save">Save</button>
<hr />
<h3>Just Dirty Items</h3>
<div data-bind="text: ko.toJSON(dirtyItems)"></div>

CSS

li { padding: 2px; margin: 2px; }
input { width: 75px; }
.dirty { border: solid yellow 2px; }

JavaScript

ko.dirtyFlag = function(root, isInitiallyDirty) {
    var result = function() {},
        _state = ko.observable(ko.toJSON(root)),
        _isDirty = ko.observable(isInitiallyDirty);

    result.isDirty = ko.dependentObservable(function() {
        return _isDirty() || _state() !== ko.toJSON(root);
    });

    result.reset = function() {
        _state(ko.toJSON(root));
        _isDirty(false);
        
        if (isInitiallyDirty) {
            isInitiallyDirty = false;
        }
    };

    result.hasRollback = ko.dependentObservable(function() {
        return result.isDirty() && !isInitiallyDirty;
    });

    result.rollback = function() {
        if (!result.hasRollback()) {
            return;
        }
    
        var data = ko.utils.parseJson(_state()), key;

        for (key in data) {
            if (data.hasOwnProperty(key)) {
                if (ko.isObservable(root[key])) {
                    root[key](data[key]);
                }
                else {
                    root[key] = data[key];
                }
            }
        }

        result.reset();
    };

    return result;
};

function Item(id, name, isDirty) {
    this.id = ko.observable(id);
    this.name = ko.observable(name);

    // non observed properties and methods
    this.xx = Math.round(Math.random()*10);
    this.yy = function(){alert(1);}
        
    // NB! to handle changes properly run after initialization
    this.dirtyFlag = ko.dirtyFlag(this, isDirty);
}

var viewModel = {
    items: ko.observableArray([
        new Item(1, "one", true),
        new Item(2, "two"),
        new Item(3, "three", true)
        ]),
    save: function() {
        alert("Sending changes to server: " + ko.toJSON(this.dirtyItems));
    }
};

viewModel.dirtyItems = ko.dependentObservable(function() {
    return ko.utils.arrayFilter(this.items(), function(item) {
        return item.dirtyFlag.isDirty();
    });
}, viewModel);

viewModel.isDirty = ko.dependentObservable(function() {
   ...