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

Filtered Paged Sorted Customizable clientside Table

5.00/5 (5 votes)
27 Jun 2014CPOL5 min read 21.9K   519  
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!

Image 1

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

Image 2

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:

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

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

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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)