Automatic Knockout model persistence with Amplify

Lately I’ve been using Knockout JS to build MVVM applications. I wanted to add some form of “offline” caching to my Model. I decided to use Amplify JS to add the model to LocalStorage.

After an extensive Google search I found a script that leverages both systems to store a single observable. The interesting thing is that it uses Knockout’s subscribe method to subscribe to changes. This means all changes are ‘automatically’ stored away. I've decided to extend Knockout's ko variable:

ko.trackChange = function (observable, key) {

 var store = amplify.store.localStorage;

 //initialize from stored value, or if no value is stored yet, 
 //use the current value
 var value = store(key) || observable();

 //track the changes
 observable.subscribe(function (newValue) {
     store(key, newValue || null);

     if (ko.toJSON(observable()) != ko.toJSON(newValue)) {
         observable(newValue);
     }
 });

 observable(value); //restore current value
};

The next step is to make a function that adds persistence to all the observables of a given view model. Check the following code:

ko.persistChanges = function(vm, prefix) {

    if (prefix === undefined) {
        prefix = '';
    }

    for (var n in vm) {

        var observable = vm[n];
        var key = prefix + n;

        if (  ko.isObservable(observable) && 
             !ko.isComputed(observable)) {

            //track change of observable
            ko.trackChange(observable, key);

            //force load
            observable();
        }
    }
};

ko.isComputed = function(instance) {
    if ( instance === null || 
         instance === undefined || 
         instance.__ko_proto__ === undefined ) {
        return false;
    }

    if (instance.__ko_proto__ === ko.dependentObservable) {
        return true;
    }

    // Walk the prototype chain
    return ko.isComputed(instance.__ko_proto__);
};

It basically looks through the given model vm and stores all non-computed observables. To prevent collisions I've also added a prefix. Hooking this up is fairly easy. The following example shows how it works:

//model specification
function SimpleModel() {
    var _this = this;

    this.message = ko.observable('Hello');
    this.subject = ko.observable('World');
    this.text = ko.computed(function () {
        return _this.message() + ' ' + _this.subject() + '!';
    });
}

//new it up
var vm = new SimpleModel();

//bind to interface
ko.applyBindings(vm);

//persist it
ko.persistChanges(vm, 'vm-');

//alert 1 - should be 'Hello World!'
//at least the first time ;-)
alert(vm.text());

//change it
vm.message('Bye');

//alert 2 - should be 'Bye World!'
alert(vm.text());

//load a new one up to check
var vm2 = new SimpleModel();
ko.persistChanges(vm2, 'vm-');

//alert 3 - should be 'Bye World!'
alert(vm2.text());

Check this JSFiddle to fiddle around with the code. Let me know what you think.

  1. Jim says:

    Thanks — I’m finally making the jump to javascript clients in a big way and I find Knockout MVVM very much like the experience I have from the WP XAML two-way binding.

    Please blog more on this topic.
    Jim

    1. Kees C. Bakker says:

      I’m working on a new script dat stores the model in a single slot in storage. This method fails when sub observables are used in arrays. Got it fixed in the lab :D

      1. Don says:

        Kees, where is the sub observables fix? I’m using knockout.mapping to populate my observableArrays. The arrays goes to localStorage, but with empty data.

        Thanks,
        Don

        1. Kees C. Bakker says:

          If you don’t mind working through some code, check http://tools.keestalkstech.com/Generator/. Just include a copy of http://tools.keestalkstech.com/Generator/Scripts/knockout.extra.js. You’ll need to use ko.onlySerializeObservables and ko.__AOM. My model shows how. I’m working on a serious blog post for this feature.

          1. André Krijnen says:

            Kees, I have a problem. When I work with persistChanges I run in a Circular Reference error.

            The problem occurs when I raise a popup to store some attachments. Before I load the popup I do persistchanges, reload the current data, because it is changed in the back to get the current version, and then try to save the knockout data.

            When using json.stringify it runs in the circular reference error.

          2. Kees C. Bakker says:

            Hey André, sorry for the late reaction. Do you have a JSFiddle example for me? I would love to help.

  2. Tyshun Jones says:

    Will this work with an observable arrays as well?

    1. Kees C. Bakker says:

      No, because with arrays you need to reconstruct the array object types en that’s hard to do. I did it for a project: http://tools.keestalkstech.com/Generator/. You might want to inspect that source.

expand_less