jqGrid with BackboneJS

In one of my past project we (team) used Backbone to give structure to web application and jqGrid for data display and manipulation. The data layer was RESTful JSON api and the problem that I faced that we were fetching data through Backbone’s RESTful JSON interface, however for other CRUD requests we wrote custom functions. So basically it was one-way data binding and once data arrived from server, the job of Backbone Collection was done.

As novice Backbone and jqGrid developer, I wondered why couldn’t we bind Backbone Collection to View (jqGrid) two-way, so that the collection get updated when we manipulate grid records and therefore synchronized with server automatically, leveraging Backbone. So, I googled and searched stackoverflow and found no help.

Now that I’m well up in Backbone and I know we can achieve that, I would like to share my-way :).

Let’s begin

Lets take the Library use case. We have books in library and each book has four attributes; the ISBN code, title of the book, author and the price. We will be adding, display, update, delete books in the library.

I’m presuming it single page demo, and all the required CSS/JS files are placed in header. In ideal case you should use RequireJS or browserify or some other means to load resources on demand with lesser http requests and asynchronously.

HTML

For simplicity, I’m not using navGrid add/edit/delete buttons, but rather buttons outside the grid (Yes we can use custom buttons in navGrid however for this tutorial my focus is not on presentation layer). There are four buttons, out of which some are hidden based on actions required; when edit button is clicked cancel and save buttons become visible and edit button get hidden and vice versa. We also have a form to add new book in the grid and no validation is done.

<table id="grid"></table>
<div id="pager"></div>
<p>
    <button type="button" id="btnEdit">Edit Grid</button>
    <button type="button" id="btnCancel" class="hidden">Cancel</button>
    <button type="button" id="btnSave" class="hidden">Save Grid</button>
    <button type="button" id="btnDelete">Delete Rows</button>
</p>
<form class="form">
    <legend>Add a new book:</legend>
    <label>Book Title: </label>
    <input type="text" name="title" id="inputTitle" />
    <label>Author: </label>
    <input type="text" name="author" id="inputAuthor" />
    <label>Price: </label>
    <input type="number" name="price" id="inputPrice" />
    <p><button type="submit">Add</button></p>
</form>

JavaScript

app.js

(function(){
    "use strict";
    //rest of the code here

    ...
})();

Common variable

We need to declare a variable $grid with application scope, which will hold the reference of table grid and used in Collection, View and jqGrid manipulations. We will assign it once the DOM will be loaded. If we put app.js in the bottom of the HTML page then we can assign it now.

    var $grid; // $grid = $('#grid');

Model

First we need to define a model named Book. Since we are using ISBN code as unique key, we tell Backbone to use isbn as ID attribute for the model. The same will be used for jqGrid rowID.

    var Book = Backbone.Model.extend({
            idAttribute : "isbn"
        });

Collection

We will now define a collection of model Book by the name Books. This is where we will define the actions to be taken (event listeners) when a model (read grid’s row) is either added or updated.

    var Books = Backbone.Collection.extend({
            model : Book,
            url : "path/to/rest/api",
            initialize : function () {
                this.on("change", this.changeMade, this);
                this.on("add", this.addedNew, this);
            },
            changeMade : function (model, options) {
                
                //Synchronize with REST server (a PUT request)

                //and on successful action, add it to grid

                Backbone.sync('update', model, {
                    success : function (model, result, xhr) {
                        $grid.jqGrid('saveRow', model.isbn, {
                            "url" : "clientArray"
                        });
                    },
                    error : function () {
                        console.log('update error');
                    }
                });
            },
            addedNew : function (model, options) {
                
                //Synchronize with REST server (a POST request)

                //1. Update collection with newly created row's ID

                //2. Add it to grid at first row

                Backbone.sync('create', model, {
                    success : function (response, result, xhr) {
                        model.id = response.isbn;
                        model.set('isbn', response.isbn);
                        $grid.jqGrid('addRowData', model.id, model.toJSON(), 'first');
                    },
                    error : function () {
                        console.log('add new error');
                    }
                });
            }
        });

View

Now that we have defined the collection, it is time to define the view with scope of entire HTML body (the scope can be restricted to particular div/section etc, which contains all action buttons). As we are handling the add/update events through collection, we will not make any modification in grid directly and rather make changes in collection, which in turn update the grid accordingly.

    var books = new Books;
    var Library = Backbone.View.extend({
            el : 'body',
            collection : books,

            // Bind event listeners to buttons and form

            events : {
                'click #btnEdit' : 'makeGridEditable',
                'click #btnSave' : 'saveGrid',
                'click #btnDelete' : 'deleteGridRecords',
                'click #btnCancel' : 'reloadGrid',
                'submit .form' : 'addNewRow',
            },

            //render:

            // 1. Fetch the latest records from REST server (a GET

            //    request)

            // 2. On successful completion render jqGrid

            // 3. The function 'renderGrid' is defined separately for

            // 4  readability purpose

            render : function () {
                books.fetch({
                    success : renderGrid, 
                    reset : true
                });
                return this;
            },

            // addNewRow:

            // 1. Get the form data

            // 2. Add it to collection 'books', which will trigger 'add'

            //    event and it's function addedNew

            addNewRow : function (e) {
                e.preventDefault();
                var data = {
                    'title' : document.getElementById('inputTitle').value,
                    'author' : document.getElementById('inputAuthor').value,
                    'price' : document.getElementById('inputPrice').value
                }
                if (data.title != '' || data.author != '' || data.price != '') {
                    books.add(data);
                } else {
                    console.log('all fields mandatory');
                }
            },

            // makeGridEditable:

            // 1. Make entire grid editable 

            // 2. Show Cancel and Save button

            // 3. Hide Edit button

            makeGridEditable : function (e) {
                var ids = $grid.getDataIDs();
                for (var i = 0, l = ids.length; i &lt; l; i++) {
                    $grid.jqGrid('editRow', ids[i]);
                }
                $('#btnCancel, #btnSave').removeClass('hidden');
                $('#btnEdit').addClass('hidden')
            },

            // saveGrid:

            // 1. Get the each row data and

            // 2. Get the corresponding model from collection

            // 3. Set the row data to model

            // 4. If the data is changed then it will trigger collection's

            //    'change' event and function 'changeMade' will be called

            // 5. Now, find out all rows where data is not changed and

            //    restore those rows to non-editable mode 

            saveGrid : function () {
                var ids = $grid.getDataIDs();
                for (var i = 0, l = ids.length; i &lt; l; i++) {
                     var rowData = $grid.jqGrid('getRowData', ids[i]);
                     var model = books.get(rowData.isbn);
                     model.set({
                         title : rowData.title,
                         author : rowData.author,
                         price : rowData.price
                     });
                     if (!model.hasChanged()) {
                         $grid.jqGrid('saveRow', ids[i], {
                             "url" : "clientArray"
                         });
                     }
                 }
                 $('#btnCancel, #btnSave').addClass('hidden');
                 $('#btnEdit').removeClass('hidden')
             },

             // deleteGridRecords:

             // 1. Get all the IDs of selected rows

             // 2. For each row selected, find the ISBN code

             // 3. If no ISBN code found then delete it from grid only

             // 4. Else destroy the corresponding model from collection. 

             //    The destroy function will send DELETE request to REST

             //    server. And on successful action, remove it from 

             //    grid as well.

             deleteGridRecords : function (e) {
                 var ids = $grid.jqGrid('getGridParam', 'selarrrow');
                 for (var i = ids.length - 1; i &gt;= 0; i--) {
                    var rowID = ids[i],
                    isbn = $grid.jqGrid('getCell', rowID, 'isbn');
                    if (isbn == '') {
                        $grid.jqGrid('delRowData', rowID);
                    } else {
                        var model = books.get(isbn);
                        if (model) {
                            model.destroy({
                                rowId : rowID,
                                success : function (model, response, xhr) {
                                    $grid.jqGrid('delRowData', xhr.rowId)
                                }
                            });
                        } else {
                            $grid.jqGrid('delRowData', rowID);
                        }
                    }
                }
            },
            
            // reloadGrid:

            // 1. Set the latest collection data to grid

            // 2. Then reload grid

            // 3. Reset the Edit/Save/Cancel buttons visibility

            reloadGrid : function (e) {
                $grid.jqGrid('setGridParam', {
                    datatype : 'local',
                    data : books.toJSON()
                });
                $grid.trigger('reloadGrid');
                $('#btnCancel, #btnSave').addClass('hidden');
                $('#btnEdit').removeClass('hidden')
            }
        });

Render jqGrid

We could define the function ‘renderGrid‘ inside view definition, however for readability and understanding purpose it is defined outside the block. When rendering the grid we need to make sure that ‘isbn‘ is being used as row ID. I also defined the custom unformatter for columns to get row data while in edit mode. Rest of the settings are regular for local data except that I defined function ‘beforeRefresh‘ for navGrid refresh icon to ensure that the grid gets refreshed with current records collection.

    function renderGrid(collection, response, options) {
        $grid.jqGrid({
            datatype : "local",
            data : collection.toJSON(),
            editurl : "clientArray",
            colNames : ['ISBN', 'Title', 'Author', 'Price'],
            colModel : [{
                    name : 'isbn',
                    index : 'isbn',
                    key : true, // rowID = isbn

                    editable : false
                }, {
                    name : 'title',
                    index : 'title',
                    editable : true,
                    unformat : function (cellvalue, options, cell) {
                        return $('input', cell).val() || cellvalue;
                    }
                }, {
                    name : 'author',
                    index : 'author',
                    editable : true,
                    unformat : function (cellvalue, options, cell) {
                        return $('input', cell).val() || cellvalue;
                    }
                }, {
                    name : 'price',
                    index : 'price',
                    align : 'right',
                    formatter : 'number',
                    editable : true,
                    unformat : function (cellvalue, options, cell) {
                        return $('input', cell).val() || cellvalue;
                    }
                }
            ],
            multiselect : true,
            gridview : true,
            rowNum : 10,
            rowList : [10, 20, 30],
            height : 'auto',
            pager : '#pager',
            sortname : 'isbn',
            viewrecords : true,
            sortorder : "desc",
            caption : "jqGrid Backbone"
        }).jqGrid('navGrid', '#pager', {
            edit : false,
            add : false,
            del : false,

            // beforeRefresh:

            // Set the latest data from collection before refreshing grid

            beforeRefresh : function () {
                $grid.jqGrid('setGridParam', {
                    datatype : 'local',
                    data : books.toJSON()
                });
            }
        });
    };

Execute Logic

Execute all the logic when DOM is ready

    $(document).ready(function () {
        $grid = $('#grid');
        var lib = new Library();
        lib.render();
    });

I will soon upload the working example online.

  1. John

    Hi,
    can You upload working example?
    Thanks

    1. Ovais

      Hi John, I do have in local but not uploaded yet. Actually, the problem was to setup as REST server on shared services. I’ll post files in Github and share the link with you.

  2. Eswaran Venkatesan

    Hi Ovais,
    I hope ur remember me, We worked for Toyota project. I need your help on backbone js and designing one screen. i called to your mobile. but its switched off. Please do let me know how to contact you..

    Have a nice day.
    Thanks & Regards,
    Eswaran Venkatesan

    1. Ovais

      Hi Eswaran, I’m in London now and local number is usually switched off. Send me your contact number and I’ll call you. 🙂

  3. Kannan

    Hi Buddy,

    Could you please upload the working code? It will be great help.

    Thanks
    Kannan

  4. Eswaran Venkatesan

    Hi Ovais,
    How are you? Hope doing good.
    I need a clarification on using technology stack..
    We are starting a new project with Front end as Angular JS with Bootstrap.
    Scenario is: We want to create a grid with paging. So which approach will be good to go: Grid using Angular JS or Grid using JQGrid .. Please Suggest

Leave a Reply

Your email address will not be published. Required fields are marked *