Click here to Skip to main content
Click here to Skip to main content

Filtered paged sorted customizable clientside table

, 27 Jun 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
A fully featured yet short (only 436 lines) replacement for datables

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 customized at this stage!
Here it is for your Web developement 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 developement 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 developement 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 produce 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 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 row displayed.
  • columns: describe 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 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 (choosen 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 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 to observable change (through the subscribe() method) just use computed function. They are automatically called if any observable they uses 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 an 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 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!

Original release on the 25/06/2014.

License

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

Share

About the Author

Super Lloyd
Software Developer (Senior) http://www.radicalsystems.com.au
Australia Australia
The Australia born French man who went back to Australia later in life...
Finally got over life long (and mostly hopeless usually, yay!) chronic sicknesses.
Worked in Sydney, Brisbane, Darwin, Billinudgel, Darwin and Melbourne.
Follow on   Twitter

Comments and Discussions

 
QuestionHI PinprofessionalSibeesh KV9-Oct-14 4:11 
AnswerRe: HI PinmemberSuper Lloyd3-Dec-14 3:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 27 Jun 2014
Article Copyright 2014 by Super Lloyd
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid