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

A jQuery Plugin for Sorting HTML Tables

4.85/5 (9 votes)
18 Apr 2016CPOL10 min read 22.6K   320  
In this article I will describe the approach I took in creating a jQuery Plugin for sorting HTML tables.

Image 1

Figure 1. Demonstration Screen Shot.Table sorted on DateReceived in descending order

Introduction

This jQuery plugin sorts HTML by the values in a selected column. It can be attached to the header row of a table so that when the user clicks on a column header, the table is sorted on that column. If the user clicks the column header again, the sort is reversed. The developer can also implement sorts based on other events and restore the table to its original order. The Plugin recognizes numeric columns and date columns and sorts accordingly.

Background

I had a Hall of Fame table in a browser based word game that I wrote. It needed a sort capability so I created this plugin.

Limitations

  • It doesn't work properly with tables that use "colspan" or "rowspan".
  • It only recognizes dates with numeric components and a single delimiter. The default delimiter is '/' but that can be changed using an optional setting. It recognizes US (mm/dd/yy][yy]), British dates (dd/mm/yy[yy]) and European dates ([yy]yy/mm/dd). It doesn't care if dates are not strictly valid - that is the job of the application that populates the table in the first place. It examines every date in a column to determine the date format. If there are only a few dates and they are ambiguous, then it may get the format wrong. if your table is small, you may want to use the optional setting to specify the date format.
  • If it finds a cell value in a column that doesn't look like a number or a date, it treats the complete column as a column of string values.
  • It slows down when tables contain thousands of rows.

Advantages

  • It is easy to add the ability to sort an HTML table to a web page.
  • It recognizes dates and numbers and sorts them correctly. There is an option to explicitly specify the date format.
  • The sort retains the original table order within any sort. This means it is stable.
  • Tables can be returned to their original sort order.
  • It doesn't impact the HTML structure of the table.
  • It uses jQuery and the JavaScript Array sort method, so it will work with most browsers.
  • It is almost instantaneous on tables under a thousand rows in modern browsers.
  • String sorts are case sensitive, but there is an option to override that.
  • No server activity is required.

Demonstration Web Pages

There are two demonstration web pages included in the download.

DemoTable.html

This page contains a table with data that looks realistic. See figure 1. The table has some simple styling to make it OK nice. It has four buttons underneath it to illustrate basic functionality of the Plugin. The first lets you restore the table to its original order. The next three let you choose the date format for the Date Received column so you can verify the automatic date format selection algorithm. There is some code to total the Price and Miles columns, but that is just to populate the footer row.

BigDemoTable.html

This page contains a table with 3000 rows of data. See figure 2. It is intended for testing all the options and for performance testing. The table also has some simple styling to make it look OK. Sorting on the RANDOM before sorting on another column effectively puts every other column in random order. There is some code to generate the EURO NUM column for testing a possible European number format.

Image 2

Figure 2. Demonstration Screen Shot. Large table sorted randomly.

Adding a reference to your web page

Assuming you place the tablesort.js JavaScript file in a subfolder named js, you would add the following line to your web page to reference the TableSort jQuery Plugin.

JavaScript
<script src="js/tablesort.js" type="text/javascript"></script>

Using the code

The easiest way to use the Plugin is to attach a click event to every header cell in your HTML table(s) to invoke the plugin. If we use a selector like $('#DemoTable tr:first').children(), we don't have to worry about whether the header row uses <td> or <th> tags. The code sample below assumes the HTML table already exists when the page has finished loading. If you are loading the table using an AJAX call, or building the table dynamically, then you would attach the column header events after the table is created.

JavaScript
<script type="text/javascript">
<!--
$(document).ready(function () {
    // Select your column headers
    $('#DemoTable tr:first').children().click(function (e) {
        // Attach a click event to each cell to invoke the Plugin
        $('#DemoTable').sortByColumn($(this).text());
    });
});
// --></script>

If you want to return the table to its original state, just invoke the Plugin without specifying a column header.

JavaScript
$('#DemoTable').sortByColumn();

Options

Instead of passing in the column header text, you can pass in an object literal that specifies an extended set of options. You only need to supply the ones you want. The options are:

JavaScript
columnText
dateFormat
dateDelimiter
decimalDelimiter
caseSensitive
ascendingClass
descendingClass

The columnText option must match the text in the first cell of a header column.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text()});

The dateFormat option can be any value but only 'US', 'UK' and 'EU' are recognized. The default is '' and the Plugin will determine which date format to use based on the contents of the column.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), dateFormat: 'UK'});

The dateDelimiter option can be set to a single character used to delimit date components. The default is '/'.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), dateDelimiter: '-'});

The decimalDelimiter option can be set to a single character used to delimit the fractional part of a decimal number. The default is '.'.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), decimalDelimiter: ','});

The caseSensitive option can be set to true or false. The default is true.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), caseSensitive: false});

The ascendingClass option is the name of the class that will be added to the header cell of the current sort column if it is in ascending order. The descendingClass option is the name of the class that will be added to the header cell of the current sort column if it is in descending order. In both cases, the default is ''.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), ascendingClass: 'redbordertop', descendingClass: 'redborderbottom'});

How it works

The Plugin extracts the data in the table into an array of objects. It invokes the JavaScript Array.Sort method to sort the array. Then it inserts the data from the sorted array of objects back into the table.

This is in contrast to the last implementation I worked with, which exchanged DOM elements and used a hard-coded sort algorithm. It started with a bubble-sort but I switched it to a comb sort to improve performance.

The Plugin identifies the column clicked by matching the jQuery text() of the column with the string value passed to the Plugin. It strips whitespace before making the comparison. It is recommended that you use the jQuery text method to specify the sort column.

It uses the jQuery data method with a key of _dir_ to store the current sorted state of a column. The values can be a for ascending, d for descending, or undefined. If your jQuery selector has returned column header cells, then you could determine the sort status of each column by checking $(this).data('_dir_') for 'a', 'd' or ''.

It uses the following object to store the data from each row.

JavaScript
var RowData = function () {
    this.Seq = 0;
    this.Key = '';
    this.KeyType = 0;  // 0 = string, 1 = number, 2 = date (mm/dd/yy[yy]), 3 - date (dd/mm/yy[yy]), 4 - date (yy[yy]/mm/dd)
    this.Cells = new Array();
}

The sort is done using Key, rather than the corresponding element in the Cells array. This enables us to sort on the actual numeric value (numbers) or the upshifted string (case insensitive string sort).

JavaScript
//
// If first time, add original sequence number to each row for stable sorting
var rownum = 0;
this.find('tr').not(':first').each(function () {
    if ($(this).data('_seq_')) {
        return false;
    }
    $(this).data('_seq_', rownum);
    rownum++;
});

The data in the table is transferred to an array of RowData objects. The Seq property tracks the original sequence of the table. It does this by saving the original sequence of each row using the jQuery data method with a key of _seq_. The sort comparison method resolves identical keys by looking at the Seq property. This ensures stability with respect to the original sort order. The sort uses the Key and KeyType in its comparison method. Here is the code that does the actual sort. If the columnText is not supplied, the sort returns the table to its original order.

JavaScript
if (settings.columnText) {
    tableData.sort(function (a, b) {
        switch (a.KeyType) {
            case 0: // string
            if (a.Key > b.Key) {
                return sortAsc ? 1 : -1;
            }
            if (a.Key < b.Key) {
                return sortAsc ? -1 : 1;
            }
            break;
            case 1: // Numeric
            if (parseFloat(a.Key) > parseFloat(b.Key)) {
                return sortAsc ? 1 : -1;
            }
            if (parseFloat(a.Key) < parseFloat(b.Key)) {
                return sortAsc ? -1 : 1;
            }
            break;
            default: // Date
            var res = DateCompare(a.KeyType, a.Key, b.Key);
            if (res == 1) {
                return sortAsc ? 1 : -1;
            }
            if (res == -1) {
                return sortAsc ? -1 : 1;
            }
        }
        if (a.Seq > b.Seq) {
            return 1;
        }
        return -1;
    });
}
else {
    tableData.sort(function (a, b) {
        if (a.Seq > b.Seq) {
            return 1;
        }
        return -1;
    });
}

Note that the Key needs to match the KeyType. While a numeric field may contain "$12,123.00", the key is reduced to its actual numeric value of "12123" using a regular expression. It does this before the sort, instead of in the comparison routine, to reduce the regular expression performance hit.

Once the sort is done, the data is copied from the array of RowData objects back to the HTML table. It skips the first row and any footer row.

JavaScript
rowX = -1;
this.find('tr').not(':first').each(function () {
    if (!$(this).parent().is('tfoot')) {
        rowX++;
        var rowData = tableData[rowX];
        $(this).data('_seq_', rowData.Seq);
        colX = -1;
        $(this).children().each(function () {
            colX++;
            $(this).text(rowData.Cells[colX]);
        });
    }
});

Determining Date Formats

The basic idea is that years can have any value, days can only be in the range 1-31 and months can only be in the range 1-12. The Plugin scans any column containing three integers separated by the dateDelimiter. It computes how many valid days and months it finds and determines the date format from the results. In a sufficiently large sample, you will get one position where no field exceeds 12 and another where no field exceeds 31. That will tell you which fields represents the day, month and year. It may not always work, since dates in one format are also valid in another format. But the odds are good! If you know the date format, you can set it explicitly using the dateFormat option.

Dealing with numbers

The way numbers are written varies from country to country. In English language countries, the period is used as the decimal delimiter and the comma as the digit group separator. In European countries, the comma may be used as the decimal delimiter and a space or period as the digit group separator. To ensure correct sorting, you can set the decimalDelimiter as an option. The digit group separator is stripped out by a regular expression that only allows digits, a minus sign and the current decimal delimiter. (var numExp = new RegExp('[^0-9' + settings.decimalDelimiter + '-]+', 'g');).

This is the option you would use to set the decimal delimiter to a comma.

JavaScript
$('#DemoTable').sortByColumn({ columnText: $(this).text(), decimalDelimiter: ','});

Image 3

Figure 3. "Date Received" converted to 'EU', Miles sorted using a comma decimal delimiter.

Why no glyphs?

Some table sorting systems use glyphs in the column header to indicate the status of the sort. This may be problematic if there is no space for the glyph or adding one changes the width of the column. This Plugin leaves the method for indicating sort direction up to the developer by letting them create CSS classes that can be applied to header cells to indicate if they are currently sorted in ascending or descending order. In DemoTable.html, the developer chose to use a top red border to indicate ascending and a bottom red border to indicate descending. That seems to work quite well. Here are the classes and the code that passes them to the Plugin.

JavaScript
.ascending {
    border-top: solid 3px red;
}
…
.descending {
    border-bottom: solid 3px red;
}
…
$('#DemoTable th').click(function (e) {
    $('#DemoTable').sortByColumn({ columnText: $(this).text(),
        ascendingClass: 'ascending',
        descendingClass: 'descending'});
    });

Performance

Performance testing was done on a Dell XPS computer running Windows 10 (64-bit) with 16GB RAM and an Intel i7-3770 processor @ 3.40 GHz. It used BigDemoTable.html which has 3000 rows and 8 columns.

Browser Version Fastest Time (secs) Slowest Time(secs)
Google Chrome 50.0.2661.75 m 0.97 1.32
Mozilla Firefox 45.0.2 0.75 0.85
Microsoft Edge 25.10586.0.0 3.66 4.10
Microsoft IE 11.212.10586.0 3.89 4.19

Firefox edges out Chrome while the Microsoft siblings are significantly slower. Note that 3000 rows is the equivalent of 50 to 100 page downs, depending on monitor resolution. Most applications would not deliver anything like that volume of data to a browser.

Creating a JQuery Plugin

It seems intimidating but it isn't that difficult. The basics are covered at this jQuery site and in this Code Project article.

Download Contents

The dowload contains the two demonstration pages and a sub folder called js that contains the TableSort Plugin (tablesort.js) and a minified version (tablesort.min.js).

Points of Interest

  • It is relatively easy to create a jQuery Plugin that adds functionality without much coding effort.
  • Dealing with various international formats and dates makes a generalized sorting Plugin more complicated to code.
  • The internal Array.Sort() method is fast. Profiling shows most of the CPU time is spent extracting data from an html table and putting back the sorted data.

History

First version for Code-Project

License

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