Filtered Paged Sorted Customizable clientside Table





5.00/5 (5 votes)
A fully featured yet short (only 436 lines) replacement for datatables
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; // REMARK: to prevent event bubbling:
// data-bind="event: { click: function() {} }, clickBubble: false"
headerTemplate: string = null;
data: (row: any) => any = null; // function to get data for row
template: string = null; // template taking the row as parameter
dataTemplate: string = null; // template taking the cell data as value property, i.e. { value: data }
cellStyle: (row) => any = null; // add style to the TD
headerStyle: any; // add style to the TH visible = ko.observable(true);
sortable = ko.observable(true);
sort = ko.observable(KOGridSortOrder.None);
constructor(config?: IKOGridColumn) {
// code removed for clarity
}
}
class KOGridModel {
// data
columns: Array<KOGridColumn> = []; // REMARK: set that before itemsSource, or call update()
itemsSource = ko.observableArray();
// UI
inputClass = ko.observable<string>('form-control inline small');
// manipulation
paginations = ko.observableArray([5, 10, 25, 50, 100]);
pageSize = ko.observable(10);
currentPage = ko.observable(1);
filter = ko.observable<string>();
sortColumn = ko.observable<number>();
// templates for rendering
templateTable = "KOGridModelDefaultViewTemplate";
templateHeader = "KOGridModelDefaultViewHeaderTemplate";
// computed
currentItems: KnockoutComputed<Array<any>>;
currentPageItems: KnockoutComputed<Array<any>>;
maxPage: KnockoutComputed<number>;
numItems: KnockoutComputed<number>;
constructor(config?: IKOGridConfig) {
// code removed for clarity...
}
// force new evaluation of computed variables
update() {
}
// as the name say
sort(column: number) {
}
// change currentPage
moveTo(dst: KOGridNavigation) {
// code removed for clarity...
}
// set some default columns properties from the data
scaffold(data){
// code removed for clarity...
}
}
itemsSource
: contains all the rows displayedcolumns
: describes each columnheader
: title of the column (string
)headerTemplate
: instead of astring
, pass a knockout template to display anything, even HTML inputs!data
: Function(row), get data for celltemplate
: use ako
template, display anything. Data context will be the rowdataTemplate
: use ako
template, display anything. Data context will be the data for the cellcellStyle
:string
orobservable<string>
, style for theTD
headerStyle
:string
orobservable<string>
, header for theTH
visible
: whether the column is visible or notsortable
: whether the colum is sortable (need data to be nonnull
too)sort
: UI state, current sort state
inputClass
: class that would be applied to the search and pagination controlspaginations
: possible page size valuepageSize
: current page size (chosen from paginations)currentPage
: UI state, the current page of data displayedfilter
: UI whichstring
is used to filter the datasortColumn
: which column is currently sortedtemplateTable
: 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 sortingcurrentPageItems
: the items (fromcurrentItems
) on thecurrentPage
maxPage
: the number of page, i.e., the number ofcurrentItems
divided bypageSize
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