Simple client storage for view models with AmplifyJS and Knockout

From time to time, for a variety of different reasons, it can be desirable to use client storage to store data. There are many ways to utilize client storage but AmplifyJS is a very neat library that provides a consistent API to handle client storage that works in most browsers. This post explores a handy technique for utilizing client storage for knockoutjs based view models.

Why would I ever use client storage?

clientstorageexampleThe canonical example of this is using client storage to improve user experience, remembering user preferences or previously entered values such that the user doesn’t have to start all over.

A good example of client storage being applied can be found in the tutorials for knockoutjs. User progress in the tutorial is “remembered” and the option to restore is given when revisiting the site at a later time. The knockout tutorial site uses AmplifyJs under the hood to record user progress into client storage.

AmplifyJs

It’s pretty easy to use AmplifyJs on your site. After adding the appropriate script references, you can use amplify like this:

var item = { foo: "bar" };

amplify.store( "storeExample1", item );

The value “storeExample1” is the key that the item is stored against, and can be used to later retrieve the value as follows:

var item = amplify.store( "storeExample1" );

Now we’ve seen how easy it is to store and retrieve values using AmplifyJS, we can use this API for storing user data when it changes, and restore the value on coming back to the page at a later time. This can sometimes be easier said than done though; we have to remember to call amplify to store new values of the data whenever it changes.

Knockout to the rescue

One of knockout’s primary components, the observable, notifies subscribers of changes to it’s underlying data. It’s counterpart, the computed observable, is a function that is dependent on one or more other observables – reevaluating every time any of these dependencies change. This allows for us to easily call amplify to store data each and every time it is updated:

var target = ko.observable(amplify.store( "key" )); //populate from amplify

ko.computed( function() {

      amplify.store( "key", target()); //store new value on every change

});

target("some new value"); //setting the new value, triggering computed observable

Using this technique, we can keep our client-storage up-to-date on every change to an observable property.

One unfortunate side-effect of using this approach is that we now have to write a computed observable for every observable property we wish to store in client storage. Fortunately we can apply another knockout technique to keep our codebase DRY.

Extending Observables

Knockout includes a neat extension point that allows developers to easily augment knockout observables with additional functionality – extenders.

Applying this technique to our observable gives us a tidy way to apply client storage to any observable property:

var target = ko.observable( "default value" ).extend( { localStore: "key" } );

Any changes made to the observable value will be stored using amplify, and will be restored into the observable value when returning to the site at a later time.

The extender shown can be implemented as follows:

ko.extenders.localStore = function (target, key) {

    var value = amplify.store(key) || target();

      

    var result = ko.computed({

        read: target,

        write: function(newValue) {

            amplify.store(key, newValue);

            target(newValue);

        }

    });

 

    result(value);

 

    return result;

};

Here is a fiddle that demonstrates the localStore extender in action: http://jsfiddle.net/craigcav/QSCsK/

About craigcav

Craig Cavalier works as a Software Developer for Liquid Frameworks in Houston Tx, developing field ticketing and job management solutions for industrial field service companies.

Posted on May 16, 2012, in Knockout and tagged , , . Bookmark the permalink. 11 Comments.

  1. this is cool stuff…thx for the article

  2. this is cool stuff…thx for the article

  3. Any chance you could show how to make it work with observableArrays?

  4. Hi lepozepo,

    To be honest, I’m no longer using this particular technique (using an extender), rather, instead I’m using something that looks like this [http://jsfiddle.net/craigcav/FsqPd/]:

    ko.trackChanges = function(observable, key) {
    if(!ko.isObservable(observable)) throw ‘item must be observable to track changes’;

    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
    }

    //then, if we want to have observables restored from local storage (and future changes tracked)
    //we call track changes like this:

    var viewModel = {
    someProperty: ko.observable();
    someArrayProperty: ko.observable();
    };

    ko.trackChanges(viewModel.someProperty);
    ko.trackChanges(viewModel.someArrayProperty);

  5. Is it really working…i’m facing some issues, can u pls create a working fiddle for it.?

    • Hi Pravesh, the jsfiddle mentioned in the post http://jsfiddle.net/QSCsK/ is a working example, however, since the post was from last year, the script references to amplify and jquery had moved and therefore required updating (which I have now done).

      What issues are you facing? Perhaps with some more information I might be able to offer some advice?

  6. Would you recommend using amplify with a SPA framework such as DurandalJS for the following situations?

    1. store the data on first page load (and first view model loads) and just take it out of storage when switching between view models

    2. I have a shell that contains multiple view models, but some remain unloaded until you activate them. I was thinking of storing the data while loading the shell and then having the unloaded VMs get the data from storage upon load.

    I don’t know if this makes sense.

    • Hi Bert,

      I’m currently using Durandal on a couple of projects at the moment; one uses amplify to wrap localstorage and another uses indexedDB (falling back to websql where indexedDB isn’t available).

      In the projects I’m working on, I find that a convenient place for me to initialize my view model with previously stored data is with the activate callback [http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks/].

      As for storing the data in the first place, it really depends on your specific use case. Sometimes data storage may be triggered by the application shell or in a nested/child view model (perhaps loading up application state from an api call) and in other cases your data maybe be “collected” through the natural usage of your application.

      • Hi Craig, thanks for the quick reply. Yes, I was thinking about the same thing, using the activate callback to load previously stored data, but what I can’t figure out is how I would go about updating the data in case the stored data is not up to date.

  1. Pingback: Single Page Apps – Local Data Store with AmplifyJS | DevDays®

Leave a comment