Skip to content

Tracking changes to complex viewmodels with Knockout.JS

January 12, 2014

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 NiemeyerWhat 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:


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:


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:


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:


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:


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:


if ((typeof obj == "object") && (obj !== null)) {
if (target.hasValueChanged()) {
return ko.mapping.toJS(obj);
}
return getChangesFromModel(obj);
}

view raw

highlight.js

hosted with ❤ by GitHub

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:


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.

Advertisement

From → Knockout.JS, Web API

One Comment
  1. Jason permalink

    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 ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: