Tracking changes to complex viewmodels with Knockout.JS
As part of a project I’ve been working on for a client, we’ve decided to implement HTTP PATCH in our API for making changes. The main client consuming the API is a web application driven by Knockout.JS, so this meant we had to find a way to figure out what had changed on our view model, and then send just those values over the wire.
There is nothing new or exciting about this requirement in itself. The question has been posed before and it was the subject of a blog post way back in 2011 by Ryan Niemeyer. What was quite exciting however was that our solution ended up doing much more than just detect changes to viewmodels. We needed to keep tabs on individual property changes, changes to arrays (adds\deletes\modifications), changes to child objects and even changes to child objects nested within arrays. The result was a complete change tracking implementation for knockout that can process not just one object but a complete object graph.
In this two part post I’ll attempt to share the code, the research and the story of how we arrived at the final implementation.
Identifying that a change has occurred
The first step was to get basic change tracking working given a view model with observable properties containing values – no complex objects.
Initial googling turned up the following approach as a starting point:
http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html
http://www.johnpapa.net/spapost10/
http://www.dotnetcurry.com/showarticle.aspx?ID=876
These methods all involved some variation on adding an isDirty computed observable to your view model. Ryan’s example stores the initial state of the object when it is defined which can then be used as a point of comparison to figure out if a change has occurred.
Suprotim’s approach is based on Ryan’s method but instead of storing a json snapshot of the initial object (which could potentially be very large for complex view models), it merely subscribes to all the observable properties of the view model and sets the isDirty flag accordingly.
Both of these are very lightweight and efficient ways of detecting that a change has occurred, but as detailed in this thread they can’t pinpoint exactly which observable caused the change. Something more was needed.
Tracking changes to simple values
After a bit more digging, a clever solution to the problem of tracking changes to individual properties emerged as described by Stack Overflow one hit wonder, Brett Green in the answer to this question and also in slightly more detail on his blog.
This made the use of knockout extenders to add properties to the observables themselves; an overall isDirty() method for the view model as a whole could then be provided by a computed observable. This post almost entirely formed the basis for the first version. After a bit of restructuring, pretty soon we’ve got an implementation that will allow us to track changes to a flat view model:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var getObjProperties = function (obj) { | |
var objProperties = []; | |
var val = ko.utils.unwrapObservable(obj); | |
if (val !== null && typeof val === 'object') { | |
for (var i in val) { | |
if (val.hasOwnProperty(i)) objProperties.push({ "name": i, "value": val[i] }); | |
} | |
} | |
return objProperties; | |
}; | |
ko.extenders.trackChange = function (target, track) { | |
if (track) { | |
target.isDirty = ko.observable(false); | |
target.originalValue = target(); | |
target.subscribe(function (newValue) { | |
// use != not !== so numbers will equate naturally | |
target.hasValueChanged(newValue != target.originalValue); | |
target.hasValueChanged.valueHasMutated(); | |
}); | |
} | |
return target; | |
}; | |
var applyChangeTrackingToObservable = function (observable) { | |
// Only apply to basic writeable observables | |
if (observable && !observable.nodeType && !observable.refresh && ko.isObservable(observable)) { | |
if (!observable.isDirty) observable.extend({ trackChange: true }); | |
} | |
}; | |
var applyChangeTracking = function (obj) { | |
var properties = getObjProperties(obj); | |
ko.utils.arrayForEach(properties, function (property) { | |
applyChangeTrackingToObservable(property.value); | |
}); | |
}; | |
var getChangesFromModel = function (obj) { | |
var changes = null; | |
var properties = getObjProperties(obj); | |
ko.utils.arrayForEach(properties, function (property) { | |
if (property.value != null && typeof property.value.isDirty != "undefined" && property.value.isDirty()) { | |
changes = changes || {}; | |
changes[property.name] = property.value(); | |
} | |
}); | |
return changes; | |
}; |
An example of utilising this change tracking is as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var viewModel = { | |
Name: ko.observable("Pete"), | |
Age: ko.observable(29), | |
Occupation: ko.observable("Developer") | |
}; | |
applyChangeTracking(viewModel); | |
viewModel.Occupation("Unemployed"); | |
getChangesFromModel(viewModel); | |
// -> { "Occupation": "Unemployed" } |
Detecting changes to complex objects
The next task was to ensure we could work with properties containing complex objects and nested observables. The issue here is that the isDirty property of an observable is only set when it’s contents are replaced. Modifying a child property of an object within an observable will not trigger the change tracking.
This thread on google groups seemed to be going in the right direction and even had links to two libraries already built:
- Knockout-Rest seemed promising, but although this was able to detect changes in complex properties and even roll them back, it still could not pinpoint the individual properties that triggered the change.
- EntitySpaces.js seemed to contain all the required elements, but it relied on generated classes and the change tracking features were too tightly coupled to it’s main use as a data access framework. At the time of writing it had not been updated for two years.
In the end we came up with a solution ourselves. In order to detect that a change had occurred further down the graph, we modified the existing isDirty extension member so that in the event that the value of our observable property was a complex object, it should also take into account the isDirty value of any properties of that child object:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var traverseObservables = function (obj, action) { | |
ko.utils.arrayForEach(getObjProperties(obj), function (observable) { | |
if (observable && observable.value && !observable.value.nodeType && ko.isObservable(observable.value)) { | |
action(observable); | |
} | |
}); | |
}; | |
ko.extenders.trackChange = function (target, track) { | |
if (track) { | |
target.hasValueChanged = ko.observable(false); | |
target.hasDirtyProperties = ko.observable(false); | |
target.isDirty = ko.computed(function () { | |
return target.hasValueChanged() || target.hasDirtyProperties(); | |
}); | |
var unwrapped = target(); | |
if ((typeof unwrapped == "object") && (unwrapped !== null)) { | |
traverseObservables(unwrapped, function (obj) { | |
applyChangeTrackingToObservable(obj.value); | |
obj.value.isDirty.subscribe(function (isdirty) { | |
if (isdirty) target.hasDirtyProperties(true); | |
}); | |
}); | |
} | |
target.originalValue = target(); | |
target.subscribe(function (newValue) { | |
// use != not !== so numbers will equate naturally | |
target.hasValueChanged(newValue != target.originalValue); | |
target.hasValueChanged.valueHasMutated(); | |
}); | |
} | |
return target; | |
}; |
Now when extending an observable to apply change tracking, if we find that the initial value is a complex object we also iterate over any properties of our child object and recursively apply change tracking to those observables as well. We also set up subscriptions to the resulting isDirty flags of the child properties to ensure we set the hasDirtyProperties flag on the target.
Tracking individual changes within complex objects
After the previous modifications, our change tracking now behaves like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var viewModel = { | |
Name: ko.observable("Pete"), | |
Age: ko.observable(29), | |
Skills: { | |
Tdd: ko.observable(true), | |
Knockout: ko.observable(true), | |
ChangeTracking: ko.observable(false), | |
}, | |
Occupation: ko.observable("Developer") | |
}; | |
applyChangeTracking(viewModel); | |
viewModel.Skills().ChangeTracking(true); | |
getChangesFromModel(viewModel); | |
/* -> { | |
"Skills": { | |
Tdd: function observable() { …. }, | |
Knockout: function observable() { …. }, | |
ChangeTracking: function observable() { …. }, | |
} | |
} */ |
Obviously there’s something missing here… we know that the Skills object has been modified and we also technically know which property of the object was modified but that information isn’t being respected by getChangesFromModel.
Previously it was sufficient to pull out changes by simply returning the value of each observable. That’s no longer the case so we have to add a getChanges method to our observables at the same level as isDirty, and then use this instead of the raw value when building our change log:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ko.extenders.trackChange = function (target, track) { | |
if (track) { | |
// … | |
// … | |
if (!target.getChanges) { | |
target.getChanges = function (newObject) { | |
var obj = target(); | |
if ((typeof obj == "object") && (obj !== null)) { | |
if (target.hasValueChanged()) { | |
return ko.mapping.toJS(obj); | |
} | |
return getChangesFromModel(obj); | |
} | |
return target(); | |
}; | |
} | |
} | |
return target; | |
}; | |
var getChangesFromModel = function (obj) { | |
var changes = null; | |
var properties = getObjProperties(obj); | |
ko.utils.arrayForEach(properties, function (property) { | |
if (property.value != null && typeof property.value.isDirty != "undefined" && property.value.isDirty()) { | |
changes = changes || {}; | |
changes[property.name] = property.value.getChanges(); | |
} | |
}); | |
return changes; | |
}; |
Now our getChangesFromModel will operate recursively and produce the results we’d expect. I’d like to draw your attention to this section of the above code in particular:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if ((typeof obj == "object") && (obj !== null)) { | |
if (target.hasValueChanged()) { | |
return ko.mapping.toJS(obj); | |
} | |
return getChangesFromModel(obj); | |
} |
There’s a reason we’ve been using seperate observables to track hasValueChanged and hasDirtyProperties; in the event that we have replaced the contents of the observable wholesale, we must pull out all the values.
Here’s the change tracking complete with complex objects in action:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var viewModel = { | |
Name: ko.observable("Pete"), | |
Age: ko.observable(29), | |
Skills: ko.observable({ | |
Tdd: ko.observable(true), | |
Knockout: ko.observable(true), | |
ChangeTracking: ko.observable(false), | |
Languages: ko.observable({ | |
Csharp: ko.observable(false), | |
Javascript: ko.observable(false) | |
}), | |
}), | |
Occupation: ko.observable("Developer") | |
}; | |
applyChangeTracking(viewModel); | |
viewModel.Skills().ChangeTracking(true); | |
viewModel.Skills().Languages({ | |
Csharp: ko.observable(true), | |
Javascript: ko.observable(true), | |
}); | |
getChangesFromModel(viewModel); | |
/* -> { | |
"Skills": { | |
ChangeTracking: true, | |
Languages: { | |
Csharp: true, | |
Javascript: true | |
} | |
} | |
} */ |
Summary
In this post we’ve seen how we can use a knockout extender and an isDirty observable to detect changes to individual properties within a view model. We’ve also seen some of the potential pitfalls you may encounter when dealing with nested complex objects and how we can overcome these to provide a robust change tracking system.
In the second part of this post, we’ll look at the real killer feature… tracking changes to complex objects within arrays.
You can view the full code for the finished example here: https://gist.github.com/Roysvork/8744757 or play around with the jsFiddle!
Pete
Edit: As part of the research for this post, I did come across https://github.com/ZiadJ/knockoutjs-reactor which takes a very similar approach and even handles arrays. It’s a shame I had not seen this when writing the code as it would have been quite useful.
What if I want to implement a “Save” functionality? – after the save the changes the user made should now be considered as “clean”. How do I do that ?