programming4us
programming4us
MOBILE

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

2/21/2011 11:38:32 AM

Server-side 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.

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, we have a table with eight rows and two columns in it, sorted initially by the first column. If the data is re-sorted by the second column, many rows may change places:

Now let's consider what happens when pagination is added to the mix. Suppose only the first four rows are provided by the server and the browser attempts to sort the data. 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:

Only the data already present on the page can be manipulated by JavaScript. To prevent this from being a problem, we must either perform both tasks on the server (polling the server for the correct data set on every page or sort operation), or both in the browser (with all possible data available to JavaScript at all times), so that the first displayed results are indeed the first rows in the data set:

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() {
paginating, dataJavaScript pagination$('table.paginated').each(function() {
var currentPage = 0;
var numPerPage = 10;
var $table = $(this);
$table.find('tbody tr').hide()
.slice(currentPage * numPerPage,
(currentPage + 1) * numPerPage)
.show();
});
});

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 hide all the rows first, then select the rows on the current page, showing the selected rows. The .slice() method shown here works like the array method of the same name; it reduces the selection to the elements in between the two positions given.

The most error-prone task in writing this code is formulating the expressions to use in the .slice() filter. 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; the .slice() method fetches the rows up to and not including this second parameter.

Displaying the pager

To add user interaction to the mix, we need to place a pager next to the table: a set of links for navigating to different pages of data. 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. Since the division may 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 above the table:

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').hide()
.slice(currentPage * numPerPage,
(currentPage + 1) * numPerPage)
.show();
};
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"></span>').text(page + 1)
.click(function() {
currentPage = page;
repaginate();
}).appendTo($pager).addClass('clickable');
}
$pager.insertBefore($table);
});
});


This works, in that the new repaginate() function is called when the page loads and when any of the page links are clicked. All of the links present us with a table that has no data rows, though:

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 also 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 when the loop is complete).

More information on how closures work can be found in Appendix C.


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 custom event 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"></span>').text(page + 1)
JavaScript paginationcustom event data, adding.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 hazards of the closure, and is frozen in time at the value it contained when the handler was bound. Now our pager links can correctly take us to different pages:

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:

$(document).ready(function() {
JavaScript paginationcurrent page number, highlighting$('table.paginated').each(function() {
var currentPage = 0;
var numPerPage = 10;
var $table = $(this);
var repaginate = function() {
$table.find('tbody tr').hide()
.slice(currentPage * numPerPage,
(currentPage + 1) * numPerPage)
.show();
};
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"></span>').text(page + 1)
.bind('click', {newPage: page}, function(event) {
currentPage = event.data['newPage'];
repaginate();
$(this).addClass('active')
.siblings().removeClass('active');

}).appendTo($pager).addClass('clickable');
}
$pager.insertBefore($table)
.find('span.page-number:first').addClass('active');

});
});


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

Paging with sorting

We began 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 methods are not limited to these events, though; we can use any string as an event name. Using this capability, we can define a new event called repaginate as a stand-in for the function we've been calling: .trigger()

$table.bind('repaginate', function() {
$table.find('tbody tr').hide()
.slice(currentPage * numPerPage,
(currentPage + 1) * numPerPage)
.show();
});

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:

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) {
if (a.sortKey < b.sortKey) return -sortDirection;
if (a.sortKey > b.sortKey) return sortDirection;
return 0;
sortingsorting and paging code});
pagingsorting and paging code$.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();
$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').hide()
.slice(currentPage * numPerPage,
(currentPage + 1) * numPerPage)
.show();
});
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"></span>').text(page + 1)
.bind('click', {newPage: page}, function(event) {
currentPage = event.data['newPage'];
$table.trigger('repaginate');
$(this).addClass('active')
.siblings().removeClass('active');
}).appendTo($pager).addClass('clickable');
}
$pager.insertBefore($table)
.find('span.page-number:first').addClass('active');
});
});
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
  •  
    PS4 game trailer XBox One game trailer
    WiiU game trailer 3ds game trailer
    Top 10 Video Game
    -   Minecraft Mods - MAD PACK #10 'NETHER DOOM!' with Vikkstar & Pete (Minecraft Mod - Mad Pack 2)
    -   Minecraft Mods - MAD PACK #9 'KING SLIME!' with Vikkstar & Pete (Minecraft Mod - Mad Pack 2)
    -   Minecraft Mods - MAD PACK #2 'LAVA LOBBERS!' with Vikkstar & Pete (Minecraft Mod - Mad Pack 2)
    -   Minecraft Mods - MAD PACK #3 'OBSIDIAN LONGSWORD!' with Vikkstar & Pete (Minecraft Mod - Mad Pack 2)
    -   Total War: Warhammer [PC] Demigryph Trailer
    -   Minecraft | MINIONS MOVIE MOD! (Despicable Me, Minions Movie)
    -   Minecraft | Crazy Craft 3.0 - Ep 3! "TITANS ATTACK"
    -   Minecraft | Crazy Craft 3.0 - Ep 2! "THIEVING FROM THE CRAZIES"
    -   Minecraft | MORPH HIDE AND SEEK - Minions Despicable Me Mod
    -   Minecraft | Dream Craft - Star Wars Modded Survival Ep 92 "IS JOE DEAD?!"
    -   Minecraft | Dream Craft - Star Wars Modded Survival Ep 93 "JEDI STRIKE BACK"
    -   Minecraft | Dream Craft - Star Wars Modded Survival Ep 94 "TATOOINE PLANET DESTRUCTION"
    -   Minecraft | Dream Craft - Star Wars Modded Survival Ep 95 "TATOOINE CAPTIVES"
    -   Hitman [PS4/XOne/PC] Alpha Gameplay Trailer
    -   Satellite Reign [PC] Release Date Trailer
    Video
    programming4us
     
     
    programming4us