Client side Grid display, Editing, Paging, Resizing, Filtering and Sorting using Knockout and JQuery

0
68

Introduction

As paradigm shifted from web form (code behind) to client side script, most of the applications are developing on client side framework built on JavaScript. Knock out and JQuery is one of the popular client side scripts. In every application (mostly business application), we are required to show data in tabular structure along with different filter, paging, sorting, editing, resizing pertaining to table. To considering these mentioned features, I will try to cover all points in this article using Knockouts framework with the help of JQuery.

After completion of this article, the reader should be able to understand Knockout, JQuery, event,  function, selector etc.

The Knockout and JQuery scripts can be downloaded from below URLs.

http://knockoutjs.com/

https://jquery.com/

https://jqueryui.com/

Following are the core functionality which we will discuss.

  • Binding (Display)
  • Editing
  • Paging
  • Resizing
  • Filtering
  • Sorting

Binding (Display):

In knockout, we render table/Grid by applying binding (result collection) to the table’s body and loop through iteration. In each iteration, html content (tags) is rendered along with dynamic data bind with in </TD> (html tag).

First we need to create observable to hold and bind data.

var self = this;
self.items = ko.observableArray();
self.itemsTemp = ko.observableArray();

Now, we need to get data from the server. To achieve this, we will make an Ajax call. On success request, the result will be stored in both self.items and self.items.Temp. There is a reason to store same data in two observables which will be discussed later (Filtering stage) in this article.

self.loadData = function (from, count) {

 $.ajax({
 type: "POST",
 url: KoGrid.aspx/GetEmployeeList',
 contentType: "application/json;",
 data: "{'from':" + from + ",'count':" + count + "}",
 dataType: "json",
 success: function (results) {
 self.items(results.d.employeeuserList);
 self.itemsTemp(results.d.employeeuserList);
 },
 error: function (err) { alert(err.status + " - " + err.statusText); }
 });
 }

The code behind web method will get data from the database. In order to avoid complexity and focus on the grid, the dummy data is generated from the web method.

[WebMethod]

public static RetunObject GetEmployeeList(int from, int count){

 List<employeeuser> empList = new List<employeeuser>(){
 new employeeuser {userId=1, name = "A", price = 5.5, sales = 2 },
 new employeeuser {userId=2, name = "AB", price = 6.5, sales = 32 },
 new employeeuser {userId=3, name = "Al", price = 7.5, sales = 42 },
 new employeeuser {userId=4, name = "FAD", price = 8.5, sales = 52 },
 new employeeuser {userId=5, name = "Az", price = 9.5, sales = 62 },
 new employeeuser {userId=6, name = "VAF", price = 1.1, sales = 72 },
 new employeeuser {userId=7, name = "AG", price = 2.5, sales = 83 },
 new employeeuser {userId=8, name = "VAH", price = 3.2, sales = 92 },
 new employeeuser {userId=9, name = "AI", price = 4.5, sales = 102 },
 new employeeuser {userId=10, name = "AJ", price = 44.5, sales = 112 },
 new employeeuser {userId=11, name = "A", price = 5.5, sales = 2 },
 new employeeuser {userId=12, name = "VAB", price = 6.3, sales = 32 },
 new employeeuser {userId=13, name = "Al", price = 7.5, sales = 45 },
 new employeeuser {userId=14, name = "AD", price = 8.5, sales = 52 },
 new employeeuser {userId=15, name = "RAz", price = 9.4, sales = 62 },
 new employeeuser {userId=16, name = "AF", price = 1.5, sales = 78 },
 new employeeuser {userId=17, name = "AG", price = 2.5, sales = 82 },
 new employeeuser {userId=18, name = "FVAH", price = 3.5, sales = 92 },
 new employeeuser {userId=19, name = "AI", price = 4.5, sales = 102 },
 new employeeuser {userId=20, name = "AJ", price = 44.6, sales = 102 },
 new employeeuser {userId=21, name = "FA", price = 5.5, sales = 2 },
 new employeeuser {userId=22, name = "AB", price = 6.5, sales = 32 },
 new employeeuser {userId=23, name = "RAl", price = 7.5, sales = 12 },
 new employeeuser {userId=24, name = "AD", price = 8.7, sales = 59 },
 new employeeuser {userId=25, name = "Az", price = 9.5, sales = 62 },
 new employeeuser {userId=26, name = "AF", price = 1.5, sales = 73 },
 new employeeuser {userId=27, name = "VAG", price = 2.5, sales = 82 },
 new employeeuser {userId=28, name = "AH", price = 3.5, sales = 92 },
 new employeeuser {userId=29, name = "AI", price = 4.5, sales = 107 },
 new employeeuser {userId=30, name = "AJ", price = 44.8, sales = 112 }
 };

 return new RetunObject() { employeeuserList = empList.Skip(from).Take(count).ToList(),
 TotalCount = empList.Count() };
 }

 public class employeeuser {
 public int userId { get; set; }
 public string name { get; set; }
 public int sales { get; set; }
 public double price { get; set; }
 }

public class RetunObject{
 public int TotalCount { get; set; }
 public List<employeeuser> employeeuserList { get; set; }
}

After successfully retrieving data from Server into observable array, the following table will be rendered dynamically by iterating elements in itemsTemp observable.

<table id="tblHeading" border="1" style="width: 100%;"> <thead> <tr> <td> <a class="cursor bold " >UserId</a> </td> <td> <a class="cursor bold" >Name</a> </td> <td> <a class="cursor bold" >Price</a> </td> <td> <a class="cursor bold" >Sales</a> </td> <td> </td> </tr> </thead> <tbody data-bind="foreach: itemsTemp"> <tr> <td id="userId" data-bind="text: userId"></td> <td data-bind="text: name"></td> <td data-bind="text: price"></td> <td data-bind="text: sales"></td> <td> <button value: userId">Edit</button> <button value: userId">Delete</button> </td> </tr> </tbody>
</table>

Call the function on page load to load and render the grid.

self.formLoad = function () {
 self.loadData(0, 5);
 }
self.formLoad();

The following table will be rendered as output after applying binding model view with view.

$(document).ready(function () {
 ko.applyBindings(new PagedGridModel());
 });

Here is the output.

Editing:

After successfully grid creation, now let’s discuss edit functionality of grid. Simply we need to bind click event with relevant functions at Edit and Delete button.

<button data-bind="click: $root.EditClick, value: userId">Edit</button>
<button data-bind="click: $root.DeleteClick, value: userId">Delete</button>

The following functions will be called on edit and delete click respectively. Here we are only showing alert with row id. You can use it as per your requirement.

self.EditClick = function (a) {
 alert("UserId " + a.userId + " edit");
 }
 self.DeleteClick = function (a) {
 alert("UserId " + a.userId + " delete");
 }

Here is the output on click on Edit or Delete button

Paging:

The grid’s paging is one of the feature which is used when there is a huge amount of records to display. We can perform paging in the grid by adding few lines of code.

First add paging observable array in view model.

self.itemsPaging = ko.observableArray();
self.pageSize = ko.observable();

The self.pageSize is the size of grid, which will be filled on load as default value or when user will change grid size value. This will be discussed more on Resizing stage.

Now create paging function which will get paging range values in self.itemsPaging array.

self.paging = function (totalRecord) { self.itemsPaging([]); var filtercount = self.pageSize(); var totalIteration = Math.ceil(totalRecord / filtercount); if (totalIteration > 1) { for (var i = 1; i <= totalIteration; i++) { self.itemsPaging.push(i); } } }

Now the following html will loop through iteration. In each iteration, a span with page number will be bind as display value with click event(“moveToPage”).

<div data-bind="foreach: itemsPaging" style="font-size: xx-large; width: 100%;"> &nbsp;<span style="background-color: lightgrey"> <a href="#" style="margin: 15px;" data-bind="text: $data, click: $root.moveToPage "></a> </span>&nbsp;
</div>

Here is the output after paging implementation.

One more thing is remaining in complete functionality of paging .On click on page number, get data accordingly from server or locally. In order to achieve this, we have bind “moveToPage” function with each number. In moveToPage, we are getting the number of page to visit and calculate the number by Page size and page number. After this, we are simply calling “loadData” function which will bring data from server. The same thing can be achieved by getting all data from server in one go and hold it in temporary observable array.

self.moveToPage = function (PageNumber) { var count = self.pageSize(); var from = (PageNumber - 1) * count; self.loadData(from, count); }

Resizing:

Grid resizing provides the functionality to restrict the number of records on a page. The paging functionality is directly associated with resizing because as we change the size (number of records) of page, the paging numbers below the grid will also update. To implement resizing functionality is also a matter of writing some lines of code.

Add dropdown (Select) on the form with some options for sizing.

<b>Page Size: </b> <select id="Scount" data-bind="value: pageSize, event: { change: pageSizeChange }"
          class="ddlWidth260 ddl"> <option value="5">5</option> <option value="10">10</option> <option value="20">20</option> <option value="50">50</option> </select>

Now we need to bind the select (drop down) value with observable in order to get current size value.

Here we will use “pageSize “observable which we have mentioned before. After this, bind select with     “PageSizeChange” function on change event. In pageSizeChange function, we are simply calling the records from zero (0) to size of page and paging will also be reset accordingly.

self.pageSizeChange = function () { self.loadData(0, self.pageSize()); }

Filtering:

Another very important functionality pertaining to grid is filtering records. Again, we can make it possible to filter the records by rendering textbox (input) on grid header and bind with relevant event which is “filtering”.

First we need to add input, assign a class of “filter” along with “Keyup” event and column name property so that it can identify which column needs to be filtered, on all those </TD> (html tag)  where we want to show filter functionality. We can do this by adding following html in table.

<thead>
 <tr>
 <td>
 <a class="cursor bold " data-bind="click: $root.sortGrid.bind(this, 'userId')">UserId</a><br />
 <input type="text" class="width100 filter" columnname="userId" 
           data-bind="event: { keyup: $root. filtering.bind(this, 'userId') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'name')">Name</a><br />
 <input type="text" class="width100 filter" columnname="name"
           data-bind="event: { keyup: $root. filtering.bind(this, 'name') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'price')">Price</a><br />
 <input type="text" class="width100 filter" columnname="price"
           data-bind="event: { keyup: $root. filtering.bind(this, 'price') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'sales')">Sales</a><br />
 <input type="text" class="width100 filter" columnname="sales" 
          data-bind="event: { keyup: $root. filtering.bind(this, 'sales') }" />
 </td>
 <td>
 </td>
 </tr>
</thead>

Then create a function with the name of “filtering” where we will filter the data based on user input. In this function, first we will get all inputs having class “filter”. As we have discussed earlier that the same data (result collection) will be stored in two observables. The reason of doing this is to hold data for user filtering. If we will store data in one observable array and user filters some data, it will filter the result but what if, user removes all filters then we do not have complete data as we have filter it. That’s the reason to store data in two observable so that if user will apply filtration on grid and then reset grid, the data from second observable (self.items()) will be stored to self.itemsTemp. By doing this, we will avoid unnecessary calling to the server.

Now all fetched input will iterate for applying relevant filtration over grid and in last the resultant data will be bind again to the table.

self.itemsTemp(self.items());

Here is the output result before filtering.

Output result after applying filtration.

Output result after reset/remove filtration.

Sorting:

Another important functionality of grid is sorting. In knock Out or any other client framework, we can achieve sorting very easily by binding event with header anchor text and pass column name in it so that we can identify the column to which we will sort the data .

<thead>
 <tr>
 <td>
 <a class="cursor bold " data-bind="click: $root.sortGrid.bind(this, 'userId')">UserId</a><br />
 <input type="text" class="width100 filter" columnname="userId" 
           data-bind="event: { keyup: $root.filtering.bind(this, 'userId') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'name')">Name</a><br />
 <input type="text" class="width100 filter" columnname="name" 
           data-bind="event: { keyup: $root.filtering.bind(this, 'name') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'price')">Price</a><br />
 <input type="text" class="width100 filter" columnname="price"
           data-bind="event: { keyup: $root.filtering.bind(this, 'price') }" />
 </td>
 <td>
 <a class="cursor bold" data-bind="click: $root.sortGrid.bind(this, 'sales')">Sales</a><br />
 <input type="text" class="width100 filter" columnname="sales"
          data-bind="event: { keyup: $root.filtering.bind(this, 'sales') }" />
 </td>
 <td>
 </td>
 </tr>
</thead>

And finally create binding event which will sort records based on that column.

self.sortGrid = function (data, obj) {
 self.itemsTemp.sort(function (a, b) {
 var x = a['' + data];
 var y = b['' + data];
 if (x < y) { return -1; }
 if (x > y) { return 1; }
 return 0;
 });
 }

Here is the grid sorted on price Column.

LEAVE A REPLY