programming4us
programming4us
MOBILE

jQuery 1.3 : Table Manipulation - Sorting and paging (part 1) : Server-side sorting & JavaScript sorting

- How To Install Windows Server 2012 On VirtualBox
- How To Bypass Torrent Connection Blocking By Your ISP
- How To Install Actual Facebook App On Kindle Fire
2/21/2011 11:34:11 AM
Two of the most common tasks performed with tabular data are sorting and paging. In a large table, being able to rearrange the information that we're looking for is invaluable. Unfortunately, these helpful operations can be some of the trickiest to put into action.

First, we'll look at what it takes to perform table sorting, reordering data into a sequence that is most helpful to the user.

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, though, when the user can determine the sort order. A common method is to make the table headers (<th>) 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">
<thead>
<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>
</thead>
<tbody>
...
</tbody>
</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 th a').click(function() {
$('#my-data tbody').load($(this).attr('href'));
return false;
});
});

Now when the anchors are clicked, jQuery sends an AJAX request to the server for the same page. When jQuery is used to make a page request using AJAX, it sets the X-Requested-With HTTP header to XMLHttpRequest so that the server can determine that an AJAX request is being made. The server code can be written to send back only the content of the<tbody> element 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 existing<tbody> element.

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 available, 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, the author names, 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="../images/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>
JavaScript sorting, sortingexample<td><img src="../images/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 the data by their respective columns. Let's explore some 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:

Using these row grouping tags, we will be able to easily select and manipulate the data rows without affecting the header.

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>

Using JavaScript to sort arrays

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 comparator function as an argument. This function compares two items in the array and should return a positive or negative number depending on which item should come first in the sorted array.

For example, take a simple array of numbers:

var arr = [52, 97, 3, 62, 10, 63, 64, 1, 9, 3, 4];

We can sort this array by calling arr.sort(). After this, the items are in the order:

[1, 10, 3, 3, 4, 52, 62, 63, 64, 9, 97]

By default, as we see here, the items are sorted lexicographically (in alphabetical order). In this case it might make more sense to sort the items numerically. To do this, we can supply a comparator function to the .sort() method:

arr.sort(function(a,b) {
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
});

This function returns a negative number if a should come first in the sorted array, a positive number if b should come first, and zero if the order of the items does not matter. With this information in hand, the .sort() method can sequence the items appropriately:

[1, 3, 3, 4, 9, 10, 52, 62, 63, 64, 97]

Using a comparator to sort table rows

Our initial sort routine looks like this:

$(document).ready(function() {
$('table.sortable').each(function() {
var $table = $(this);
$('th', $table).each(function(column) {
var $header = $(this);
if ($header.is('.sort-alpha')) {
$header.addClass('clickable').hover(function() {
$header.addClass('hover');
}, function() {
$header.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 the iteration explicit. Even though we could bind a click handler to all headers that have the sort-alpha $('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. class just by calling

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().

Now that we have an array of DOM nodes, we can sort them, but to do this we need to write an appropriate comparator function. We want to sort the rows according to the textual contents of the relevant table cells, so this will be the information the comparator function will examine. 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. We store the key values in variables to avoid redundant calculations, compare them, and return a positive or negative number as discussed above.

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 the AJAX solution discussed earlier, this technique cannot function without JavaScript; we are assuming the server has no scripting language available to it for this example. Since JavaScript is required for the sort to work, we are adding the clickable class through code only, thereby making sure that the interface indicates that sorting is possible (with a background image) only if 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:

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) {
var $header = $(this);
if ($header.is('.sort-alpha')) {
$header.addClass('clickable').hover(function() {
$header.addClass('hover');
}, function() {
$header.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;
basic alphabetical sorting, JavaScript sortingcomparator, usingif (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:

The power of plugins

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

jQuery.fn.alternateRowColors = function() {
JavaScript sorting, sortingplug-ins$('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.

  • It is defined as a new property of jQuery.fn rather than as a standalone function. This registers the function as a plugin method.

  • We use the keyword this as a replacement for our $table parameter. Within a plugin method, this refers to the jQuery object that is being acted upon.

  • Finally, we return this at the end of the function. Supplying the jQuery object as the return value makes our new method chainable.

With our new plugin defined, we can call $table.alternateRowColors(), a more natural jQuery statement, instead of alternateRowColors($table).
Performance concerns

Our code works, but it 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). It is safe to say, though, that doubling the number of items in an array will more than double the number of times the comparator function is called.

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

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

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 .sortKey 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 strictly 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.


Instead of using expando properties, jQuery provides an alternative data storage mechanism we could use. The .data() method sets or retrieves arbitrary information associated with page elements, and the .removeData() method gets rid of any such stored information:

$.each(rows, function(index, row) {
$(row).data('sortKey', $(row).children('td')

.eq(column).text().toUpperCase());
});
rows.sort(function(a, b) {
if ($(a).data('sortKey') < $(b).data('sortKey'))

return -1;
if ($(a).data('sortKey') > $(b).data('sortKey'))

return 1;
return 0;
});
$.each(rows, function(index, row) {
$table.children('tbody').append(row);
$(row).removeData('sortKey');

});

Using .data() instead of expando properties can, at times, be more convenient, since we are often working with jQuery objects rather than directly with DOM nodes. It also avoids potential problems with Internet Explorer memory leaks. However, for the remainder of this example, we will stick with expando properties in order to practice switching between operations on DOM nodes and operations on jQuery objects.

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. 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="../images/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="../images/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>


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 already 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 provided key, thereby sorting by last name:

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

Sorting other types of data

Our user should be able to sort not just by the Title and Author(s) columns, but the Publish Date and Price columns as well. Since we streamlined our comparator function, it can handle all kinds of data, but first 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 so that we can compare them numerically:

var key = parseFloat($cell.text().replace(/^[^\d.]*/, ''));
row.sortKey = isNaN(key) ? 0 : key;

The regular expression used here removes any leading characters other than numbers and decimal points, passing the result on to parseFloat(). The result of parseFloat() then needs to be checked, because if no number can be extracted from the text, NaN (not a number) is returned. This 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:

jQuery.fn.alternateRowColors = function() {
$('tbody tr:odd', this)
.removeClass('even').addClass('odd');
$('tbody tr:even', this)
.removeClass('odd').addClass('even');
return this;
};
$(document).ready(function() {
$('table.sortable').each(function() {
var $table = $(this);
$table.alternateRowColors();
$('th', $table).each(function(column) {
var $header = $(this);
var findSortKey;
if ($header.is('.sort-alpha')) {
findSortKey = function($cell) {
return $cell.find('.sort-key')
.text().toUpperCase()
+ ' ' + $cell.text().toUpperCase();
};
}
else if ($header.is('.sort-numeric')) {
findSortKey = function($cell) {
var key = $cell.text().replace(/^[^\d.]*/, '');
key = parseFloat(key);
return isNaN(key) ? 0 : key;
};
}
else if ($header.is('.sort-date')) {
findSortKey = function($cell) {
return Date.parse('1 ' + $cell.text());
};
}
if (findSortKey) {

$header.addClass('clickable').hover(function() {
$header.addClass('hover');
}, function() {
$header.removeClass('hover');
}).click(function() {
var rows = $table.find('tbody > tr').get();
$.each(rows, function(index, row) {
var $cell = $(row).children('td').eq(column);
row.sortKey = findSortKey($cell);

});
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;
});
$table.alternateRowColors();
});
}
});
});
});


The findSortKey variable doubles as the function to calculate the key, and a flag to indicate whether the column header is marked with a class making it sortable. We can now sort on date or price:

Column highlighting

It can be a nice user interface enhancement to visually remind the user of what has been done in the past. By highlighting the column that was most recently used for sorting, we can focus the user's attention on the part of the table that is most likely to be relevant. Fortunately, since we've already determined how to select the table cells in the column, applying a class to those cells is simple:

$table.find('td').removeClass('sorted')
.filter(':nth-child(' + (column + 1) + ')')
.addClass('sorted');

This snippet first removes the sorted class from all cells, then adds it to cells that are in the same column we just used for our sort. Note that we have to add 1 to the column index we found earlier, since the :nth-child() selector is one-based rather than zero-based. With this code in place, we get a highlighted column after any sort operation:

Alternating sort directions

Our final sorting enhancement is to allow for both ascending and descending sort orders. When the user clicks on a column that is already sorted, we want to reverse the current sort order.

To reverse a sort, all we have to do is to invert the values returned by our comparator. We can do this with a simple variable:

if (a.sortKey < b.sortKey) return -sortDirection;
if (a.sortKey > b.sortKey) return sortDirection;

If sortDirection equals 1, then the sort will be the same as before. If it equals -1, the sort will be reversed. We can use classes to keep track of the current sort order of a column:

jQuery.fn.alternateRowColors = function() {
$('tbody tr:odd', this)
.removeClass('even').addClass('odd');
$('tbody tr:even', this)
.removeClass('odd').addClass('even');
return this;
};
$(document).ready(function() {
$('table.sortable').each(function() {
var $table = $(this);
$table.alternateRowColors();
$('th', $table).each(function(column) {
var $header = $(this);
var findSortKey;
if ($header.is('.sort-alpha')) {
findSortKey = function($cell) {
return $cell.find('.sort-key').text().toUpperCase()
+ ' ' + $cell.text().toUpperCase();
};
}
else if ($header.is('.sort-numeric')) {
findSortKey = function($cell) {
var key = $cell.text().replace(/^[^\d.]*/, '');
key = parseFloat(key);
return isNaN(key) ? 0 : key;
};
}
else if ($header.is('.sort-date')) {
findSortKey = function($cell) {
return Date.parse('1 ' + $cell.text());
};
}
if (findSortKey) {
$header.addClass('clickable').hover(function() {
$header.addClass('hover');
}, function() {
$header.removeClass('hover');
}).click(function() {
var sortDirection = 1;
if ($header.is('.sorted-asc')) {
sortDirection = -1;
}

var rows = $table.find('tbody > tr').get();
$.each(rows, function(index, row) {
var $cell = $(row).children('td').eq(column);
row.sortKey = findSortKey($cell);
});
rows.sort(function(a, b) {
JavaScript sorting, sortingsort, reversingif (a.sortKey < b.sortKey) return -sortDirection;
if (a.sortKey > b.sortKey) return sortDirection;
return 0;
});
$.each(rows, function(index, row) {
$table.children('tbody').append(row);
row.sortKey = null;
});
$table.find('th').removeClass('sorted-asc')
.removeClass('sorted-desc');
if (sortDirection == 1) {
$header.addClass('sorted-asc');
}
else {
$header.addClass('sorted-desc');
}

$table.find('td').removeClass('sorted')
.filter(':nth-child(' + (column + 1) + ')')
.addClass('sorted');
$table.alternateRowColors();
});
}
});
});
});


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

Other  
  •  Windows Phone 7 Development : Understanding Trial and Full Modes (part 3) - Simulating Application Trial and Full Modes
  •  Windows Phone 7 Development : Understanding Trial and Full Modes (part 2) - Using the Marketplace APIs
  •  Windows Phone 7 Development : Understanding Trial and Full Modes (part 1) - Using the IsTrial Method
  •  Mobile Application Security : SymbianOS Security - Application Packaging
  •  Mobile Application Security : SymbianOS Security - Code Security
  •  iPhone Application Development : Getting the User’s Attention - Generating Alerts
  •  iPhone Application Development : Getting the User’s Attention - Exploring User Alert Methods
  •  iPhone Application Development : Using Advanced Interface Objects and Views - Using Scrolling Views
  •  Working with the Windows Phone 7 Application Life Cycle (part 2) - Managing Application State
  •  Working with the Windows Phone 7 Application Life Cycle (part 1) - Observing Application Life Cycle Events
  •  
    Top 10
    - Microsoft Visio 2013 : Adding Structure to Your Diagrams - Finding containers and lists in Visio (part 2) - Wireframes,Legends
    - Microsoft Visio 2013 : Adding Structure to Your Diagrams - Finding containers and lists in Visio (part 1) - Swimlanes
    - Microsoft Visio 2013 : Adding Structure to Your Diagrams - Formatting and sizing lists
    - Microsoft Visio 2013 : Adding Structure to Your Diagrams - Adding shapes to lists
    - Microsoft Visio 2013 : Adding Structure to Your Diagrams - Sizing containers
    - Microsoft Access 2010 : Control Properties and Why to Use Them (part 3) - The Other Properties of a Control
    - Microsoft Access 2010 : Control Properties and Why to Use Them (part 2) - The Data Properties of a Control
    - Microsoft Access 2010 : Control Properties and Why to Use Them (part 1) - The Format Properties of a Control
    - Microsoft Access 2010 : Form Properties and Why Should You Use Them - Working with the Properties Window
    - Microsoft Visio 2013 : Using the Organization Chart Wizard with new data
    REVIEW
    - First look: Apple Watch

    - 3 Tips for Maintaining Your Cell Phone Battery (part 1)

    - 3 Tips for Maintaining Your Cell Phone Battery (part 2)
    programming4us programming4us
    programming4us
     
     
    programming4us