65.9K
CodeProject is changing. Read more.
Home

SLGrid - jQuery/Knockout Client Component

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (4 votes)

Oct 13, 2014

MIT

3 min read

viewsIcon

20880

downloadIcon

506

SLGrid - jQuery/Knockout client component for CRUD operations

Try it online

Introduction

SLGrid is jQuery/Knockout client component. It can be used in any web development platform: ASP.NET/MVC, PHP, Java, ... and with any database system. It has standard grid features like sorting, paging and filtering. SLGrid uses Knockout which enables each grid cell to optionally contain Knockout Widget like: DropDown, Date picker and others, bound to columns (fields) of the JavaScript Objects. Many Knockout Widgets like: Money, Percentage, Image, ... can be found in public libraries on the net. For CRUD operations, SLGrid generates templates which are applied in Bootstrap popovers.

Background

SLGrid is a client component which communicates with any of database systems using Ajax calls. It uses custom database adapters like: in-memory adapter, Ajax adapter, LighStreamer adapter ...

SLGrid uses Knockout which does declarative bindings connecting parts of UI to data model.
Because of declarative bindings, the same entity model (document) can be bound to different Views like:

  • Row View
  • Row Edit View
  • Form View
  • Form Edit View

That's why data rows need to be out of the grid, and data rows are located into the ListView.

SLGrid uses Knockout with the following advantages:

  • Very fast update of grid cells, because DOM element contained in grid cell is already bound to JavaScript Field (Column). No need for getDocumentById() upon each change.
  • We modify JavaScript array: Add/Update/Delete row, Knockout reflects visual changes in grid.
  • Each cell can contain reusable Widget

Use this library as the startup point, customize it for your necessities. For example, there are a few ways to present detail form when user clicks on row edit button:

  • Display form in Bootstrap Popover
  • Display form in expanding panel below the row
  • Display form in separate page (with encoded URL for getting back)
  • ...

SLGrid uses object oriented (inheritance) programming of JavaScript and a few design patterns like: Module pattern, ... A great deal of code for CRUD functionality is located in base classes. It is very useful for CRUD operations with many entities (Admin modules).

JavaScript Prototypal Inheritance

    var SLEntity = function () {
        this.whichTemplate = function() {
            return this.displayMode == "edit" ? "person-edit-template" : "person-view-template"
        }
    }

    var Person = function (mode) {
        // instance properties
        // method whichTemplate(), in base class SLEntity, uses displayMode property
        this.displayMode = mode;
    }

    //  Person extends SLEntity
    Person.prototype = new SLEntity();

    var person =  new Person("edit");
    alert(person.whichTemplate());

Module Pattern

The module pattern overcomes the limitations of the object literal, offering privacy for variables and functions while exposing a public API. In our case, only PersonList.viewModel is accessible from outer space.

    function SLGridViewModel(configuration) {
        this.itemsAtPage = ko.observableArray([]);
        ...
    }

    var PersonList = (function (DB) {
        var db = DB;

        var StorePerson  = function (data) { 
            db.StorePerson(data) 
        }

        var UpdatePerson = function (data) { 
            db.UpdatePerson(data) 
        }

        function GridViewModel() {
            ...
        }

        GridViewModel.prototype = new SLGridViewModel();

        return {
            viewModel: new GridViewModel()
        }

    })(new PersonDB());      

Using the Code

The core functionality, from definitions to rules and business logic, is located in entity class like Person.

Define Classes: Person, PersonList, PersonDB

Usually, these classes are generated as separate js files by SLGen generator.

     var Person = function (data) {
        this.PersonId = ko.observable(data.PersonId || 0)
        this.Name = ko.observable(data.Name || "")

        this.TwitterName = ko.observable(data.TwitterName || "")

        this.CityId = ko.observable(data.CityId || 0)
        this.chosenCity = ko.computed(function () {
            return CityList.getCity(this.CityId())
        }, this);

        // instance properties, functionality is into the base (Entity) class
        this.isNew = ko.observable(data.isNew || false);
        this.rowDisplayMode = ko.observable(data.isNew ? "rowAdd" : Person.DEFAULTS.rowDisplayMode);
    }

    //  Person EXTENDS SLEntity
    Person.prototype = new SLEntity();

    Person.prototype.PersonId = ko.observable(0)
        .extend({
            primaryKey: true,
            //headerText: "Id",
            formLabel: "Id",
            width: "100px",
            defaultValue: function () { return this.getNextId() }
        });

    Person.prototype.Name = ko.observable("")
        .extend({
            headerText: "Name",
            formLabel: "Name",
            presentation: "bsPopoverLink", // view form bootstrap popover
            width: "200px",
            defaultValue: "",
            required: true,
            minLength: 2,
            pattern: { message: 'Please, start with uppercase !', params: '^([A-Z])+' }
        });

    Person.prototype.TwitterName = ko.observable("")
        .extend({
            headerText: "Twitter",
            formLabel: "Twitter name",
            width: "auto", // one of the columns with take rest of row space
            defaultValue: ""
        });

    Person.prototype.CityId = ko.observable(0)
        .extend({
            defaultValue: 101
        });

    Person.prototype.chosenCity = ko.observable()
        .extend({
            headerText: "City",
            formLabel: "City",
            width: "200px",
            presentation: "bsSelectCity"
            //presentation: "bsTypeaheadCity"
        }); 

Add View Models to Global View Model

        var globalViewModel = {
            ...
            cityList: CityList.viewModel,
            personList: PersonList.viewModel
        }

        $(document).ready(function () {
            ko.applyBindings(globalViewModel);
        });

Use Default EntityListTemplate or Provide Your PersonListTemplate

    < script type="text/html" id="EntityListTemplate">
        <div style="float:left">
            <h4 data-bind="text:listHeader"></h4> 
        </div>

        <div style="clear:both"></div>

        <div style="width:100%"> 
            <div data-bind="SLGrid: $data" /> 
            <div>
                <div style="float:left">
                    <button class="btn btn-primary btn-xs" 
                        data-bind="click: add, enable: canAdd, text:textAdd"></button>
                </div>
                <div style="float:right;" data-bind="SLGridPager: $data">
                </div>
                <div style="float:right; margin-right:20px;">
                    <span data-bind='text: nItems'></span> 
                    <span data-bind='text:textPager'></span>
                    <span data-bind="visible:maxPageIndex()>=itemsOnPager()">, 
                    <span data-bind='text:maxPageIndex()+1'></span> pages</span>
                </div>
                <div style="clear:both"></div>
            </div>
        </div>
    </script>

Bind Template to PersonList.viewModel

    <div data-bind="template: { 
                name: 'EntityListTemplate',
                data : personList, 
                afterRender: personList.afterRender }" >
    </div>

Knockout Widgets

Knockout enables creation of custom bindings. This is how to control how observables interact with DOM elements, and gives you a lot of flexibility to encapsulate sophisticated behaviors in an easy-to-reuse way.

SLGrid cell can contain Knockout Widget like: DropDown, Date picker, ... to present Column (Fields) value in display and edit modes. Here is the example of widget which displays float value with thousands separated by '.'.

ko.bindingHandlers.bsMoney = {

    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var mode = viewModel.rowDisplayMode()
        var inRow = allBindings.get("inRow");
        var isViewMode = inRow && viewModel.isRowViewMode() || !inRow && viewModel.isViewMode()

        if (!isViewMode) {
            var colName = $(element).data("field");
            $(element).html("<input style='text-align:right' 
            class='form-control' data-field='" + colName 
            + "' data-bind=\"value: " + colName + 
            "().toFixed(2), valueUpdate:'keyup'\">")
        }
    },

    update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var value = ko.utils.unwrapObservable(valueAccessor())
        var mode = viewModel.rowDisplayMode()
        var inRow = allBindings.get("inRow");
        var isViewMode = inRow && viewModel.isRowViewMode() || !inRow && viewModel.isViewMode()

        if (isViewMode) {
            var tokens = value.toFixed(2).replace('-', '').split('.');
            var s = '$' + $.map(tokens[0].split('').reverse(), function (elm, i) {
                return [(i % 3 === 0 && i > 0 ? ',' : ''), elm];
            }).reverse().join('') + '.' + tokens[1];
            var markup = value < 0 ? '-' + s : s;
            $(element).html(markup)
        }
    }
}

That widget can be easily applied to field using property presentation:"bsMoney".

    Person.prototype.Amount = ko.observable()
        .extend({
            headerText: "Amount",
            formLabel: "Amount",
            defaultValue: 0,
            presentation: "bsMoney",
            align : 'right'
        });

History

  • 13th October, 2014: Initial version