19 min read

In this article by Karl Swedberg and Jonathan Chaffer, we will use an online bookstore as our model website, but the techniques we cook up can be applied to a wide variety of other sites as well, from weblogs to portfolios, from market-facing business sites to corporate intranets.

In this article, we will use jQuery to apply techniques for increasing the readability, usability, and visual appeal of tables, though we are not dealing with tables used for layout and design. In fact, as the web standards movement has become more pervasive in the last few years, table-based layout has increasingly been abandoned in favor of CSS‑based designs. Although tables were often employed as a somewhat necessary stopgap measure in the 1990s to create multi-column and other complex layouts, they were never intended to be used in that way, whereas CSS is a technology expressly created for presentation.

But this is not the place for an extended discussion on the proper role of tables. Suffice it to say that in this article we will explore ways to display and interact with tables used as semantically marked up containers of tabular data. For a closer look at applying semantic, accessible HTML to tables, a good place to start is Roger Johansson’s blog entry, Bring on the Tables at www.456bereastreet.com/archive/200410/bring_on_the_tables/.

Some of the techniques we apply to tables in this article can be found in plug‑ins such as Christian Bach’s Table Sorter. For more information, visit the jQuery Plug‑in Repository at http://jQuery.com/plugins.

Sorting

One of the most common tasks performed with tabular data is sorting. In a large table, being able to rearrange the information that we’re looking for is invaluable. Unfortunately, this helpful operation is one of the trickiest to put into action. We can achieve the goal of sorting in two ways, namely Server-Side Sorting and JavaScript Sorting.

Server-Side Sorting

A common solution for data sorting is to perform it on the server side. Data in tables often comes from a database, which means that the code that pulls it out of the database can request it in a given sort order (using, for example, the SQL language’s ORDER BY clause). If we have server-side code at our disposal, it is straightforward to begin with a reasonable default sort order.

Sorting is most useful when the user can determine the sort order. A common idiom is to make the headers of sortable columns into links. These links can go to the current page, but with a query string appended indicating the column to sort by:

 

   

   

 

  …

Name Date

The server can react to the query string parameter by returning the database contents in a different order.

Preventing Page Refreshes

This setup is simple, but requires a page refresh for each sort operation. As we have seen, jQuery allows us to eliminate such page refreshes by using AJAX methods. If we have the column headers set up as links as before, we can add jQuery code to change those links into AJAX requests:

$(document).ready(function() {

  $(‘#my-data .name a’).click(function() {

    $(‘#my-data’).load(‘index. php?sort=name&type=ajax’);

    return false;

  });

  $(‘#my-data .date a’).click(function() {

    $(‘#my-data’).load(‘index. php?sort=date&type=ajax’);

    return false;

  });

});

Now when the anchors are clicked, jQuery sends an AJAX request to the server for the same page. We add an additional parameter to the query string so that the server can determine that an AJAX request is being made. The server code can be written to send back only the table itself, and not the surrounding page, when this parameter is present. This way we can take the response and insert it in place of the table.

This is an example of progressiveenhancement. The page works perfectly well without any JavaScript at all, as the links for server-side sorting are still present. When JavaScript is present, however, the AJAX hijacks the page request and allows the sort to occur without a full page load.

JavaScript Sorting

There are times, though, when we either don’t want to wait for server responses when sorting, or don’t have a server-side scripting language available to us. A viable alternative in this case is to perform the sorting entirely on the browser using JavaScript client-side scripting.

For example, suppose we have a table listing books, along with their authors, release dates, and prices:

 

   

     

     

     

     

     

   

 

 

   

     

     

     

     

     

   

   

     

     

     

     

     

   

    …

 

Title Author(s) Publish Date Price

       

             height=”61″ alt=”Building Websites with

                                                Joomla! 1.5 Beta 1″ />

     

Building Websites with Joomla! 1.5 Beta 1 Hagen Graf Feb 2007 $40.49

               height=”61″ alt=”Learning Mambo: A Step-by-Step

               Tutorial to Building Your Website” />

Learning Mambo: A Step-by-Step Tutorial to Building Your

          Website

Douglas Paterson Dec 2006 $40.49

We’d like to turn the table headers into buttons that sort by their respective columns. Let us look into ways of doing this.
 

Row Grouping Tags

Note our use of the and tags to segment the data into row groupings. Many HTML authors omit these implied tags, but they can prove useful in supplying us with more convenient CSS selectors to use. For example, suppose we wish to apply typical even/odd row striping to this table, but only to the body of the table:

$(document).ready(function() {

  $(‘table.sortable tbody tr:odd’).addClass(‘odd’);

  $(‘table.sortable tbody tr:even’).addClass(‘even’);

});

This will add alternating colors to the table, but leave the header untouched:

jQuery Table Manipulation: Part 1

Basic Alphabetical Sorting

Now let’s perform a sort on the Titlecolumn of the table. We’ll need a class on the table header cell so that we can select it properly:

 

   

   

Title

   

Author(s)

   

Publish Date

   

Price

 

To perform the actual sort, we can use JavaScript’s built in .sort()method. It does an in‑place sort on an array, and can take a function as an argument. This function compares two items in the array and should return a positive or negative number depending on the result. Our initial sort routine looks like this:

$(document).ready(function() {

  $(‘table.sortable’).each(function() {

    var $table = $(this);

    $(‘th’, $table).each(function(column) {

      if ($(this).is(‘.sort-alpha’)) {

        $(this).addClass(‘clickable’).hover(function() {

          $(this).addClass(‘hover’);

        }, function() {

          $(this).removeClass(‘hover’);

        }).click(function() {

          var rows = $table.find(‘tbody > tr’).get();

          rows.sort(function(a, b) {

            var keyA = $(a).children(‘td’).eq(column).text()
                                                      .toUpperCase();

            var keyB = $(b).children(‘td’).eq(column).text()
                                                      .toUpperCase();

            if (keyA

            if (keyA > keyB) return 1;

            return 0;

          });

          $.each(rows, function(index, row) {

            $table.children(‘tbody’).append(row);

          });

        });

      }

    });

  });

});

The first thing to note is our use of the .each() method to make iteration explicit. Even though we could bind a click handler to all headers with the sort-alpha class just by calling $(‘table.sortable th.sort-alpha’).click(), this wouldn’t allow us to easily capture a crucial bit of information—the column index of the clicked header. Because .each() passes the iteration index into its callback function, we can use it to find the relevant cell in each row of the data later.

Once we have found the header cell, we retrieve an array of all of the data rows. This is a great example of how .get()is useful in transforming a jQuery object into an array of DOM nodes; even though jQuery objects act like arrays in many respects, they don’t have any of the native array methods available, such as .sort().

With .sort() at our disposal, the rest is fairly straightforward. The rows are sorted by comparing the textual contexts of the relevant table cell. We know which cell to look at because we captured the column index in the enclosing .each() call. We convert the text to uppercase because string comparisons in JavaScript are case-sensitive and we wish our sort to be case-insensitive. Finally, with the array sorted, we loop through the rows and reinsert them into the table. Since .append() does not clone nodes, this moves them rather than copying them. Our table is now sorted.

This is an example of progressive enhancement’s counterpart, gracefuldegradation. Unlike with the AJAX solution discussed earlier, we cannot make the sort work without JavaScript, as we are assuming the server has no scripting language available to it in this case. The JavaScript is required for the sort to work, so by adding the “clickable” class only through code, we make sure not to indicate with the interface that sorting is even possible unless the script can run. The page degrades into one that is still functional, albeit without sorting available.

We have moved the actual rows around, hence our alternating row colors are now out of whack:

jQuery Table Manipulation: Part 1

We need to reapply the row colors after the sort is performed. We can do this by pulling the coloring code out into a function that we call when needed:

$(document).ready(function() {

  var alternateRowColors = function($table) {

    $(‘tbody tr:odd’, $table).removeClass(‘even’).addClass(‘odd’);

    $(‘tbody tr:even’, $table).removeClass(‘odd’).addClass(‘even’);

  };

 

  $(‘table.sortable’).each(function() {

    var $table = $(this);

    alternateRowColors($table);

    $(‘th’, $table).each(function(column) {

      if ($(this).is(‘.sort-alpha’)) {

        $(this).addClass(‘clickable’).hover(function() {

          $(this).addClass(‘hover’);

        }, function() {

          $(this).removeClass(‘hover’);

        }).click(function() {

          var rows = $table.find(‘tbody > tr’).get();

          rows.sort(function(a, b) {

            var keyA = $(a).children(‘td’).eq(column).text()
                                                      .toUpperCase();

            var keyB = $(b).children(‘td’).eq(column).text()
                                                      .toUpperCase();

            if (keyA

            if (keyA > keyB) return 1;

            return 0;

          });

          $.each(rows, function(index, row) {

            $table.children(‘tbody’).append(row);

          });

          alternateRowColors($table);

        });

      }

    });

  });

});

This corrects the row coloring after the fact, fixing our issue:

jQuery Table Manipulation: Part 1

 

The Power of Plug-ins

The alternateRowColors()function that we wrote is a perfect candidate to become a jQuery plug-in. In fact, any operation that we wish to apply to a set of DOM elements can easily be expressed as a plug-in. In this case, we only need to modify our existing function a little bit:

jQuery.fn.alternateRowColors = function() {

  $(‘tbody tr:odd’, this).removeClass(‘even’).addClass(‘odd’);

  $(‘tbody tr:even’, this).removeClass(‘odd’).addClass(‘even’);

  return this;

};

We have made three important changes to the function.

  1. It is defined as a new property of jQuery.fn rather than as a standalone function. This registers the function as a plug-in method.

  2. We use the keyword this as a replacement for our $table parameter. Within a plug-in method, thisrefers to the jQuery object that is being acted upon.

  3. Finally, we return this at the end of the function. The return value makes our new method chainable.

More information on writing jQuery plug-ins can be found in Chapter 10 of our book Learning jQuery. There we will discuss making a plug-in ready for public consumption, as opposed to the small example here that is only to be used by our own code.

With our new plug-in defined, we can call $table.alternateRowColors(), which is a more natural jQuery syntax, intead of alternateRowColors($table).

Performance Concerns

Our code works, but is quite slow. The culprit is the comparator function, which is performing a fair amount of work. This comparator will be called many times during the course of a sort, which means that every extra moment it spends on processing will be magnified.

The actual sort algorithm used by JavaScript is not defined by the standard. It may be a simple sort like a bubble sort (worst case of Θ(n2) in computational complexity terms) or a more sophisticated approach like quick sort (which is Θ(n log n) on average). In either case doubling the number of items increases the number of times the comparator function is called by more than double.

The remedy for our slow comparator is to pre-compute the keys for the comparison. We begin with the slow sort function:

rows.sort(function(a, b) {

  keyA = $(a).children(‘td’).eq(column).text().toUpperCase();

  keyB = $(b).children(‘td’).eq(column).text().toUpperCase();

  if (keyA

  if (keyA > keyB) return 1;

  return 0;

});

$.each(rows, function(index, row) {

  $table.children(‘tbody’).append(row);

});

We can pull out the key computation and do that in a separate loop:

$.each(rows, function(index, row) {

  row.sortKey = $(row).children(‘td’).eq(column).text().toUpperCase();

});

rows.sort(function(a, b) {

  if (a.sortKey

  if (a.sortKey > b.sortKey) return 1;

  return 0;

});

$.each(rows, function(index, row) {

  $table.children(‘tbody’).append(row);

  row.sortKey = null;

});

In the new loop, we are doing all of the expensive work and storing the result in a new property. This kind of property, attached to a DOM element but not a normal DOM attribute, is called an expando.This is a convenient place to store the key since we need one per table row element. Now we can examine this attribute within the comparator function, and our sort is markedly faster. 

We set the expando property to null after we’re done with it to clean up after ourselves. This is not necessary in this case, but is a good habit to establish because expando properties left lying around can be the cause of memory leaks. For more information, see Appendix C.

 

Finessing the Sort Keys

Now we want to apply the same kind of sorting behavior to the Author(s) column of our table. By adding the sort-alpha class to its table header cell, the Author(s)column can be sorted with our existing code. But ideally authors should be sorted by last name, not first. Since some books have multiple authors, and some authors have middle names or initials listed, we need outside guidance to determine what part of the text to use as our sort key. We can supply this guidance by wrapping the relevant part of the cell in a tag:

 

   

            alt=”Building Websites with Joomla! 1.5 Beta 1″ />

 

Building Websites with Joomla! 1.5 Beta 1

 

Hagen Graf

 

Feb 2007

 

$40.49

 

   

         alt=”Learning Mambo: A Step-by-Step Tutorial to Building

                                                Your Website” />

 

    Learning Mambo: A Step-by-Step Tutorial to Building Your Website

 

 

Douglas Paterson

 

Dec 2006

 

$40.49

 

   

                  alt=”Moodle E-Learning Course Development” />

 

Moodle E-Learning Course Development

 

William Rice

 

May 2006

 

$35.99

Now we have to modify our sorting code to take this tag into account, without disturbing the existing behavior for the Titlecolumn, which is working well. By prepending the marked sort key to the key we have previously calculated, we can sort first on the last name if it is called out, but on the whole string as a fallback:

$.each(rows, function(index, row) {

  var $cell = $(row).children(‘td’).eq(column);

  row.sortKey = $cell.find(‘.sort-key’).text().toUpperCase()

                                  + ‘ ‘ + $cell.text().toUpperCase();

});

Sorting by the Author(s)column now uses the last name:

 

jQuery Table Manipulation: Part 1

 

If two last names are identical, the sort uses the entire string as a tiebreaker for positioning.


Subscribe to the weekly Packt Hub newsletter. We'll send you this year's Skill Up Developer Skills Report.

* indicates required

LEAVE A REPLY

Please enter your comment!
Please enter your name here