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.

  • Mark Morrison

    Hello:

    This is one of the best solutions I have come across: All of the other knockoutjs sort solutions I have seen does not specify the data type. Which often result in unpredictable results when sorting.

    With that said, I am attempting to use the knockout mapping plugin so that I can convert the JSON data to individual observables. This allows me to edit the data in the table and have it update else where.

    I cannot get the sorting to work if I do the following:

    var mappedData = ko.mapping.fromJS(data);
    var array = mappedData();
    self.countries(array);

    Attempting to sort the countries does not work?

    Is this possible? What I need is to map the data so that they are observable and than have the ability to edit by doing value or textInput binding.

    Great work!!

    • Thanks for your kind comment Mark!

      Not sure if I could answer your question without knowing what the structure of your data looks like and what the type of self.countries is.

      Do you have the code available anywhere so I can try and help debug?

      • Mark Morrison

        I figured it out: When passing observables from ko.mapping, I needed to add the opening/closing parenthesis on the property:

        Instead of this:

        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]);
        }

        Changed to this:

        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]());
        }
        The goal was to use ko.mapping so that I am able to edit each data element in the array as an observable.

        • Garrett Smith

          Mark, I was doing the same thing you were. Nice fix!

          Phil, great tutorial, especially your github example!

          • Garret,
            So glad you found it helpful! Thanks for stopping by.

  • Spence Prahl

    Nice work Phil! This came in handy!

    • Spence! No way, how did you find my blog? Glad it came in handy. Looking forward to working with you again some day.