change tracking

by rniemeyer

HTML

<script src="http://rniemeyer.github.com/KnockMeOut/Scripts/knockout-1.3pre.js"></script>
<input type="checkbox" data-bind="checked: someFlag" />
<hr/>
<ul data-bind="foreach: items">
    <li>
        <input data-bind="value: id" />
        <input data-bind="value: name" />
    </li>
</ul>
            
<hr />

<table data-bind="visible: myTracker.changes().length">
    <tr>
        <th>Property</th>
        <th>New Value</th>
        <th>Source Object</th>
    </tr>
    <tbody data-bind="foreach: myTracker.changes">
        <tr>
            <td data-bind="text: property"></td>
            <td data-bind="text: ko.toJSON(value)"></td>
            <td data-bind="text: ko.toJSON(source)"></td>
        </tr>
    </tbody>
</table>

CSS

.val { font-weight: bold; }
td, th { margin: 2px; padding: 2px; border: solid 1px black; }
th { background-color: #333; color: #fff; }

JavaScript

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

var viewModel = {
    items: ko.observableArray([
       new Item(1, "one"),
       new Item(2, "two"),
       new Item(3, "three")
       ]),
    someFlag: ko.observable(false)
};


ko.changeTracker = function(viewModel) {
   var self = this;
   self.changes = ko.observableArray();
   
   //add this change to the array of changes
   function onPropertyChanged(property, source) {
       self.changes.push({ property: property, value: ko.toJS(source[property]), source: source });
   }
    
   //track a single change
   function trackChange(prop, source) { 
       var value = source[prop];
        if (ko.isObservable(value)) {
            value.subscribe(function() {
               onPropertyChanged(prop, source);  
            }); 
        }  
   }
       
   //expose a function to track changes for each property on an object
   self.trackChanges = function(model) {
        for (var prop in model) {
            if (model.hasOwnProperty(prop)) {
               trackChange(prop, model);
               
               var underlying = ko.utils.unwrapObservable(model[prop]);
                if (underlying instanceof Array) {
                    ko.utils.arrayForEach(underlying, function(item) {
                       self.trackChanges(item);   
                    })
                } 
                else if (typeof underlying === "object") {
                    self.trackChanges(underlying);
                }
            } 
        }  
   }
}

var myTracker = new ko.changeTracker();
myTracker.trackChanges(viewModel);

ko.applyBindings(viewModel);