//not used in this example. one time flag, that drops its subscriptions after the first change. ko.oneTimeDirtyFlag = function (root) { var _initialized; //one-time dirty flag that gives up its dependencies on first change var result = ko.computed(function () { if (!_initialized) { //just for subscriptions ko.toJS(root); //next time return true and avoid ko.toJS _initialized = true; //on initialization this flag is not dirty return false; } //on subsequent changes, flag is now dirty return true; }); return result; }; ko.dirtyFlag = function(root, isInitiallyDirty) { var result = function() {}, _initialState = ko.observable(ko.toJSON(root)), _isInitiallyDirty = ko.observable(isInitiallyDirty); result.isDirty = ko.computed(function() { return _isInitiallyDirty() || _initialState() !== ko.toJSON(root); }); result.reset = function() { _initialState(ko.toJSON(root)); _isInitiallyDirty(false); }; return result; }; function Item(id, name) { this.id = ko.observable(id); this.name = ko.observable(name); this.dirtyFlag = new ko.dirtyFlag(this); } var ViewModel = function(items) { this.items = ko.observableArray([ new Item(1, "one"), new Item(2, "two"), new Item(3, "three") ]); this.save = function() { alert("Sending changes to server: " + ko.toJSON(this.dirtyItems)); }; this.dirtyItems = ko.computed(function() { return ko.utils.arrayFilter(this.items(), function(item) { return item.dirtyFlag.isDirty(); }); }, this); this.isDirty = ko.computed(function() { return this.dirtyItems().length > 0; }, this); }; ko.applyBindings(new ViewModel());
<ul data-bind="foreach: items"> <li data-bind="css: { dirty: dirtyFlag.isDirty }"> <span data-bind="text: id"></span> <input data-bind="value: name" /> <button data-bind="click: dirtyFlag.reset">Reset</button> </li> </ul> <button data-bind="enable: isDirty, click: save">Save</button> <hr /> <h3>Just Dirty Items</h3> <div data-bind="text: ko.toJSON(dirtyItems)"></div>
li { padding: 2px; margin: 2px; } input { width: 75px; } .dirty { border: solid yellow 2px; }