Sorting Tables Using KnockoutJS

I recently came up with a useful method for sorting tables using KnockoutJS that I felt was worth sharing. Supporting sorting of a grid/table is a pretty common feature you see on the web. However, most implementations are using an AJAX callback (or sometimes a full postback) to request the data in the desired sort pattern. The example I outline here uses KnockoutJS’ client side data binding and dependency tracking to sort efficiently without extra trips to the server. (See Demo)

Setting up the ViewModel

There are two key object properties on the viewmodel that make the sorting possible: columns (self.columns) and data (self.players). Since we are using KnockoutJS, both of the objects are instances of observableArrays:

// Observable array that represents each column in the table
self.columns = ko.observableArray([
    { property: "firstName", header: "First Name", type: "string", state: ko.observable("") },
    { property: "lastName", header: "Last Name", type: "string", state: ko.observable("") },
    { property: "dob", header: "Date of Birth", type: "date", state: ko.observable("") },
    { property: "wsWon", header: "World Series Championships", type: "number", state: ko.observable("") },
    { property: "stats.hr", header: "Home Runs", type: "object", state: ko.observable("") },
    { property: "stats.avg", header: "Batting Average", type: "object", state: ko.observable("") }
]);

// Observable array that will be our data
self.players = ko.observableArray([
    { firstName: "Micky", lastName: "Mantle", dob: "10/20/1931", wsWon: "7", stats: { hr: "536", avg: ".298" } },
    { firstName: "Ken", lastName: "Griffey Jr.", dob: "11/21/1969", wsWon: "0", stats: { hr: "630", avg: ".284" } },
    { firstName: "Derek", lastName: "Jeter", dob: "6/26/1974", wsWon: "5", stats: { hr: "260", avg: ".310" } },
    { firstName: "Lenny", lastName: "Dykstra", dob: "2/10/1963", wsWon: "1", stats: { hr: "81", avg: ".285" } },
    { firstName: "Ty", lastName: "Cobb", dob: "12/18/1886", wsWon: "0", stats: { hr: "117", avg: ".367" } }
]);

Notice how each object in the self.columns observable array has a key “property” that’s value corresponds to a key (or in some cases a child object and key) in the object that makes of the observable array of self.players. It is this relationship that we will capitalize on.

Binding Data in the View

In the view, we use two separate KnockoutJS “foreach” data binds to define our table. The first in the table header that builds the headers for each column. The second in the table body that fills the table body with rows of data:

After the bindings are applied (and a little Twitter Bootstrap CSS) the output in the browser looks like this:Sortable Table

Performing Sorts for Different Data Types

One of key features in this implementation is ability to sort different data types. JavaScript’s Array.sort method is a handy feature (read more about it here) of the language but unfortunately needs different compare functions for different data types. Below are the different sorts based on data type:

 // Generic sort method for numbers and strings
 self.stringSort = function (column) { // Pass in the column object

     self.players(self.players().sort(function (a, b) {

         // Set strings to lowercase to sort in a predictive way
         var playerA = a[column.property].toLowerCase(), playerB = b[column.property].toLowerCase();
         if (playerA < playerB) {
             return (column.state() === self.ascending) ? -1 : 1;
         }
         else if (playerA > playerB) {
             return (column.state() === self.ascending) ? 1 : -1;
         }
         else {
             return 0
         }
     }));
 };

 // Sort numbers
 self.numberSort = function (column) {
     self.players(self.players().sort(function (a, b) {

         var playerA = a[column.property], playerB = b[column.property];
         if (column.state() === self.ascending) {
             return playerA - playerB;
         }
         else {
             return playerB - playerA;
         }
     }));
 };

 // Sort by date
 self.dateSort = function (column) {

     self.players(self.players().sort(function (a, b) {

         if (column.state() === self.ascending) {
             return new Date(a[column.property]) - new Date(b[column.property]);
         }
         else {
             return new Date(b[column.property]) - new Date(a[column.property]);
         }
     }));
 };

 // Using a deep get method to find nested object properties
 self.objectSort = function (column) {

     self.players(self.players().sort(function (a, b) {

         var playerA = self.deepGet(a, column.property),
         playerB = self.deepGet(b, column.property);

         if (playerA < playerB) {
             return (column.state() === self.ascending) ? -1 : 1;
         }
         else if (playerA > playerB) {
             return (column.state() === self.ascending) ? 1 : -1;
         }
         else {
             return 0
         }
     }));
 };

See the Code In Action

To see this example in action please check out the demo. The full code is also available on GitHub.