KendoUI Grid popup editing and inline editing mixed together


If this is not the first time that you read my posts, you probably know that I actively participate in StackOverflow and you probably have already gone through some post where I write about some question that I think that deserves a little more information.

Few days back someone asked how (if possible at all) to get both popup and inline editing in the same grid. Basically what he was trying to do is using popup when creating a new record and use inline (or incell) for existing records. We might argue if from a standard UX point of view if this is good but I can see some cases where this might make sense so I’ve decided to investigate a little on it and I’ve tried to answer the question.

Mixing popup and inline editing in a grid

Research

The very first thing that I thought about was trying to invoke directly the functions that creates the popup window form for editing a record and the function that switches into inline (or incell) mode. The idea was if I can invoke the internal methods directly, I would be able to trigger the mode that I want defining custom buttons.

But when examining KendoUI code (yes, it’s is not Open Source but as Telerik Most Valuable Professional I have access to it) I see that I need to send extra arguments to the methods for popup and incell and it was not easy (clean) for me getting these arguments.

Second approach was invoking KendoUI Grid addRow method that enters in edit mode and depending on the initialization value, it is going to be “popup” or “inline”. The question was … if I change the edition mode dynamically and not at initialization time, will it be used?

The answer is yes, you can change the option dynamically and next time edit event is triggered the edition mode will change. This is not the case with some options for this and other Kendo UI Widget that once the widget is created, they will not read back the options.

How to implement it

What I had to do is:

  1. Initialize the Grid for using, by default, inline mode.
  2. Define a Custom button in the toolbar that looks like the standard “Add new row” but executes our own code.
  3. Define the button handler that forces the edition mode to “popup”, invokes editRow and restores previous edition mode (“inline”)

This would be something like

var grid = $("#grid").kendoGrid({
    dataSource: ds,
    toolbar: [ 
        {
            // My own version of "Add new record" button, with name **popup**
            text : "Add new record", 
            name: "popup", 
            iconClass: "k-icon k-add"
        }
    ],
    // By default is **inline**
    editable: "inline",
    ...
}).data("kendoGrid");

Here we can see that I’ve created in the toolbar a button with name “popup” and showing the text “Add new record”, the same being displayed by the default “create” button. Finally, I’ve define the icon that should be displayed in the button having the button look exactly the same than the standard “create” button.

The second important question is that I’ve defined “editable” as “inline” so it is expected that I define an “edit” button for each row and when clicked, it enters in “inline” edition.

Now, the missing part: attach a click event handler attached to my “popup” button. This is:

// Event handler for my **popup** button defined in the grid toolbar
$(".k-grid-popup", grid.element).on("click", function () {
    // Temporarily set editable to "popup"
    grid.options.editable = "popup";
    // Insert row
    grid.addRow();
    // Revert editable to inline
    grid.options.editable = "inline";
});

Binding a function to the click event for buttons defined in the toolbar is defining using “on” jQuery method for the CSS class “.k-grid-” in our case since “name” is equal to toolbar, we use the CSS “.k-grid-popup”.

As you can see is a pretty simple process, pretty clean and you don’t have to do a lot of code edition or source code change.

You can play with it in this JSFiddle.

KendoUI Release Q2’2014 Toolbar (basics)


Today, just few minutes back, Telerik has announced their new version of KendoUI: Q2’2014. Some new widgets, features and AngularJS framework integrated in the official code.

In this release there are some very powerful widgets, actually I do consider them as full applications more than just simple widgets. Focusing in Kendo UI Web we find the following new Widgets:

  • Pivot Grid, see demo here, with functionality similar to the one that you can find in Microsoft Excel and very useful for analyzing data in a numeric way.
  • Gant, see demo here, with functionality similar to Microsoft Plan Project, and good for displaying project status.
  • And, my favorite one, ToolBar, see demo here. A ToolBar similar to the one that you can find in Editor Widget and something that we missed for a long time.

Toolbar

Kendo UI new ToolBar widget includes the following features:

  • Button: regular button that might include an icon, a text or both.
  • Button Group: set of buttons rendered together (no space between buttons)
  • Split Button: a button that include a text plus a drop down menu with more buttons.
  • Separator: separate a group of buttons by a vertical bar.
  • If it does not fit in your screen it includes an overflow button with the hidden buttons.

Defining a toolbar

A toolbar is basically an array of items where each item is a Button, a Button Group, a Split Button or a Separator. When the element is a Button Group, then it will include the different buttons that compose the group.

Basic buttons are defined as:

$("#toolbar").kendoToolBar({
  items: [
    { type: "button", icon: "plus", text: "Add" },
    { type: "button", icon: "dash", text: "Remove" },
  ]
});

Each element in the array includes:

  • A “type” that indicates the type of element. Supported types are “button”, “splitButton”, “buttonGroup”, “separator”.
  • A “text” the displayed text in the button. If not defined there is no text (only icon).
  • An “icon” the displayed icon in the button. If not defined there is no icon (only text).

It might also include other properties as “id” (assign a unique identifier to the button), “click” event handler that will execute when the button is clicked.

Defining a button group is as simple as defining a button.

$("#toolbar").kendoToolBar({
  items: [
    { 
      type: "buttonGroup", 
      buttons: [
        { type: "button", icon: "clock" },
        { type: "button", icon: "note" },
        { type: "button", icon: "refresh" }
      ]
    }
  ]
});

Define the “type” as “buttonGroup” and then define an array of buttons defined as before.

A “separatore” is equally very easy to define. Simply try:

$("#toolbar").kendoToolBar({
  items: [
    { type: "button", icon: "plus", text: "Add" },
    { type: "separator" },
    { type: "button", icon: "dash", text: "Remove" },
  ]
});

Really simple and really useful Widget!

NOTE: Default supported icons are found in Styling / Icons

KendoUI masked text boxes


One of the new widgets just introduced in Kendo UI is kendoMaskedTextBox. This is something that we have been waiting for long time… no need to keep waiting. We can start today enjoying it. Let’s see how.

Introduction to KendoUI MaskedTextBox

What is a maked input field? Their definition: Define and restrict the data to be entered in an html input field.

Let me show it with an example, you have to define an input field and you want to control that what the user types follows some specific format (v.g. telephone number, credit card number, date…) so you are implicitly defining some validation rules. Both the user types it with the format that you want and it gets displayed with the correct format.

kendoMaskedTextBox: Defining a mask for a phone number

In my country, the telephone number are 9 digits and you typically have them in the format 999-999-999. So I want to define an input fields that only allows me to type phones so they have to be all digits and as soon as you finish a group of three the cursor moves to the next group.

What you would have is something like:

Masked Telephone

or when empty:

Masked input empty

And the definition is as simple as:

<br />
$("#phone_number").kendoMaskedTextBox({<br />
    mask: "000-000-000",<br />
    value: "555123456"<br />
});<br />

or you can define using just HTML doing:

<div>
    <label for="phone_number">Phone number:</label>
    <input id="phone_number" data-role="maskedtextbox"
           data-mask="000-000-000" value="555123456">
</div>

Pretty simple and powerful!

kendoMaskedTextBox: Defining masks, the syntax

Kendo UI masked input predefines a series of mask rules (what to accept on each position of the mask). These are the predefined masks.

  1. 0 – Digit. Accepts any digit between 0 and 9.
  2. 9 – Digit or space. Accepts any digit between 0 and 9, plus space.
  3. # – Digit or space. Like 9 rule, but allows also (+) and (-) signs.
  4. L – Letter. Restricts input to letters a-z and A-Z. This rule is equivalent to [a-zA-Z] in regular expressions.
  5. ? – Letter or space. Restricts input to letters a-z and A-Z. This rule is equivalent to [a-zA-Z] in regular expressions.
  6. & – Character. Accepts any character. The rule is equivalent to \S in regular expressions.
  7. C – Character or space. Accepts any character. The rule is equivalent to . in regular expressions.
  8. A – Alphanumeric. Accepts letters and digits only.
  9. a – Alphanumeric or space. Accepts letters, digits and space only.
  10. . – Decimal placeholder. The decimal separator will be get from the current culture used by Kendo.
  11. , – Thousands placeholder. The display character will be get from the current culture used by Kendo.
  12. $ – Currency symbol. The display character will be get from the current culture used by Kendo.

kendoMaskedTextBox: Defining custom masks

But you do not have to use only those predefined mask rules, you might define your own.

Example: Defining a mask that allows typing only even numbers is as simple as:

$("#all-even").kendoMaskedTextBox({
    mask: "eeee",
    rules: {
        "e": /[02468]/
    }
});

Where I define that the input supports 4 characters which mask rule is an e and then I define what e means by defining a rule that says that e might be a digit in the group 02468.

But the rule might even be a function. So this can also be implemented as:

$("#all-even").kendoMaskedTextBox({
    mask: "eeee",
    rules: {
        "e": function(d) {
            return d % 2 == 0;
        }
    }
});

Simply cool!!!

kendoMaskedTextBox: assigning values

This is important to note. In widgets as KendoDateTimePicker when you initialize the widget with an invalid date, this is displayed as it is until you edit the field.

Example: I initialize the DatePicker to February 29th, 2014 but this day does not exist. What we see is:

Initializing mask text box with invalid value

And when we enter in edition mode we get:

Pick a date

But, kendoMaskedTextBox works in a different way, the characters defined in the initialization are like typed by you and those that do not met the rule restriction are simply ignored.

So if we define a value of 12345678 for our 4 even digits mask, what we will get is:

Initializing mask text box with invalid value

$("#all-even").kendoMaskedTextBox({
    mask: "eeee",
    rules: {
        "e": "/[02468]/"
    },
    value: 123456789
});

That shows that the date that we used during the initialization is just ignored.

kendoMaskedTextBox: Still some features missing

This is actually a great widget. The only limitation that I’ve found is that masks do not support repeating factor something like saying that a number repeated seven times (something like 9(3) meaning that a digit between 0 and 9 repeated 3 times) or that some character mask should be typed between n and m times (something like 9(1..3) meaning that a number between 0 and 9 repeated between 1 and 3 times).

KendoUI Q1’14 Release just announced


Telerik just announced their KendoUI Q1’14 Release and this time they bring a lot of cool new widget and some very interesting new options for old ones.

In this post I will list some of this cool options that I’ve already been able to test while in Beta and now I can enjoy in its full flavor.

New Widgets

There are some cool and very nice widget as:

  1. Notification: Very easy and non blocking notifications for your sites. Forget about alerts or have to handcraft popup windows. They provide you new notifications even with custom categories (not just warning and errors) and you can choose where to display them (top, bottom, left, right, fixed position, contained, floating…), for how long (they can autohide after some period of time), they can be templated,…
  2. MaskedTextBox: This is likely my favorite one because of the long time waited. Yes, you can finally have masked text box for telephones, credit cards, or you can even create your own. This actually deserves a post by itself that I’m already writing.
  3. Sortable: This is actually a widget per se but it is also used in Grid were you can move rows and sort them. Really nice effect!!!
  4. “Real-time” DataSource: Wow!!! this is also cool. Have two grids, listviews… and see what you do in one being updated in the other. Do you remember what I did with Dropbox in the post ? Now you have the same effect with plain KendoUI. This is a new level of Observable Objects
  5. Grid Frozen Columns: another new options for making Kendo UI Grids even better. Now you might have some columns stick where they are when you scroll horizontally. Perfect when you have large amount of data or even better when your screen size is not that large.

Please, go to their demo web site and see them in action. It’s worthy.

KendoUI MultiSelect with server side filtering


Today I got a question where someone wanted to use KendoUI AutoComplete but wanted it to look like a KendoUI MultiSelect. When I wonder why he/she wanted to use an AutoComplete the only reason was because the list of elements was huge. So, why not using Server Side Filtering in a MultiSelect?.

KendoUI Server Side Filtering

When you need to manage a large list of elements the solution doing some filtering in the server and just transfer a few of them.

The advantages of this are multiple:

  1. Save bandwidth sending only few elements
  2. Do not render an HTML element for each element returned
  3. Save memory due to less DOM elements
  4. Save memory due to a smaller KendoUI DataSource
  5. Increase performance do to a smaller DOM

So why not always using Server Side Filtering? Sometimes the fact of going back and forth getting new elements is not worthy for a small list of elements and this increases the complexity in the server that needs to deal with filtering.

Think about the number of elements and how they are going to be displayed in screen, if there is no good reason for having all pre-loaded, try doing it in the server.

KendoUI MultiSelect with server side filtering: MySQL table structure

This example will show a KendoUI MultiSelect that allows to choose countries from a MySQL database.

The structure of the countries table in MySQL is:

mysql> desc countries;
+-------+---------------+------+-----+---------+-------+
| Field | Type          | Null | Key | Default | Extra |
+-------+---------------+------+-----+---------+-------+
| id    | decimal(10,0) | NO   | PRI | NULL    |       |
| name  | varchar(128)  | NO   |     | NULL    |       |
+-------+---------------+------+-----+---------+-------+
2 rows in set (0,00 sec)

mysql> 

And one sample of the content is:

mysql> select * from countries order by name limit 10;
+----+---------------------+
| id | name                |
+----+---------------------+
|  0 | Afghanistan         |
|  2 | Albania             |
|  3 | Algeria             |
|  4 | American Samoa      |
|  5 | Andorra             |
|  6 | Angola              |
|  7 | Anguilla            |
|  8 | Antarctica          |
|  9 | Antigua and Barbuda |
| 10 | Argentina           |
+----+---------------------+
10 rows in set (0,00 sec)

mysql> 

KendoUI MultiSelect with server side filtering: PHP select code

The PHP code for selecting the elements is:

<?php
// Create connection to the database 
// db server:  127.0.0.1 (localhost)
// db user: so
// db password : password
// db name : so
$con = mysqli_connect("127.0.0.1", "so", "password", "so");

// Initialize the list of countries to return
$countries = [];
// Check that we actually get a filter condition
if ($_GET && $_GET['filter']['filters'][0]) {
    // Get the value
    // NOTE: We are not checking other fields in filter since autocomplete will
    //       always send it as "starts with" and ignore case.
    $filter = $_GET['filter']['filters'][0]['value'];
    $where  = "where name like '" . $filter . "%' ";
}
else {
    // Did not get filter, do not build a where clause
    $where = "";
}
// Query MySQL and limit the list of results to 5
$result = mysqli_query($con, "SELECT name FROM countries " . $where . " order by 'name' limit 5");

// Build the array of results
while ($row = mysqli_fetch_array($result)) {
    array_push($countries, array("name" => $row['name']));
}
mysqli_close($con);

// Return it as JSON
header("Content-type: application/json");
echo json_encode($countries);
?>

KendoUI MultiSelect with server side filtering: JavaScript

And finally the MultiSelect definition:

// DataSource where the read URL is the PHP code above
var ds = new kendo.data.DataSource({
    transport      : {
        read: "select.php"
    },
    // Server Side Filtering
    serverFiltering: true
});

// MultiSelect Widget initialization
$("#items").kendoMultiSelect({
    dataValueField: "name",
    dataTextField : "name",
    dataSource    : ds
});

KendoUI DataSources + Dropbox DataStore: Change Listener


My last posts were about Dropbox DataStore and Kendo UI DataSource. I have demonstrated how to read, create, delete and update records.

And I have shown how the original example provided with Dropbox was able to listen for changes in my application and automatically synchronize those changes.

I want to do the same! When Dropbox sample code changes the DataStore content, I want that my grid automatically gets updated.

Event listeners in DataStore Dropbox

We have seen in previous videos how when we create, update or delete a record from our Grid, the HTML sample code from Dropbox magically gets updated. This is because Dropbox provide a mechanism for registering a function (a listener) to changes in the DataStore. What Dropbox library does is that when you updates the copy in Dropbox server, it notifies the library and this calls you.

dataStore.recordsChanged.addListener(function(ev) {
    // Event handler code...
});

What we do in this event handler is either getting the changes and update the affected rows or fully update the table.

In my case and for simplicity, I’m going to update the complete Grid by invoking `DataSource.read()` method. Something like:

// Add listener for changes
dataStore.recordsChanged.addListener(function(ev) { taskTableDS.read() });

NOTE: If I just add these lines of code to what I have in my previous post, I will get an error because in the current implementation I was expecting that the DataStore was opened only one: so I was not closing it and I could try opening it twice if DataStore.read happen to be called twice.

So I need to slightly change readTask code to control that the DataStore is alreayd open and not try to do it again.

function readTasks(op) {
    if (client.isAuthenticated()) {
        // Client is authenticated. Display UI.
        if (dataStore === null) {
            var datastoreManager = client.getDatastoreManager();
            datastoreManager.openDefaultDatastore(function (error, datastore) {
                if (error) {
                    alert('Error opening default datastore: ' + error);
                }
                dataStore = datastore;

                // Add listener for changes
                dataStore.recordsChanged.addListener(function (ev) { taskTableDS.read() });

                taskTable = datastore.getTable('tasks');
                op.success(taskTable.query());
            });
        } else {
            op.success(taskTable.query());
        }
    }
}

And the complete code is:

// Insert your Dropbox app key here:
var DROPBOX_APP_KEY = '0sh....';

// Exposed for easy access in the browser console.
var client = new Dropbox.Client({key: DROPBOX_APP_KEY});
var taskTable = null;
var dataStore = null;

// Try to finish OAuth authorization.
client.authenticate({interactive: true}, function (error) {
    if (error) {
        alert('Authentication error: ' + error);
    }
});

function parseItem(elem) {
    return {
        id       : elem.getId(),
        taskname : elem.get("taskname"),
        created  : elem.get("created"),
        completed: elem.get("completed")
    };
}

function readTasks(op) {
    if (client.isAuthenticated()) {
        // Client is authenticated. Display UI.
        if (dataStore === null) {
            var datastoreManager = client.getDatastoreManager();
            datastoreManager.openDefaultDatastore(function (error, datastore) {
                if (error) {
                    alert('Error opening default datastore: ' + error);
                }
                dataStore = datastore;

                // Add listener for changes
                dataStore.recordsChanged.addListener(function (ev) { taskTableDS.read() });

                taskTable = datastore.getTable('tasks');
                op.success(taskTable.query());
            });
        } else {
            op.success(taskTable.query());
        }
    }
}

function parseDropboxRecords(d) {
    var res = [];
    $.each(d, function (idx, elem) {
        res.push(parseItem(elem));
    });
    return (res);
}

var taskTableDS = new kendo.data.DataSource({
    transport: {
        read   : function (op) {
            readTasks(op);
        },
        update : function (op) {
            var data = op.data;
            var id = data.id;
            // Remove id to do not have it duplicated
            delete op.data.id;
            var record = taskTable.get(id).update(data);
            op.success([record]);
        },
        destroy: function (op) {
            taskTable.get(op.data.id).deleteRecord();
            op.success();
        },
        create : function (op) {
            // Remove id to do not have it duplicated
            delete op.data.id;
            var record = taskTable.insert(op.data);
            op.success([record]);
        }
    },
    schema   : {
        model: {
            id    : "id",
            fields: {
                id       : { type: "string" },
                taskname : { type: "string" },
                created  : { type: "date", editable: false },
                completed: { type: "boolean" }
            }
        },
        parse: parseDropboxRecords
    }
});

$("#grid").kendoGrid({
    dataSource: taskTableDS,
    editable  : "popup",
    toolbar   : ["create"],
    columns   : [
        { command: ["edit", "destroy"], width: 180 },
        { field: "taskname", width: 80 },
        { field: "created", format: "{0:G}", width: 200 },
        { field: "completed", width: 70 }
    ]
});

Which looks like:

KendoUI DataSources + Dropbox DataStore: Delete


After seeing first how to read data from Dropbox DataStore into Kendo UI DataSource and then how to update it, now it is time for deleting records.

Deleting records from Dropbox DataStore

Start adding a delete button to the Grid.

$("#grid").kendoGrid({
    dataSource: taskTableDS,
    editable  : "inline",
    columns   : [
        { command: ["edit", "destroy"], width: 180 },
        { field: "taskname", width: 80 },
        { field: "created", format: "{0:G}", width: 200 },
        { field: "completed", width: 70 }
    ]
});

Deleting a record from DropBox is actually pretty simple, just invoke deleteRecord and it is gone! Then invoke success in Kendo UI side and also gone from Kendo UI DataSource.

So, my DataSource transport.destroy method is:

destroy: function (op) {
    taskTable.get(op.data.id).deleteRecord();
    op.success();
}

And the complete code is:

// Insert your Dropbox app key here:
var DROPBOX_APP_KEY = '0sh............';

// Exposed for easy access in the browser console.
var client = new Dropbox.Client({key: DROPBOX_APP_KEY});
var taskTable = null;

// Try to finish OAuth authorization.
client.authenticate({interactive: true}, function (error) {
    if (error) {
        alert('Authentication error: ' + error);
    }
});

function parseItem(elem) {
    return {
        id       : elem.getId(),
        taskname : elem.get("taskname"),
        created  : elem.get("created"),
        completed: elem.get("completed")
    };
}

function readTasks(op) {
    if (client.isAuthenticated()) {
        // Client is authenticated. Display UI.
        var datastoreManager = client.getDatastoreManager();
        datastoreManager.openDefaultDatastore(function (error, datastore) {
            if (error) {
                alert('Error opening default datastore: ' + error);
            }
            taskTable = datastore.getTable('tasks');
            var records = taskTable.query();
            op.success(records);
        });
    }
}

var taskTableDS = new kendo.data.DataSource({
    transport: {
        read   : function (op) {
            readTasks(op);
        },
        update : function (op) {
            var data = op.data;
            var id = data.id;
            // Remove id to do not have it duplicated
            delete op.data.id;
            var record = taskTable.get(id).update(data);
            op.success([record]);
        },
        destroy: function (op) {
            taskTable.get(op.data.id).deleteRecord();
            op.success();
        }
    },
    schema   : {
        model: {
            id    : "id",
            fields: {
                id       : { type: "string" },
                taskname : { type: "string" },
                created  : { type: "date", editable: false },
                completed: { type: "boolean" }
            }
        },
        parse: function (d) {
            var res = [];
            $.each(d, function (idx, elem) {
                res.push(parseItem(elem));
            });
            return (res);
        }
    }
});

$("#grid").kendoGrid({
    dataSource: taskTableDS,
    editable  : "popup",
    columns   : [
        { command: ["edit", "destroy"], width: 180 },
        { field: "taskname", width: 80 },
        { field: "created", format: "{0:G}", width: 200 },
        { field: "completed", width: 70 }
    ]
})

What I get is:

KendoUI DataSources + Dropbox DataStore: Update


In my previous post I showed you how to read data from Dropbox DataStore. Now, it is time for updating it.

Kendo UI DataSource and Dropbox DataStore: Updating

Reading data was pretty simple, we used openDefaultDatastore for getting access to the DataStore, then getTable for accessing the table and finally query for retrieving the selected data.

When we get to update there are two main modes that we might choose:

  1. Save each field of a record independently using set: which might be fine for attributes on a structure.
  2. Use update for updating a record in a single transaction.

For my grid, I opted for the second, despite some fields might not have changed I do prefer to transfer all and reduce number of transaction with Dropbox server. Actually, there is a second reason that is that KendoUI keeps track of dirty records but not dirty fields.

DataSource grid editable

Now, my Grid definition is:

$("#grid").kendoGrid({
    dataSource: taskTableDS,
    editable  : "popup",
    columns   : [
        { command: ["edit" ], width: 90 },
        { field: "taskname", width: 80 },
        { field: "created", format: "{0:G}", width: 200 },
        { field: "completed", width: 70 }
    ]
})

The difference is that I’ve added a button edit for opening a popup window where I can edit the record.

Now, I should work in the KendoUI DataSource transport.update method.

DataSource transport update

Updating a record in Dropbox server is basically transforming the data from KendoUI DataSource format into Dropbox DataStore keeping in mind a couple of questions.

  1. If the JSON field containing the new record data contains an attribute called id, then Dropbox will save it as attribute but this is not going to be the same than the id that Dropbox uses for the record. So for having it nicer, I’m going to delete the id from the JSON before sending it to Dropbox server.
  2. The result of updating the record is expected to be sent to KendoUI invoking success or error on the object received as argument of KendoUI transport.update
update : function (op) {
    var data = op.data;
    var id = data.id;
    // Remove id to do not have it duplicated
    delete op.data.id;
    var record = taskTable.get(id).update(data);
    op.success([record]);
},

Now, I can update my grid records and I will see them immediately updated in Dropbox example of task list.

As you can see, since Dropbox example adds listener for detecting changes done by other devices or application, what I modify in KendoUI grid is immediately visible in their example but not viceversa (stay tuned since I will get there).

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: "&nbsp;", 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: "&nbsp;", 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: "&nbsp;", 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);
});

KendoUI Grid editing records when using rowTemplate: tips and tricks


I use to define templates for some columns of my Grid but sometimes I find more convenient using a rowTemplate.

It is not very likely that when using a rowTemplate you need to edit the record but … why not!

KendoUI grid with rowTemplate
KendoUI grid with rowTemplate

The question is that it is not trivial if you simply follow KendoUI examples.

KendoUI Grid: using a rowTemplate

Row Templates allows you to write some HTML used for displaying a record of your grid. This feature provides you the mechanism to make a row anything you want. Lets start defining a row for displaying the information.

<script id="rowTemplate" type="text/x-kendo-tmpl">
    <tr>
        <td class="photo">
            <img src="http://demos.kendoui.com/content/web/Employees/#:data.EmployeeID#.jpg"
                 alt="#: data.EmployeeID #"/>
        </td>
        <td class="details">
            <span class="title">#: Title #</span>
            <span class="description">Name : #: FirstName# #: LastName#</span>
            <span class="description">Country : #: Country# </span>
        </td>
        <td class="employeeID">
            #: EmployeeID #
        </td>
    </tr>
</script>

I define an HTML table row with 3 cells for displaying the Photo, Information about the Employee and the Employee Number. That looks like this:

KendoUI Grid using a three cell RowTemplate
KendoUI Grid using a three cell RowTemplate

And the code for initializing the Grid is:

$("#grid").kendoGrid({
    dataSource    : {
        type     : "odata",
        transport: {
            read: {
                url: "http://demos.kendoui.com/service/Northwind.svc/Employees"
            }
        }
    },
    rowTemplate   : kendo.template($("#rowTemplate").html()),
    height        : 430,
    columns       : [
        { title: "Picture", width: 140 },
        { title: "Details", width: 300 },
        { title: "ID" }
    ]
});

Just need to specify rowTemplate and KendoUI takes care of it and since we are binding more than one field to each column, we do not specify field but only the title.

Kendo UI Grid and Row Template: add edit button

If we want to add an edit button, the very first thing is getting creating an extra column for it both in the Grid definition:

$("#grid").kendoGrid({
    dataSource    : {
        type     : "odata",
        transport: {
            read: {
                url: "http://demos.kendoui.com/service/Northwind.svc/Employees"
            }
        }
    },
    rowTemplate   : kendo.template($("#rowTemplate").html()),
    height        : 430,
    columns       : [
        { title: "&nbsp;", width: 90 },
        { title: "Picture", width: 140 },
        { title: "Details", width: 300 },
        { title: "ID" }
    ]
});

and in the template:

<script id="rowTemplate" type="text/x-kendo-tmpl">
    <tr>
        <td>
            Edit
        </td>
        <td class="photo">
            <img src="http://demos.kendoui.com/content/web/Employees/#:data.EmployeeID#.jpg"
                 alt="#: data.EmployeeID #"/>
        </td>
        <td class="details">
            <span class="title">#: Title #</span>
            <span class="description">Name : #: FirstName# #: LastName#</span>
            <span class="description">Country : #: Country# </span>
        </td>
        <td class="employeeID">
            #: EmployeeID #
        </td>
    </tr>
</script>
KendoUI Grid with RowTemplate added column for edit button
KendoUI Grid with RowTemplate added column for edit button

But we want it to look like the regular KendoUI button, so we have to style it:

<td>
    <a class="k-button k-button-icontext k-grid-edit" href="\#">
        <span class="k-icon k-edit"></span>Edit
    </a>
</td>

And we have a perfect KendoUI button:

Styled Edit button
Styled Edit button

But if we click on it, it does not open the record. Actually even if we define a button that does something like:

$("#edit-first").on("click", function() {
    var grid = $("#grid").data("kendoGrid");
    grid.editRow($("tr:nth(1)", grid.tbody));
});

So, what happens! Just a little detail, we did not specify the uid of the row. We should have defined in our template the <tr> element as:

<script id="rowTemplate" type="text/x-kendo-tmpl">
    <tr data-uid="#= uid #">
        <td>
            <a class="k-button k-button-icontext k-grid-edit" href="\#">
                <span class="k-icon k-edit"></span>Edit
            </a>
        </td>
        <td class="photo">
            ...

This is explained in the documentation as:

The outermost HTML element in the template must be a table row (<tr>). That table row must have the uid data attribute set to #= uid #. The grid uses the uid data attribute to determine the data to which a table row is bound to.

But not used in the demos, so if we cut and paste code we end-up having that does not work.

So, problem solved!