jQuery Table Manipulation: Part 1

Exclusive offer: get 50% off this eBook here
jQuery 1.4 Reference Guide

jQuery 1.4 Reference Guide — Save 50%

This book and eBook is a comprehensive exploration of the popular JavaScript library

$23.99    $12.00
by Jonathan Chaffer Karl Swedberg | August 2007 | AJAX Open Source Web Development

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:

<table id="my-data">

  <tr>

    <th class="name"><a href="index.php?sort=name">Name</a></th>

    <th class="date"><a href="index.php?sort=date">Date</a></th>

  </tr>

  ...

</table>

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 progressive enhancement. 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:

<table class="sortable">

  <thead>

    <tr>

      <th></th>

      <th>Title</th>

      <th>Author(s)</th>

      <th>Publish&nbsp;Date</th>

      <th>Price</th>

    </tr>

  </thead>

  <tbody>

    <tr>

      <td>

        <img src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="../covers/small/1847192386.png" width="49"

             height="61" alt="Building Websites with

                                                Joomla! 1.5 Beta 1" />

      </td>

      <td>Building Websites with Joomla! 1.5 Beta 1</td>

      <td>Hagen Graf</td>

      <td>Feb 2007</td>

      <td>$40.49</td>

    </tr>

    <tr>

      <td><img src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="../covers/small/1904811620.png" width="49"

               height="61" alt="Learning Mambo: A Step-by-Step

               Tutorial to Building Your Website" /></td>

      <td>Learning Mambo: A Step-by-Step Tutorial to Building Your

          Website</td>

      <td>Douglas Paterson</td>

      <td>Dec 2006</td>

      <td>$40.49</td>

    </tr>

    ...

  </tbody>

</table>

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 <thead> and <tbody> 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 Title column of the table. We'll need a class on the table header cell so that we can select it properly:

<thead>

  <tr>

    <th></th>

    <th class="sort-alpha">Title</th>

    <th>Author(s)</th>

    <th>Publish&nbsp;Date</th>

    <th>Price</th>

  </tr>

</thead>

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 < keyB) return -1;

            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, graceful degradation. 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 < keyB) return -1;

            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, this refers 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 < keyB) return -1;

  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 < b.sortKey) return -1;

  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:

<tr>

  <td>

    <img src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="../covers/small/1847192386.png" width="49" height="61"

            alt="Building Websites with Joomla! 1.5 Beta 1" /></td>

  <td>Building Websites with Joomla! 1.5 Beta 1</td>

  <td>Hagen <span class="sort-key">Graf</span></td>

  <td>Feb 2007</td>

  <td>$40.49</td>

</tr>

<tr>

  <td>

    <img src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="../covers/small/1904811620.png" width="49" height="61"

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

                                                Your Website" /></td>

  <td>

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

  </td>

  <td>Douglas <span class="sort-key">Paterson</span></td>

  <td>Dec 2006</td>

  <td>$40.49</td>

</tr>

<tr>

  <td>

    <img src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="../covers/small/1904811299.png" width="49" height="61"

                  alt="Moodle E-Learning Course Development" /></td>

  <td>Moodle E-Learning Course Development</td>

  <td>William <span class="sort-key">Rice</span></td>

  <td>May 2006</td>

  <td>$35.99</td>

</tr>

Now we have to modify our sorting code to take this tag into account, without disturbing the existing behavior for the Title column, 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.

jQuery 1.4 Reference Guide This book and eBook is a comprehensive exploration of the popular JavaScript library
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Sorting Other Types of Data

Our sort routine should be able to handle not just the Title and Author columns, but the Publish Dates and Price as well. Since we streamlined our comparator function, it can handle all kinds of data, but the computed keys will need to be adjusted for other data types. For example, in the case of prices we need to strip off the leading $ character and parse the rest, then compare them:

var key = parseFloat($cell.text().replace(/^[^d.]*/, ''));

row.sortKey = isNaN(key) ? 0 : key;

The result of parseFloat() needs to be checked, because if no number can be extracted from the text, NaN is returned, which can wreak havoc on .sort(). For the date cells, we can use the JavaScript Date object:

row.sortKey = Date.parse('1 ' + $cell.text());

The dates in this table contain a month and year only; Date.parse() requires a fully‑specified date, so we prepend the string with 1. This provides a day to complement the month and year, and the combination is then converted into a timestamp, which can be sorted using our normal comparator.

We can apportion these expressions across separate functions, and call the appropriate one based on the class applied to the table header:

$.fn.alternateRowColors = function() {

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

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

  return this;

};

 

$(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);

    $table.alternateRowColors($table);

    $('th', $table).each(function(column) {

      var findSortKey;

      if ($(this).is('.sort-alpha')) {

        findSortKey = function($cell) {

          return $cell.find('.sort-key').text().toUpperCase()

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

        };

      }

      else if ($(this).is('.sort-numeric')) {

        findSortKey = function($cell) {

          var key = parseFloat($cell.text().replace(/^[^d.]*/, ''));

          return isNaN(key) ? 0 : key;

        };

      }

      else if ($(this).is('.sort-date')) {

        findSortKey = function($cell) {

          return Date.parse('1 ' + $cell.text());

        };

      }

      if (findSortKey) {

$.fn.alternateRowColors = function() {

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

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

  return this;

};

 

$(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);

    $table.alternateRowColors($table);

    $('th', $table).each(function(column) {

      var findSortKey;

      if ($(this).is('.sort-alpha')) {

        findSortKey = function($cell) {

          return $cell.find('.sort-key').text().toUpperCase() + ' ' +
                                           $cell.text().toUpperCase();

        };

      }

      else if ($(this).is('.sort-numeric')) {

        findSortKey = function($cell) {

          var key = parseFloat($cell.text().replace(/^[^d.]*/, ''));

          return isNaN(key) ? 0 : key;

        };

      }

      else if ($(this).is('.sort-date')) {

        findSortKey = function($cell) {

          return Date.parse('1 ' + $cell.text());

        };

      }

      if (findSortKey) {

        $(this).addClass('clickable').hover(function() {

          $(this).addClass('hover');

        }, function() {

          $(this).removeClass('hover');

        }).click(function() {

          var newDirection = 1;

          if ($(this).is('.sorted-asc')) {

            newDirection = -1;

          }

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

         

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

            row.sortKey =
                        findSortKey($(row).children('td').eq(column));

          });

          rows.sort(function(a, b) {

            if (a.sortKey < b.sortKey) return -newDirection;

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

            return 0;

          });

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

            $table.children('tbody').append(row);

            row.sortKey = null;

          });

         $table.find('th').removeClass('sorted-asc')

                                         .removeClass('sorted-desc');

          var $sortHead = $table.find('th').filter('

                                  :nth-child(' + (column + 1) + ')');

          if (newDirection == 1) {

            $sortHead.addClass('sorted-asc');

          } else {

            $sortHead.addClass('sorted-desc');

          }

          $table.find('td').removeClass('sorted')

            .filter(':nth-child(' + (column + 1) + ')')

                                               .addClass('sorted');

         $table.alternateRowColors($table);

        });

      }

    });

  });

});

As a side benefit, since we use classes to store the sort direction we can style the columns headers to indicate the current order as well:

 

jQuery Table Manipulation: Part 1

 

Pagination

Sorting is a great way to wade through a large amount of data to find information. We can also help the user focus on a portion of a large data set by paginating the data. Pagination can be done in two ways—Server-Side Pagination and JavaScript Pagination.

Server-Side Pagination

Much like sorting, pagination is often performed on the server. If the data to be displayed is stored in a database, it is easy to pull out one chunk of information at a time using MySQL's LIMIT clause, ROWNUM in Oracle, or equivalent methods in other database engines.

As with our initial sorting example, pagination can be triggered by sending information to the server in a query string, such as index.php?page=52. And again as before, we can perform this task either with a full page load or by using AJAX to pull in just one chunk of the table. This strategy is browser-independent, and can handle large data sets very well.

Sorting and Paging Go Together

Data that is long enough to benefit from sorting is likely long enough to be a candidate for paging. It is not unusual to wish to combine these two techniques for data presentation. Since they both affect the set of data that is present on a page, though, it is important to consider their interactions while implementing them.

Both sorting and pagination can be accomplished either on the server or in the web browser. However, we must keep the strategies for the two tasks in sync; otherwise, we can end up with confusing behavior. Suppose, for example, that both sorting and paging is done on the server:

jQuery Table Manipulation: Part 1

 

When the table is re-sorted by number, a different set of rows is present on Page 1 of the table. If paging is done by the server and sorting by the browser, the entire data set is not available for the sorting routine, making the results incorrect:

jQuery Table Manipulation: Part 1

 

 

Only the data already present on the page can be displayed. To prevent this from being a problem, we must either perform both tasks on the server, or both in the browser.

JavaScript Pagination

So, let's examine how we would add JavaScript pagination to the table we have already made sortable in the browser. First, we'll focus on displaying a particular page of data, disregarding user interaction for now:

$(document).ready(function() {

  $('table.paginated').each(function() {

    var currentPage = 0;

    var numPerPage = 10;

    var $table = $(this);

    $table.find('tbody tr').show()

      .lt(currentPage * numPerPage)

        .hide()

      .end()

      .gt((currentPage + 1) * numPerPage - 1)

        .hide()

      .end();

  });

});

This code displays the first page—ten rows of data.

Once again we rely on the presence of a <tbody> element to separate data from headers; we don't want to have the headers or footers disappear when moving on to the second page. For selecting the rows containing data, we show all the rows first, then select the rows before and after the current page, hiding them. The method chaining supported by jQuery makes another appearance here when we filter the set of matched rows twice, using .end() in between to pop the current filter off the stack and start afresh with a new filter.

The most error-prone task in writing this code is formulating the expressions to use in the filters. To use the .lt() and .gt() methods, we need to find the indices of the rows at the beginning and end of the current page. For the beginning row, we just multiply the current page number by the number of rows on each page. Multiplying the number of rows by one more than the current page number gives us the beginning row of the next page; to find the last row of the current page, we must subtract one from this.~

Displaying the Pager

To add user interaction to the mix, we need to place the pager itself next to the table. We could do this by simply inserting links for the pages in the HTML markup, but this would violate the progressive enhancement principle we've been espousing. Instead, we should add the links using JavaScript, so that users without scripting available are not misled by links that cannot work.

To display the links, we need to calculate the number of pages and create a corresponding number of DOM elements:

var numRows = $table.find('tbody tr').length;

var numPages = Math.ceil(numRows / numPerPage);

 

var $pager = $('<div class="pager"></div>');

for (var page = 0; page < numPages; page++) {

  $('<span class="page-number">' + (page + 1) + '</span>')

    .appendTo($pager).addClass('clickable');

}

$pager.insertBefore($table);

The number of pages can be found by dividing the number of data rows by the number of items we wish to display on each page. If the division does not yield an integer, we must round the result up using Math.ceil() to ensure that the final partial page will be accessible. Then, with this number in hand, we create buttons for each page and position the new pager before the table:

jQuery Table Manipulation: Part 1

Enabling the Pager Buttons

To make these new buttons actually work, we need to update the currentPage variable and then run our pagination routine. At first blush, it seems we should be able to do this by setting currentPage to page, which is the current value of the iterator that creates the buttons:

$(document).ready(function() {

  $('table.paginated').each(function() {

    var currentPage = 0;

    var numPerPage = 10;

    var $table = $(this);

    var repaginate = function() {

      $table.find('tbody tr').show()

        .lt(currentPage * numPerPage)

          .hide()

        .end()

        .gt((currentPage + 1) * numPerPage - 1)

          .hide()

        .end();

    };

    var numRows = $table.find('tbody tr').length;

    var numPages = Math.ceil(numRows / numPerPage);

    var $pager = $('<div class="pager"></div>');

    for (var page = 0; page < numPages; page++) {

      $('<span class="page-number">' + (page + 1) + '</span>')

        .click(function() {

          currentPage = page;

          repaginate();

        })

        .appendTo($pager).addClass('clickable');

    }

    $pager.insertBefore($table);

    repaginate();

  });

});

This mostly works. The new repaginate() function is called when the page loads and when any button is clicked. All of the buttons take us to a page with no rows on it, though:

jQuery Table Manipulation: Part 1

 

The problem is that in defining our click handler, we have created a closure. The click handler refers to the page variable, which is defined outside the function. When the variable changes the next time through the loop, this affects the click handlers that we have already set up for the earlier buttons. The net effect is that, for a pager with 7 pages, each button directs us to page 8 (the final value of page). More information on how closures work can be found in Appendix C, JavaScript Closures.

To correct this problem, we'll take advantage of one of the more advanced features of jQuery's event binding methods. We can add a set of data to the handler when we bind it that will still be available when the handler is eventually called. With this capability in our bag of tricks, we can write:

$('<span class="page-number">' + (page + 1) + '</span>')

  .bind('click', {'newPage': page}, function(event) {

    currentPage = event.data['newPage'];

    repaginate();

  })

  .appendTo($pager).addClass('clickable');

The new page number is passed into the handler by way of the event's data property. In this way the page number escapes the closure, and is frozen in time at the value it contained when the handler was bound. Now our pager buttons can correctly take us to different pages:

 

jQuery Table Manipulation: Part 1

Marking the Current Page

Our pager can be made more user-friendly by highlighting the current page number. We just need to update the classes on the buttons every time one is clicked:

var $pager = $('<div class="pager"></div>');

for (var page = 0; page < numPages; page++) {

  $('<span class="page-number">' + (page + 1) + '</span>')

    .bind('click', {'newPage': page}, function(event) {

      currentPage = event.data['newPage'];

      repaginate();

      $(this).addClass('active').siblings().removeClass('active');

    })

    .appendTo($pager).addClass('clickable');

}

$pager.find('span.page-number:first').addClass('active');

$pager.insertBefore($table);

Now we have an indicator of the current status of the pager:

jQuery Table Manipulation: Part 1

Paging with Sorting

We began with sorting this discussion by noting that sorting and paging controls needed to be aware of one another to avoid confusing results. Now that we have a working pager, we need to make sort operations respect the current page selection.

Doing this is as simple as calling our repaginate() function whenever a sort is performed. The scope of the function, though, makes this problematic. We can't reach repaginate() from our sorting routine because it is contained inside a different $(document).ready() handler. We could just consolidate the two pieces of code, but instead let's be a bit sneakier. We can decouple the behaviors, so that a sort calls the repaginate behavior if it exists, but ignores it otherwise. To accomplish this, we'll use a handler for a custom event.

In our earlier event handling discussion, we limited ourselves to event names that were triggered by the web browser, such as click and mouseup. The .bind() and .trigger() methods are not limited to these events, though; we can use any string as an event name. In this case, we can define a new event called repaginate as a stand-in for the function we've been calling:

$table.bind('repaginate', function() {

  $table.find('tbody tr').show()

    .lt(currentPage * numPerPage)

      .hide()

    .end()

    .gt((currentPage + 1) * numPerPage - 1)

      .hide()

    .end();

});

Now in places where we were calling repaginate(), we can call:

$table.trigger('repaginate');

We can issue this call in our sort code as well. It will do nothing if the table does not have a pager, so we can mix and match the two capabilities as desired.

The Finished Code

The completed sorting and paging code in its entirety follows:

$.fn.alternateRowColors = function() {

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

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

  return this;

};

 

$(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);

    $table.alternateRowColors($table);

    $table.find('th').each(function(column) {

      var findSortKey;

 

      if ($(this).is('.sort-alpha')) {

        findSortKey = function($cell) {

          return $cell.find('.sort-key').text().toUpperCase() +
                                    ' ' + $cell.text().toUpperCase();

        };

      }

      else if ($(this).is('.sort-numeric')) {

        findSortKey = function($cell) {

          var key = parseFloat($cell.text().replace(/^[^d.]*/, ''));

          return isNaN(key) ? 0 : key;

        };

      }

      else if ($(this).is('.sort-date')) {

        findSortKey = function($cell) {

          return Date.parse('1 ' + $cell.text());

        };

      }

 

      if (findSortKey) {

        $(this).addClass('clickable').hover(function() {

          $(this).addClass('hover');

        }, function() {

          $(this).removeClass('hover');

        }).click(function() {

          var newDirection = 1;

          if ($(this).is('.sorted-asc')) {

            newDirection = -1;

          }

 

          rows = $table.find('tbody > tr').get();

 

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

            row.sortKey =
                        findSortKey($(row).children('td').eq(column));

          });

          rows.sort(function(a, b) {

            if (a.sortKey < b.sortKey) return -newDirection;

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

            return 0;

          });

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

            $table.children('tbody').append(row);

            row.sortKey = null;

          });

 

          $table.find('th').removeClass('sorted‑asc')
                                        .removeClass('sorted-desc');

          var $sortHead = $table.find('th').filter(':nth-child('
                                               + (column + 1) + ')');

          if (newDirection == 1) {

            $sortHead.addClass('sorted-asc');

          } else {

            $sortHead.addClass('sorted-desc');

          }

          $table.find('td').removeClass('sorted')

            .filter(':nth-child(' + (column + 1) + ')')
                                                 .addClass('sorted');

          $table.alternateRowColors($table);

          $table.trigger('repaginate');

        });

      }

    });

  });

});

$(document).ready(function() {

  $('table.paginated').each(function() {

    var currentPage = 0;

    var numPerPage = 10;

   

    var $table = $(this);

   

    $table.bind('repaginate', function() {

      $table.find('tbody tr').show()

        .lt(currentPage * numPerPage)

          .hide()

        .end()

        .gt((currentPage + 1) * numPerPage - 1)

          .hide()

        .end();

    });

 

    var numRows = $table.find('tbody tr').length;

    var numPages = Math.ceil(numRows / numPerPage);

 

    var $pager = $('<div class="pager"></div>');

    for (var page = 0; page < numPages; page++) {

      $('<span class="page-number">' + (page + 1) + '</span>')

       .bind('click', {'newPage': page}, function(event) {

         currentPage = event.data['newPage'];

         $table.trigger('repaginate');

         $(this).addClass('active').siblings().removeClass('active');

       })

       .appendTo($pager).addClass('clickable');

    }

    $pager.find('span.page-number:first').addClass('active');

    $pager.insertBefore($table);

   

    $table.trigger('repaginate');

  });

});

>> Continue Reading jQuery Table Manipulation, Part Two

 

[ 1 | 2 ]
jQuery 1.4 Reference Guide This book and eBook is a comprehensive exploration of the popular JavaScript library
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Jonathan Chaffer

Jonathan Chaffer is a member of Rapid Development Group, a web development firm located in Grand Rapids, Michigan. His work there includes overseeing and implementing projects in a wide variety of technologies, with an emphasis in PHP, MySQL, and JavaScript. He also leads on-site training seminars on the jQuery framework for web developers.

In the open source community, he has been very active in the Drupal CMS project, which has adopted jQuery as its JavaScript framework of choice. He is the creator of the Content Construction Kit, a popular module for managing structured content on Drupal sites. He is also responsible for major overhauls of Drupal's menu system and developer API reference.

He lives in Grand Rapids with his wife, Jennifer.

Karl Swedberg

Karl Swedberg is a web developer at Fusionary Media in Grand Rapids, Michigan, where he spends much of his time making cool things happen with JavaScript. As a member of the jQuery team, he is responsible for maintaining the jQuery API site at api.jquery.com. He is also a member of jQuery's Board of Advisors and a presenter at workshops and conferences. When he isn't coding, he likes to hang out with his family, roast coffee in his garage, and exercise at the local CrossFit gym.

Books From Packt

Learning jQuery 1.3
Learning jQuery 1.3

jQuery 1.4 Reference Guide
jQuery 1.4 Reference Guide

Joomla! 1.5 JavaScript jQuery
Joomla! 1.5 JavaScript jQuery

NetBeans Platform 6.9 Developer's Guide
NetBeans Platform 6.9 Developer's Guide

Spring Security 3
Spring Security 3

JavaFX 1.2 Application Development Cookbook
JavaFX 1.2 Application Development Cookbook

GlassFish Security
GlassFish Security

High Availability MySQL Cookbook
High Availability MySQL Cookbook

Your rating: None Average: 4.4 (14 votes)
Hello everyone by
it is nice to find a forum like this. i ajust found this forum on the internet.
Fast JS sorting, paging & search with jOrder by
If you want to perform fast queries on datasets in the 1k < range, use jOrder (http://jorder.net, http://github.com/danstocker/jorder) and display the result set using jQuery plugins for instance. jOrder is lightning fast, and works on massive tables in the browser, even on mobile devices. Benchmarks: http://wiki.github.com/danstocker/jorder/benchmarks

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Q
G
8
u
z
z
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software