KendoUI + jStorage: saving data in your browser


I love the cloud and being always connected but somedays are sunny and there are no clouds near you. Being able to save your work locally while you are on the road and synchronize it when you get home late in the night is always nice. Let’s see how.

This post is about one way of saving data in your browser and yes, you can only save a small amount of information but for most applications it is pretty likely to be enough.

jStorage – store data locally with JavaScript

jStorage is a cross-browser key-value store database that stores data locally in the browser. They claim to support all major browser both in desktop and in mobile.

It is pretty easy to use, you download it from their web site (here) and then you just need to include this file and jQuery (although it also works with Prototype and with MooTools).

Since I’m planning to use it in conjunction with Kendo UI, jQuery fits perfect to me.

jStorage: save values

As I’ve mention before, jStorage is a key-value database, so each item saved is identified by a key. Saving it is as easy as invoking set method with two arguments: the key that identifies the value in the database; and the value being saved. Optionally you might specify a TTL (Time To Live), how long the value is going to be saved in the DataBase expressed in milliseconds:

$.jStorage.set(key, value[, options]);

jStorage: retrieve values

This is as easy as saving it: invoke get method with the key of the value being retrieved an optionally the default value if the key is not found in the database.

$.jStorage.get(key, defaultValue);

 jStorage: delete value

In order to delete a value from the database, you should use deleteKey. The argument is the key being removed.

$.jStorage.deleteKey(key);

jStorage: delete all

Use flush for deleting all keys / values from the database.

$.jStorage.flush(key);

KendoUI + jStorage

In this section I’m going to start with an example from Kendo UI demos and I will transform it to work with jStorage.

This is how it initially looks like:

Original Grid with Remote DataSource

And this is the code

var crudServiceBaseUrl = "http://demos.kendoui.com/service";
var dataSource = new kendo.data.DataSource({
    transport: {
        read        : {
            url     : crudServiceBaseUrl + "/Products",
            dataType: "jsonp"
        },
        update      : {
            url     : crudServiceBaseUrl + "/Products/Update",
            dataType: "jsonp"
        },
        destroy     : {
            url     : crudServiceBaseUrl + "/Products/Destroy",
            dataType: "jsonp"
        },
        create      : {
            url     : crudServiceBaseUrl + "/Products/Create",
            dataType: "jsonp"
        },
        parameterMap: function (options, operation) {
            if (operation !== "read" && options.models) {
                return {models: kendo.stringify(options.models)};
            }
        }
    },
    batch    : true,
    pageSize : 20,
    schema   : {
        model: {
            id    : "ProductID",
            fields: {
                ProductID   : { editable: false, nullable: true },
                ProductName : { validation: { required: true } },
                UnitPrice   : { type: "number", validation: { required: true, min: 1} },
                Discontinued: { type: "boolean" },
                UnitsInStock: { type: "number", validation: { min: 0, required: true } }
            }
        }
    }
});

$("#grid").kendoGrid({
    dataSource: dataSource,
    pageable  : true,
    height    : 430,
    toolbar   : ["create"],
    columns   : [
        { field: "ProductName", width: 240 },
        { field: "UnitPrice", title: "Unit Price", format: "{0:c}", width: "100px" },
        { field: "UnitsInStock", title: "Units In Stock", width: "100px" },
        { field: "Discontinued", width: "100px" },
        { command: ["edit", "destroy"], title: " ", width: "172px" }
    ],
    editable  : "inline"
});

KendoUI + jStorage: extract the model

Since our grid is going to be managing two data sources (local stored in browser database and remote). We will define one common schema making sure both data source use the same.

var schema = {
    model: {
        id    : "ProductID",
        fields: {
            ProductID   : { editable: false, nullable: true },
            ProductName : { validation: { required: true } },
            UnitPrice   : { type: "number", validation: { required: true, min: 1} },
            Discontinued: { type: "boolean" },
            UnitsInStock: { type: "number", validation: { min: 0, required: true } }
        }
    }
}

And I will reference it in my remote data source (what in Kendo UI code was the dataSource object);

var remoteDataSource = new kendo.data.DataSource({
    transport: {
        ...
    },
    batch    : true,
    pageSize : 20,
    schema   : schema
});

KendoUI + jStorage: define local and remote datasource

Now, we will introduce the local data source: the one that saves data in the database of the browser. This is something like:

var localDataSource = new kendo.data.DataSource({
    transport: {
        ...
    },
    batch    : true,
    pageSize : 20,
    schema   : schema
}); 

That is the same than the remoteDataSource except the transport definition.

KendoUI + jStorage: transport for remote datasource

What I am going to do is save all the data of the grid in the browser database. Of course, this is only possible for a small data set, but once you get the idea you might use much more sophisticated solutions that save each element under its key.

This way we only need to methods: one for save the data and one for loading.

// DB key for storing Grid Data
var IsLocalKey = "ob-local";
var DbKey = "ob-products";

/**
 * Save Data in Browser Data Store
 * @param op Options
 */
function saveData(op) {
    var data = localDataSource.data();
    $.jStorage.set(DbKey, data);
    op.success(data);
}

/**
 * Save Data in Browser Data Store
 * @param op Options
 */
function loadData(op) {
    var data = $.jStorage.get(DbKey, data);
    op.success([]);
}

Now, my local data source looks like:

var localDataSource = new kendo.data.DataSource({
    transport: {
        read   : loadData,
        update : saveData,
        destroy: saveData,
        create : saveData
    },
    batch    : true,
    pageSize : 20,
    schema   : schema
});

KendoUI + jStorage: define the grid

Next is defining the Kendo UI Grid: I will reuse the original one change the dataSource to be either the local or remote DataSource. So, I will start setting it to remote and introduce a new variable called dataSource that will actually be localDataSource or remoteDataSource.

var isLocal = $.jStorage.get(IsLocalKey) || false;
var dataSource = isLocal ? localDataSource : remoteDataSource;
$("#grid").kendoGrid({
    dataSource: dataSource,
    pageable  : true,
    height    : 430,
    toolbar   : ["create"],
    columns   : [
        { field: "ProductName", width: 240 },
        { field: "UnitPrice", title: "Unit Price", format: "{0:c}", width: "100px" },
        { field: "UnitsInStock", title: "Units In Stock", width: "100px" },
        { field: "Discontinued", width: "100px" },
        { command: ["edit", "destroy"], title: " ", width: "172px" }
    ],
    editable  : "inline"
});

KendoUI + jStorage: switch from remote to local

So far my grid works as Kendo UI demo. Now, I will introduce a button in the toolbar to switch to local. This is what is known as custom toolbar buttons and consists of a new entry in toolbar array:

toolbar   : [
    "create",
    { name: "switch-db", text: "Switch DB", className: "k-grid-switch-b", imageClass: "k-icon k-i-refresh" }
],

And the code is:

$(".k-grid-switch-db").on("click", function () {
    if (!isLocal) {
        // Get Data from remote dataSource and save it into local dataSource
        var data = remoteDataSource.data();
        $.jStorage.set(DbKey, data);
        dataSource = localDataSource;
    } else {
        // Get Data from local dataSource and save it into remote dataSource.
        // NOTE: Pending
        dataSource = remoteDataSource;
    }
    // Refresh Grid
    grid.setDataSource(dataSource);
    grid.dataSource.read();
    isLocal = !isLocal;
    $.jStorage.set(IsLocalKey, isLocal);
});

What I do is get the data from the data source being used and save it in the other. Saving it in the local is as simple as $.jStorage.set(DbKey, data) saving it in the remote is a little tricky since you just want to save changes, so we will see it in the next post.

After that, I just need to update the grid with the new data source by doing grid.setDataSource(dataSource); and invoke a read for refreshing the grid with the data from the alternative data source.

Finally I save if latest version of the data is either local storage or remote. So next time I reload the page I get the right version.

This is how it looks like:

Grid with support for switching data sources

My final code is:

// DB key for storing Grid Data
var DbKey = "ob-products";
var IsLocalKey = "ob-local";

/**
 * Save Data in Browser Data Store
 * @param op Options
 */
function saveData(op) {
    var data = localDataSource.data();
    $.jStorage.set(DbKey, data);
    op.success([]);
}

/**
 * Save Data in Browser Data Store
 * @param op Options
 */
function loadData(op) {
    console.log("loading");
    var data = $.jStorage.get(DbKey) || [];
    op.success(data);
}

var schema = {
    model: {
        id    : "ProductID",
        fields: {
            ProductID   : { editable: false, nullable: true },
            ProductName : { validation: { required: true } },
            UnitPrice   : { type: "number", validation: { required: true, min: 1} },
            Discontinued: { type: "boolean" },
            UnitsInStock: { type: "number", validation: { min: 0, required: true } }
        }
    }
};

var crudServiceBaseUrl = "http://demos.kendoui.com/service";
var remoteDataSource = new kendo.data.DataSource({
    transport: {
        read        : {
            url     : crudServiceBaseUrl + "/Products",
            dataType: "jsonp"
        },
        update      : {
            url     : crudServiceBaseUrl + "/Products/Update",
            dataType: "jsonp"
        },
        destroy     : {
            url     : crudServiceBaseUrl + "/Products/Destroy",
            dataType: "jsonp"
        },
        create      : {
            url     : crudServiceBaseUrl + "/Products/Create",
            dataType: "jsonp"
        },
        parameterMap: function (options, operation) {
            if (operation !== "read" && options.models) {
                return {models: kendo.stringify(options.models)};
            }
        }
    },
    batch    : true,
    pageSize : 20,
    schema   : schema
});

var localDataSource = new kendo.data.DataSource({
    transport: {
        read   : loadData,
        update : saveData,
        destroy: saveData,
        create : saveData
    },
    batch    : true,
    pageSize : 20,
    schema   : schema
});

var isLocal = $.jStorage.get(IsLocalKey) || false;
var dataSource = isLocal ? localDataSource : remoteDataSource;
var grid = $("#grid").kendoGrid({
    dataSource: dataSource,
    pageable  : true,
    height    : 430,
    toolbar   : [
        "create",
        { name: "switch-db", text: "Switch DB", className: "k-grid-switch-b", imageClass: "k-icon k-i-refresh" }
    ],
    columns   : [
        { field: "ProductName", width: 240 },
        { field: "UnitPrice", title: "Unit Price", format: "{0:c}", width: "100px" },
        { field: "UnitsInStock", title: "Units In Stock", width: "100px" },
        { field: "Discontinued", width: "100px" },
        { command: ["edit", "destroy"], title: " ", width: "172px" }
    ],
    editable  : "inline"
}).data("kendoGrid");

$(".k-grid-switch-db").on("click", function () {
    if (!isLocal) {
        // Get Data from remote dataSource and save it into local dataSource
        var data = remoteDataSource.data();
        $.jStorage.set(DbKey, data);
        dataSource = localDataSource;
    } else {
        // Get Data from local dataSource and save it into remote dataSource.
        // NOTE: Pending
        dataSource = remoteDataSource;
    }
    // Refresh Grid
    grid.setDataSource(dataSource);
    grid.dataSource.read();
    isLocal = !isLocal;
    $.jStorage.set(IsLocalKey, isLocal);
});