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) {
this.displayMode = mode;
}
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);
this.isNew = ko.observable(data.isNew || false);
this.rowDisplayMode = ko.observable(data.isNew ? "rowAdd" : Person.DEFAULTS.rowDisplayMode);
}
Person.prototype = new SLEntity();
Person.prototype.PersonId = ko.observable(0)
.extend({
primaryKey: true,
formLabel: "Id",
width: "100px",
defaultValue: function () { return this.getNextId() }
});
Person.prototype.Name = ko.observable("")
.extend({
headerText: "Name",
formLabel: "Name",
presentation: "bsPopoverLink",
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",
defaultValue: ""
});
Person.prototype.CityId = ko.observable(0)
.extend({
defaultValue: 101
});
Person.prototype.chosenCity = ko.observable()
.extend({
headerText: "City",
formLabel: "City",
width: "200px",
presentation: "bsSelectCity"
});
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