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


After checking how to read data from Dropbox DataStore, update, and delete. It is time for creating new records.

Creating records into Dropbox DataStore from KendoUI Grid

Now, the important question to remember is that when KendoUI creates a record in a Grid, the value assigned to id is default value defined in the model, which typically is not defined and as consequence stays to null.

But, when the record is created in the server, it has to return an id not null to be used.

Grid modification for creating

What I am going to do is adding a button to the toolbar for creating a new record. Something like:

$("#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 }
    ]
});

First step easy!

Add create method to transport

Now, the second step is adding the transport.create method that saves the data into Dropbox (using insert method) and informs KendoUI about the id of the newly created record.

create : function (op) {
    // Remove id to do not have it duplicated
    delete op.data.id;
    var record = taskTable.insert(op.data);
    op.success([record]);
}

It is important to note that KendoUI success will actually call schema.model.parse for converting record from Dropbox format into KendoUI format.

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

Now, the complete code of the example 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();
        },
        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: function (d) {
            var res = [];
            $.each(d, function (idx, elem) {
                res.push(parseItem(elem));
            });
            return (res);
        }
    }
});

$("#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 }
    ]
});

And the example looks like:

KendoUI MultiSelect in a Grid (part 2): Yes, we can!


In my previous post I showed how to work with KendoUI MultiSelect in a Grid. There was one last thing pending: what about if we get from the server an array of id instead of directly the text?

KendoUI MultiSelect: working with ids

Let consider that our Cities DataSource is something like this:

var CitiesDS = new kendo.data.DataSource({
    transport : {
        read : {
            url: "Cities.json"
        }
    },
    schema: {
        model: {
            id    : "id",
            fields: {
                text: {type: "string"}
            }
        }
    }
});

and the return JSON is:

[
    { "id": 1, "text": "Seattle" },
    { "id": 2, "text": "Tacoma" },
    { "id": 3, "text": "Kirkland" },
    { "id": 4, "text": "Redmond" },
    { "id": 5, "text": "London" },
    { "id": 6, "text": "Philadelphia" },
    { "id": 7, "text": "New York" },
    { "id": 8, "text": "Seattle" },
    { "id": 9, "text": "London" },
    { "id": 10, "text": "Boston" }
]

We have two questions to solve, the first is pretty simple: change editor function to use as values the id. We add dataTextField and dataValueField to kendoMultiSelect options.

function citiesEditor(container, options) {
    $("<select multiple='multiple' data-bind='value : Cities'/>").appendTo(container).kendoMultiSelect({
        dataTextField : "text",
        dataValueField: "id",
        dataSource    : CitiesDS
    });
}

The second question is how, while not editing, we get the list of values displayed as text and not as string. Now, there is no magic way of doing it (as we did joining text). So, let’s write a function that does the trick.

1. Start redefining the Column Cities for using as template this new function.

columns   : [
    ...
    { field: "Cities", width: 300, template: citiesDisplay, editor: citiesEditor }
]

2. Implement citiesDisplay as:

function citiesDisplay(data) {
    var res = [];
    $.each(data.Cities, function (idx, elem) {
        res.push(CitiesDS.get(elem).text);
    });
    return res.join(", ");
}

Where I create res array for containing the values as string and then use our good friend join for converting it to CSV.

Now, we get:

KendoUI MultiSelect id: error in console
KendoUI MultiSelect id: error in console

Oops! an error in console. Why?

It turns out that I have defined in the DataSource for the Grid the field Cities as a string and now we are storing it as an array of numbers.

var ds = new kendo.data.DataSource({
    transport: {
        ...
    },
    pageSize : 10,
    schema   : {
        model: {
            id    : "Id",
            fields: {
                Id       : { type: 'number' },
                FirstName: { type: 'string' },
                LastName : { type: 'string' },
                Cities   : { type: 'string' }
            }
        }
    }
});

But … array is not a valid type definition (according the documentation) … ok! do not say anything about the type.

var ds = new kendo.data.DataSource({
    transport: {
        ...
    },
    pageSize : 10,
    schema   : {
        model: {
            id    : "Id",
            fields: {
                Id       : { type: 'number' },
                FirstName: { type: 'string' },
                LastName : { type: 'string' },
                Cities   : { }
            }
        }
    }
});

Et, voilà! Mission accomplished!

KendoUI Grid using MultiSelect and storging IDs
KendoUI Grid using MultiSelect and storging IDs

Everything works again!

KendoUI: Gaming with it (part 1)


This post is about a game that I did for my kids but also as a excuse for me for practicing KendoUI MVVM. So if you are interested on learning about MVVM this is for you. If not, at least you can play the game in here.

Game Description

  1. There is a board where you are going to place some pebbles with random numbers ranging from 1 to 9.
  2. You have to choose a column where to insert the pebble.
  3. Pebbles slide down until get the lowest empty position.
  4. Pebbles are (automatically) removed when you get a number of contiguous pebbles of at least the number of the pebble. Pebbles with 1 are special (keep reading).
  5. The largest the area the better the score. The greater the number the better the score.
  6. Two contiguous pebbles can be combined (added) to get a larger one -here where pebbles with 1 start being important-.
  7. For combining two pebbles, choose the one being added and then the one to be added to. The first, gets removed and the second replace by the value of adding them.
  8. Holes in the board (result of combined pebbles or automatically remove contiguous area) collapse, meaning that shift down.
  9. Initially the maximum pebble number is 5, meaning that you only get random pebbles from 1 to 5 and you cannot combine them for getting values larger than 5.
  10. Every 5 pebbles that you place in the board, the system inserts a row of random pebbles in the bottom shifting up all pebbles 1 row.
  11. Every 10 shifts-up maximum allowed pebble is increased by one until a maximum of 9.
  12. The game ends when some pebble gets the top of the board overflowing.

Give it a try… Numbers Flooding

KendoUI move rows between grids


One other post that comes from my experience in Stack Overflow…

This time I will explain something that is commonly used: have two grids side by side and move rows between them.

These typically look like this:

Original two gridsWhere you can select one or more rows in both grids:

Rows selectionAnd then click on a button for moving selected arrows resulting:

Rows moved

Obviously, you can select rows in the second grid and move them back to the first.

KendoUI: moving rows between grids

First, I will start defining a container for both grids and buttons. This would be something like:

<div id="panels">
    <div id="grid1"></div>
    <div id="commands">
        <div><a href="#" id="copySelectedToGrid2" class="k-button">&gt;</a></div>
        <div><a href="#" id="copySelectedToGrid1" class="k-button">&lt;</a></div>
    </div>
    <div id="grid2"></div>
</div>

And I need some CSS styling:

#panels { display: table-row; }
#grid1, #grid2 { width: 400px; display: table-cell; }
#commands { text-align: center; width: 50px; vertical-align: middle; display: table-cell; }
#commands div { padding: 5px; }
#commands div a { width: 35px; text-align: center; }

Then, I define the grids that are something like this:

var grid1 = $("#grid1").kendoGrid({
    dataSource: ds1,
    editable:   "popup",
    selectable: "multiple",
    pageable:   false,
    columns:    [
        { field: "FirstName", width: 90, title: "First Name" } ,
        { field: "LastName", width: 90, title: "Last Name" }
    ]
}).data("kendoGrid");

var grid2 = $("#grid2").kendoGrid({
    dataSource: ds2,
    editable:   "popup",
    selectable: "multiple",
    pageable:   false,
    columns:    [
        { field: "FirstName", width: 90, title: "First Name" } ,
        { field: "LastName", width: 90, title: "Last Name" }
    ]
}).data("kendoGrid");

The only remarkable question is that I’m allowing multiple selection and disabling pagination since I want visible all values of each of the grids (making easier see how rows move between grids).

Moving rows

The first part is defining the click event handler for the two buttons. This is something like:

$("#copySelectedToGrid2").on("click", function (idx, elem) {
    moveTo(grid1, grid2);
});
$("#copySelectedToGrid1").on("click", function (idx, elem) {
    moveTo(grid2, grid1);
});

And moveTo function is:

function moveTo(from, to) {
    var selected = from.select();
    if (selected.length > 0) {
        var items = [];
        $.each(selected, function (idx, elem) {
            items.push(from.dataItem(elem));
        });
        var fromDS = from.dataSource;
        var toDS = to.dataSource;
        $.each(items, function (idx, elem) {
            toDS.add($.extend({}, elem));
            fromDS.remove(elem);
        });
        toDS.sync();
        fromDS.sync();
    }
}

Where I get selected rows and iterate getting the items selected. The reason why I do it in two steps (get the data item and the insert into second grid and remove from the first grid) is because arrays are expected to immutable, which means: I cannot remove one element from an array while iterating on it since this alters the array not being able to correctly continue the iteration.

Finally, I invoke sync on both DataSources for sending the modification to the server.

Pretty easy, don’t think so!

If you want to see it running, there is a JSFiddle here.

KendoUI: readonly rows (how to and command template)


A couple days ago Piyush, a frequent reader and commenter on this blog, asked me about how to make a row in a KendoUI grid read-only.

What he tried was handling edit event and if the row was read-only then he exited the edit mode but this introduced and undesired effect: a flickering (due to entering and exiting edit popup/inline mode).

KendoUI: read-only rows

Flickering

What Piyush originally asked me was how to avoid flickering but this is actually not possible since by the time edit event is fired, the row is already in edit mode.

You can easily demonstrate this introducing and alert(“editing”); in the code and you see that the popup window for editing fields is active while alert window is waiting for ok so, you will always have the flickering.

Lets have the following DataSource definition:

var dataSource = new kendo.data.DataSource({
    data    : [
        { symbol: "AAPL", company: "Apple Inc.", readonly: true },
        { symbol: "AMZN", company: "Amazon.com Inc.", readonly: true },
        { symbol: "IBM", company: "International Business Machines Corp." },
        { symbol: "CSCO", company: "Amazon.com Inc", readonly: true },
        { symbol: "MSFT", company: "Microsoft Corp.", readonly:false },
        { symbol: "INTC", company: "Intel Corp." },
        { symbol: "QCOM", company: "QUALCOMM Inc." },
        { symbol: "ORCL", company: "Oracle Corp." },
        { symbol: "HPQ", company: "Hewlett-Packard Company" },
        { symbol: "CRM", company: "salesforce.com, Inc." }
    ],
    pageSize: 5
});

And the grid defined as:

var grid = $("#stocks_tbl").kendoGrid({
    dataSource: dataSource,
    columns   : [
        { command: "edit", width: 100 },
        { field: "symbol", title: "Symbol", width: 100 },
        { field: "company", title: "Company", width: 300 }
    ],
    editable  : "popup",
    edit      : function () {
        alert("editing");
        grid.closeCell();
    },
    pageable  : true
}).data("kendoGrid");

If you run this you will see:

Capture1

So, there is nothing that we can do for avoiding the flickering but there are some things that I can do for getting the desired result.

Template based solution

This solution is based on defining a template that creates an Edit button for each row but controlled by readonly field.

The template would be something like:

<script id="edit-template" type="text/x-kendo-template">
    # if (!data.readonly) { #
    <a class="k-button k-button-icontext k-grid-edit" href="\#"><span class="k-icon k-edit"></span>Edit</a>
    # } #
</script>

and the new JavaScript code:

var editTemplate = kendo.template($("#edit-template").html());
var grid = $("#stocks_tbl").kendoGrid({
    dataSource: dataSource,
    columns   : [
        { field: "readonly", title: "", width: 100, template: editTemplate },
        { field: "symbol", title: "Symbol", width: 100 },
        { field: "company", title: "Company", width: 300 }
    ],
    editable  : "popup",
    pageable  : true
}).data("kendoGrid");

And this looks like:

Capture2

Done! … Actually almost, if I click on Edit button, I get:

Capture3

Where the field readonly is editable.

I might try to define a Schema for the DataSource and define readonly as not editable. But this does not prevent the field from being rendered.

Capture4

What else may I try?

  1. Remove the title of the field by defining it to a white-space using title: “ “ (note that there is a blank between both “).
  2. Define an editor function that actually does nothing.

What I have now is:

var grid = $("#stocks_tbl").kendoGrid({
    dataSource: dataSource,
    columns   : [
        { field: "readonly", title: " ", width: 100, template: editTemplate, editor: function() {} },
        { field: "symbol", title: "Symbol", width: 100 },
        { field: "company", title: "Company", width: 300 }
    ],
    editable  : "popup",
    pageable  : true
}).data("kendoGrid");

And when I edit the field I finally get:

Screen Shot 2012-12-26 at 00.43.28