ignore

sorttable: Make all your tables sortable

While the web design community gradually moves away from using tables to lay out the structure of a page, tables really do have a vital use, their original use; they're for laying out tabular data. For instance, imagine a table of employees.

NameSalaryExtensionStart date
Bloggs, Fred$12000.00 135318/08/2003
Turvey, Kevin$191200.00 234202/05/1979
Mbogo, Arnold$32010.12 275509/08/1998
Shakespeare, Bill$122000.00 321112/11/1961
Shakespeare, Hamnet$9000 900501/01/2002
Fitz, Marvin$3300 555422/05/1995

Pretty simple. But if you saw that table in a client-side application, you'd expect to be able to click on the headers and have the table sort, would you not? I know it always annoys me when you can't. A fair few web applications do allow this; most of them, which are pulling this data by submitting a SQL query to a relational database (an environment eminently suited to tabular data) implement this by resubmitting the whole page with something like ordercolumn=4 in the URL, and then adding an ORDER BY clause to their SQL query to return the data from the DB ordered by the specified column.

Resubmit the page? Just to sort data we already have? I'm sure we can do better than that.

NameSalaryExtensionStart date
Bloggs, Fred$12000.00 135318/08/2003
Turvey, Kevin$191200.00 234202/05/1979
Mbogo, Arnold$32010.12 275509/08/1998
Shakespeare, Bill$122000.00 321112/11/1961
Shakespeare, Hamnet$9000 900501/01/2002
Fitz, Marvin$3300 555422/05/1995

As you can see, the above table now has clickable headers that sort the table by the clicked column. Note how the numeric and date columns all sort properly, too, rather than sorting alphanumerically.

This is not a new trick, sorting a table using the DOM. However, this mini-library has two nice attributes; the first is, unsurprisingly, that it follows my principles of unobtrusive DHTML, as you'll see below. The second is that, as mentioned above, it knows how to sort a variety of different data types, and it works them out for itself -- you don't have to tell it.

First, how to use it, then we'll get on to How It Works for those of you who don't want to just take the code and use it (like all the other JS libraries here, it's under the MIT licence). To make a table of your choice sortable, there are three steps:

  1. Include the Javascript library, by putting a link to it in the HEAD of your page, like so:
    <script src="/img/spacer.gif"> 
  2. Mark your table as a sortable one by giving it a class of "sortable":
    <table class="sortable">
  3. Ensure that your table has an ID:
    <table id="unique_id">

And that's all you need. Your table will now have column sorting available by clicking the headers. For niceness, you might want to add the following styles to your stylesheet, or make up some of your own based on this:


/* Sortable tables */
table.sortable a.sortheader {
    background-color:#eee;
    color:#666666;
    font-weight: bold;
    text-decoration: none;
    display: block;
}
table.sortable span.sortarrow {
    color: black;
    text-decoration: none;
}

If you just want to use the code, that's it; take the JS library, link to it as above, and mark your tables as sortable. Remember to give them IDs, too. For those of you with enquiring minds who want to know how it works, read "How It Works" below.

Sorting a table the first time the page is loaded

Lots of people ask, "how do I make sorttable sort the table the first time the page is loaded?" The answer is: you don't. Sorttable is about changing the HTML that is served from your server without a page refresh. When the page is first served from the server, you have to incur the wait for it to be served anyway. So, if you want the table sorted when a page is first displayed, serve the table in sorted order. Tables often come out of a database; get the data from the database in a sorted order with an ORDER BY clause in your SQL.

The other thing that people say to that is: no, no, no, you've got it wrong, I want people who have visited the page before and sorted the data to get the data sorted that way again on their next visit, so I need sorttable to run when the page loads. No, you don't. Any solution which involves you running sorttable as soon as the page loads (i.e., without user input) is a wrong solution. To do what you want, what you should do is:

  1. Hack sorttable.js so that it sets a cookie when a table is sorted, storing which table was sorted and which column it was sorted by, and whether that sort was ascending or descending.
  2. Change your server code that generates the HTML in the first place to read the cookie, if it exists, and serve the data sorted in the way that the cookie dictates.

This procedure is left as an exercise for the reader. It would be a useful addition, but I can't add it to the library because I don't know how your server code works, and I won't add something that fires sorttable to do sorting as soon as the page loads. That's the wrong solution; if you're determined to do it then I can't stop you, but you're on your own.

How It Works

As said above (briefly) using the DOM to sort a table on the fly (rather than doing a new server request) is not a new idea. A table object in the DOM has a rows attribute which is basically a two dimensional array defining the table; each item in the rows list has a cells list, so rows is essentially a TR and each cell in a row is essentially a TD within that row. So, sorting a table means sorting the rows list by the value in a particular column, i.e., the value in row.cells[n] where n is the column number.

JavaScript arrays have a sort() method. Calling myarray.sort() sorts the array in place. You can also pass a sort function to this, so myarray.sort(sortfn) will use sortfn() to sort the array. We can use this to sort 2d arrays (where each array element is itself an array); sortfn() is passed two things, and much return whether the first is larger, smaller, or the same as the second. So, if we're sorting an array of arrays, sortfn() will be repeatedly called with two parameters, both of which are arrays themselves. Clear as mud? Thought so. A few examples*:

Sorting an array with a sort function


myarray = [4,2,6,5,3,1];
function mysortfn(a,b) {
  WScript.Echo("Comparing "+a+" and "+b);
  if (a<b) return -1;
  if (a>b) return 1;
  return 0;
}
	
WScript.Echo(myarray);
myarray.sort(mysortfn);
WScript.Echo(myarray);

Output

4,2,6,5,3,1
Comparing 1 and 6
Comparing 5 and 3
Comparing 5 and 2
Comparing 5 and 6
Comparing 6 and 4
Comparing 1 and 4
Comparing 5 and 4
Comparing 5 and 1
Comparing 2 and 3
Comparing 3 and 1
Comparing 3 and 4
Comparing 4 and 1
Comparing 3 and 1
Comparing 3 and 2
Comparing 2 and 1
1,2,3,4,5,6

As you can see, the mysortfn() function is called a lot to work out how to sort. Now, let's try:

Sorting a multi-dimensional array


// This time, use a multi-dimensional array, and sort by column 2
myarray = [
    ['Fred','Bloggs','$12000.00',1353],
    ['Kevin','Turvey','$191200.00',2342],
    ['Arnold','Mbogo','$32010.12',2755],
    ['Bill','Shakespeare','$122000.00',3211],
    ['Hamnet','Shakespeare','$9000',9005],
    ['Marvin','Fitz','$3300',5554]
  ];

function mysortfn(a,b) {
  WScript.Echo("Comparing "+a+" and "+b);
  // Note that each thing we are passed is an array, so we don't compare the things
  // we're passed; instead, we compare their second column
  if (a[1]<b[1]) return -1;
  if (a[1]>b[1]) return 1;
  return 0;
}
	
WScript.Echo(myarray);
myarray.sort(mysortfn);
WScript.Echo(myarray);

Output

Fred,Bloggs,$12000.00,1353,Kevin,Turvey,$191200.00,2342,Arnold,Mbogo,$32010.12,2755,Bill,Shakespeare,$122000.00,3211,Hamnet,Shakespeare,$9000,9005,Marvin,Fitz,$3300,5554
Comparing Marvin,Fitz,$3300,5554 and Arnold,Mbogo,$32010.12,2755
Comparing Bill,Shakespeare,$122000.00,3211 and Hamnet,Shakespeare,$9000,9005
Comparing Hamnet,Shakespeare,$9000,9005 and Kevin,Turvey,$191200.00,2342
Comparing Kevin,Turvey,$191200.00,2342 and Arnold,Mbogo,$32010.12,2755
Comparing Kevin,Turvey,$191200.00,2342 and Fred,Bloggs,$12000.00,1353
Comparing Bill,Shakespeare,$122000.00,3211 and Hamnet,Shakespeare,$9000,9005
Comparing Hamnet,Shakespeare,$9000,9005 and Fred,Bloggs,$12000.00,1353
Comparing Hamnet,Shakespeare,$9000,9005 and Arnold,Mbogo,$32010.12,2755
Comparing Hamnet,Shakespeare,$9000,9005 and Marvin,Fitz,$3300,5554
Comparing Bill,Shakespeare,$122000.00,3211 and Fred,Bloggs,$12000.00,1353
Comparing Bill,Shakespeare,$122000.00,3211 and Marvin,Fitz,$3300,5554
Comparing Bill,Shakespeare,$122000.00,3211 and Arnold,Mbogo,$32010.12,2755
Comparing Bill,Shakespeare,$122000.00,3211 and Fred,Bloggs,$12000.00,1353
Comparing Marvin,Fitz,$3300,5554 and Fred,Bloggs,$12000.00,1353
Comparing Marvin,Fitz,$3300,5554 and Arnold,Mbogo,$32010.12,2755
Comparing Arnold,Mbogo,$32010.12,2755 and Fred,Bloggs,$12000.00,1353
Comparing Marvin,Fitz,$3300,5554 and Fred,Bloggs,$12000.00,1353
Fred,Bloggs,$12000.00,1353,Marvin,Fitz,$3300,5554,Arnold,Mbogo,$32010.1,2755,Bill,Shakespeare,$122000.00,3211,Hamnet,Shakespeare,$9000,9005,Kevin,Turvy,$191200.00,2342

This time, we told our sort function to explicitly use column 2 to compare each of the arrays we were passed, and it worked.

That being the case, it's relatively easy to see how the sortable table is implemented. Each of the clickable headers needs to call a function to sort the table's rows array by that column, so the first clickable header calls the function and tells it to sort this table using column 1.

There are, naturally, a few minor wrinkles. You might think that you could just sort table.rows in place with table.rows.sort(sortfn). Nuh-uh. Can't do that. So, we invent a newRows array and copy table.rows into it, and then sort that, and then re-add those sorted rows to the page. One of the neat things about the DOM is that if you insert into the document an element that is already in the document, then it moves it from where it is to where you inserted it. That's nice for this, because we don't have to delete the table and then reconstruct it from newRows; instead, we just walk through the sorted newRows and insert each row in it into the table, and because we're inserting them in sorted order, the table gets swapped around to be in sorted order. Very handy.

There's also a little bit of code to do with showing an arrow in the header that you've clicked on to show which way it's sorted, and to allow us to toggle the sort to ascending or descending. This uses innerHTML, slightly naughtily, but it seems to work OK in my testing.

The next important part is diagnosing field types; this is a canonical problem for sorting, because numeric fields must be sorted in numeric order, not alphanumeric order (20,153,11 should sort as 11,20,153, not 11,153,20). The code does this by looking at the data in the first row and then choosing a sort function based on what the data looks like; it matches the data against a series of regular expressions, one that looks for dates, one for numbers, one for currency fields, and so on. Each regex is paired with a special sort function written particularly for that sort of data (so the numeric sort function sorts numbers numerically, for example). If no regexes match, then the code defaults to a generic function which sorts alphanumerically and case-insensitively. (Note that the date function is keyed towards UK format dates (dd/mm/[cc]yy) and will need poking for dates in other formats.)

The code also treats rows with class sortbottom specially; they always appear at the bottom of the table and don't take their place in the sort order. It does this by sorting them into the sort order along with everything else, but then re-inserting them into the table last. This is useful if you have a row of totals, for example, that should remain at the bottom of the table regardless of sorting.

Finally, we return to something we glossed over earlier; how the clickable sort headers are created. This is the unobtrusive DHTML part; the code adds an onload event to the page which calls the sortables_init() function. This walks through all tables on the page, and for any that have class sortable, walks through each of the cells in the first row of that table and makes the contents of those cells into clickable links.

Thanks

I pinched the getInnerText() function from Erik over at WebFX. Their sortable table implementation does the same sort of thing as mine, but it requires you to add some JavaScript to instantiate the table, and you also have to explicitly specify the types of fields, which is a bit obtrusive.

This was partially developed for a project at work, so thanks to my boss. Thanks also to C. David Eagle for pointing out a bug where I didn't close the script tag in the example.

Updated 3rd April 2006 to work with Safari, as per a Google Answer. Thanks to Brad Kemper and wildeeo-ga!

Stuart Langridge, November 2003

kryogenix.org | other browser experiments

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.