Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

SLGrid - jQuery/Knockout Client Component

4.86/5 (4 votes)
13 Oct 2014MIT3 min read 20.9K   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.

Image 1

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

JavaScript
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.

JavaScript
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.

JavaScript
 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

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

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

Use Default EntityListTemplate or Provide Your PersonListTemplate

HTML
< 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

HTML
<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 '.'.

JavaScript
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".

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

History

  • 13th October, 2014: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License