Introduction
One day, after many more hours trying to customize my knockout binding for datatables further, I had had enough of it! And decided to write my own, knockout friendly, datatable.
Good that I did! It took only 12 hours and 436 lines of code! Moreover, it's quite simple to customize at this stage!
Here, it is for your Web development benefit!
Background
For our line of business app, we need a good looking grid that can be sorted and filtered. For better user experience, it should do that on the client side. For better development experience, it should be simple to setup (i.e., simply specify the member field to display in each column) and flexible (or provide a function or even a knockout template). Columns should be hidable. And ideally, it should be data-driven (i.e., no jQuery event, just set your control's property in JavaScript and the Grid updates! It's called MVVM programming).
The technologies I decided to rely on for this control are HTML & TypeScript (it's a web app!), Bootstrap (to make the table look good), and Knockout (to enable MVVM development style).
My starting point was the Paged Grid sample on the Knockout examples section, which served me loosely as a source of inspiration.
While I developed this component with VS2013 Update 2 as my IDE, (which includes the TypeScript compiler), the component is ready to go. Double click on TestPage.html to start it. KOGridBinding.js (included in the .zip file) is all you need if you can't or won't use TypeScript.
What's in the Project
Apart from the style files (it's a web app!) and the test files (of course), there are 2 source files:
- knockout.d.ts, the TypeScript Knockout binding, from NuGet. Its only use is for TypeScript to do fully typed call to the Knockout API. It produces no JavaScript file and is not used at runtime, only at compile time.
- KOGridBinding.ts, the source for the grid, the whole 380 lines of it that are going to be summarily explained below.
Alternatively, you can skip the TypeScript version and directly use the (pre-build) JavaScript version. All you need is KOGridBinding.js (included) and the style files.
Using the Code
To make use of the grid, simply call the "kogrid
" binding on an appropriately styled TABLE
tag, as in:
<table class="table table-striped table-bordered table-hover table-condensed"
data-bind='kogrid: gridViewModel'> </table>
The classes here are for bootstrap table styling. The data passed to the kogrid
binding MUST be a KOGridModel
(as defined in KOGridBinding.ts) and an exception will be thrown as a reminder if it's not the case.
The KOGridModel
class has all the properties to describe the state of the grid.
class KOGridColumn {
header: string = null;
headerTemplate: string = null;
data: (row: any) => any = null;
template: string = null;
dataTemplate: string = null;
cellStyle: (row) => any = null;
headerStyle: any;
sortable = ko.observable(true);
sort = ko.observable(KOGridSortOrder.None);
constructor(config?: IKOGridColumn) {
}
}
class KOGridModel {
columns: Array<KOGridColumn> = [];
itemsSource = ko.observableArray();
inputClass = ko.observable<string>('form-control inline small');
paginations = ko.observableArray([5, 10, 25, 50, 100]);
pageSize = ko.observable(10);
currentPage = ko.observable(1);
filter = ko.observable<string>();
sortColumn = ko.observable<number>();
templateTable = "KOGridModelDefaultViewTemplate";
templateHeader = "KOGridModelDefaultViewHeaderTemplate";
currentItems: KnockoutComputed<Array<any>>;
currentPageItems: KnockoutComputed<Array<any>>;
maxPage: KnockoutComputed<number>;
numItems: KnockoutComputed<number>;
constructor(config?: IKOGridConfig) {
}
update() {
}
sort(column: number) {
}
moveTo(dst: KOGridNavigation) {
}
scaffold(data){
}
}
itemsSource
: contains all the rows displayed columns
: describes each column
header
: title of the column (string
) headerTemplate
: instead of a string
, pass a knockout template to display anything, even HTML inputs! data
: Function(row), get data for cell template
: use a ko
template, display anything. Data context will be the row dataTemplate
: use a ko
template, display anything. Data context will be the data for the cell cellStyle
: string
or observable<string>
, style for the TD
headerStyle
: string
or observable<string>
, header for the TH
visible
: whether the column is visible or not sortable
: whether the colum is sortable (need data to be non null
too) sort
: UI state, current sort state
inputClass
: class that would be applied to the search and pagination controls paginations
: possible page size value pageSize
: current page size (chosen from paginations) currentPage
: UI state, the current page of data displayed filter
: UI which string
is used to filter the data sortColumn
: which column is currently sorted templateTable
: KO template for rendering this model. There is a default one provided (as seen on the picture) templateHeader
: KO template to render the control header (for search and pagination) there are 2 default one provided
Additionally, some UI state properties are automatically computed from the combination of: filter + sortColumn + itemsSource + currentPage + pageSize
:
currentItems
: itemsSource
after filtering and sorting currentPageItems
: the items (from currentItems
) on the currentPage
maxPage
: the number of page, i.e., the number of currentItems
divided by pageSize
numItems
: the number of items after filtering
The grid model can be initialized with a model like JSON object and will fill in missing properties, such as the code in the sample app:
this.gridViewModel = new KOGridModel({
data: this.items,
columns: [
{ header: "Item Name", data: "name" },
{ header: "Sales Count", data: "sales" },
{ header: "Price", data: function (item) { return "$" + parseFloat(item.price()).toFixed(2) } },
{ headerTemplate: "headerPrice", template: "columnPrice", visible: this.showPriceEdit }
],
pageSize: 4
});
Points of Interest
This code is a good starting point to look at a real life, yet simple, custom knockout binding.
The AddTemplate()
function from the original ko sample gave me a novel idea to provide my template from JavaScript!
function addTemplate(name, markup) {
document.write("<script type='text/html' id='" + name + "'>" + markup + "<" + "/script>");
};
Computed! I really need to emphasize that, Computed! They are awesome! Don't subscribe to observable change (through the subscribe()
method), just use computed function. They are automatically called if any observable they use change.
Subtle use of Bootstrap for good looking control (look no further than the filter and pagination header) is a must.
I had to solve the problem of recomputing observable when they were using some non observable (such as when one set itemsSource
, instead of itemsSource()
to share the items
array with the model). Look at how I implemented update()
. There is a hidden observable which is changed in update()
and requested in the currentItems()
computed.
Summary
Here, I introduce a new Knockout binding extension: the "kogrid
" and its associated data model, the "KOGridModel
". Together, they make it excessively simple to have good looking, customizable, filtered, sorted datagrid
.
History
- 27/06/2014: After 2 days of heavy usage, I found a couple of missing features and bugs. Like custom TD style, nicer pagination control, sorting of boolean, slow bulk operations on sorted column or while filtered, out of the box working sample (without VS). This latest version fixes them!
- 25/06/2014: Original release