|
|
BROWSE
All Titles WordPress Web Services SOA BPEL Web Graphics & Video Web Development RAW Portugues, Espanol, Italiano, French PHP/MySQL Oracle Open Source Networking & Telephony Moodle Microsoft & .NET Linux Servers jQuery Joomla! JBoss Java e-Learning e-Commerce Dynamics Drupal CRM Cookbook Content Management Beginner Guides Architecture and Analysis AJAX Future Titles Recently Published Titles The accordion widget is another UI widget made up of a series of containers for your content, all of which are closed except for one. Therefore, most of its content is initially hidden from view. Each container has a heading element associated with it, which is used to open the container and display the content. When you click on a heading, its content is displayed. When you click on another heading, the currently visible content is hidden while the new content is shown. It should be noted that the height of the accordion's container element will automatically be set so that there is room to show the tallest content panel in addition to the headers. This will vary, of course, depending on the width that you set on the widget's container. In this article by Dan Wellman, we are going to cover the following topics:
See More |
jQuery Table Manipulation
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. 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. |
|
|
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:

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 waysServer-Side Pagination and JavaScript 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.
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:

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:

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.
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 pageten 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.~
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:

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:

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:

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:

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 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');
});
});
|
|
The Dojo Toolkit is an Open source JavaScript toolkit which can be used to develop stunning web pages. I liked it from the very beginning. It is very fast and provides lots of tools to work with DOM, Animations, AJAX etc. The base code is lightweight (~26 KB). jQuery, even lighter, also Open Source, is the write-less, do-more, cross-browser, CSS3 compliant JavaScript library. In this article by Dr. Jayaram Krishnaswamy, we will experiment embedding jQuery in DOJO 123's Accordion widget and try to identify if there exists any cross-code interactions. The code is also tested for cross-browser suitability.
| ||||||||