|
|
BOOK ![]() Learning jQuery : Better Interaction Design and Web Development with Simple JavaScript Techniques See More BOOK ![]() jQuery Reference Guide See More See More |
jQuery Table Manipulation Part 2
Read Part One of jQuery Table Manipulation here. Advanced Row StripingRow striping can be as simple as two lines of code to alternate the background color: $(document).ready(function() { $('table.sortable tbody tr:odd').addClass('odd'); $('table.sortable tbody tr:even').addClass('even'); }); If we declare background colors for the odd and even classes as follows, we can see the rows in alternating shades of gray: tr.even { background-color: #eee; } tr.odd { background-color: #ddd; } While this code works fine for simple table structures, if we introduce non‑standard rows into the table, such as sub-headings, the basic odd-even pattern no longer suffices. For example, suppose we have a table of news items grouped by year, with columns for date, headline, author, and topic. One way to express this information is to wrap each year's news items in a <tbody> element and use <th colspan="4"> for the subheading. Such a table's HTML (in abridged form) would look like this: <table class="striped"> <thead> <tr> <th>Date</th> <th>Headline</th> <th>Author</th> <th class="filter-column">Topic</th> </tr> </thead> <tbody> <tr> <th colspan="4">2007</th> </tr> <tr> <td>Mar 11</td> <td>SXSWi jQuery Meetup</td> <td>John Resig</td> <td>conference</td> </tr> <tr> <td>Feb 28</td> <td>jQuery 1.1.2</td> <td>John Resig</td> <td>release</td> </tr> <tr> <td>Feb 21</td> <td>jQuery is OpenAjax Compliant</td> <td>John Resig</td> <td>standards</td> </tr> <tr> <td>Feb 20</td> <td>jQuery and Jack Slocum's Ext</td> <td>John Resig</td> <td>third-party</td> </tr> </tbody> <tbody> <tr> <th colspan="4">2006</th> </tr> <tr> <td>Dec 27</td> <td>The Path to 1.1</td> <td>John Resig</td> <td>source</td> </tr> <tr> <td>Dec 18</td> <td>Meet The People Behind jQuery</td> <td>John Resig</td> <td>announcement</td> </tr> <tr> <td>Dec 13</td> <td>Helping you understand jQuery</td> <td>John Resig</td> <td>tutorial</td> </tr> </tbody> <tbody> <tr> <th colspan="4">2005</th> </tr> <tr> <td>Dec 17</td> <td>JSON and RSS</td> <td>John Resig</td> <td>miscellaneous</td> </tr> </tbody> </table>
With separate CSS styles applied to <th> elements within <thead> and <tbody>, a snippet of the table might look like this:
To ensure that the alternating gray rows do not override the color of the subheading rows, we need to adjust the selector expression: $(document).ready(function() { $('table.striped tbody tr:not([th]):odd').addClass('odd'); $('table.striped tbody tr:not([th]):even').addClass('even'); }); The added selector, :not([th]), removes any table row that contains a <th> from the matched set of elements. Now the table will look like this:
Three-color Alternating PatternThere may be times when we want to apply more complex striping. For example, we can apply a pattern of three alternating row colors rather than just two. To do so, we first need to define another CSS rule for the third row. We'll also reuse the odd and even styles for the other two, but add more appropriate class names for them: tr.even, tr.first { background-color: #eee; } tr.odd, tr.second { background-color: #ddd; } tr.third { background-color: #ccc; } To apply this pattern, we start the same way as the previous example—by selecting all rows that are descendants of a <tbody>, but filtering out the rows that contain a <th>. This time, however, we attach the .each() method so that we can use its built-in index: $(document).ready(function() { $('table.striped tbody tr').not('[th]').each(function(index) { //Code to be applied to each element in the matched set. }); }); To make use of the index, we can assign our three classes to a numeric key: 0, 1, or 2. We'll do this by creating an object, or map: $(document).ready(function() { var classNames = { 0: 'first', 1: 'second', 2: 'third' }; $('table.striped tbody tr').not('[th]').each(function(index) { // Code to be applied to each element in the matched set. }); }); Finally, we need to add the class that corresponds to those three numbers, sequentially, and then repeat the sequence. The modulus operator, designated by a %, is especially convenient for such calculations. A modulus returns the remainder of one number divided by another. This modulus, or remainder value, will always range between 0 and one less than the dividend. Using 3 as an example, we can see this pattern: 3/3 = 1, remainder 0. 4/3 = 1, remainder 1. 5/3 = 1, remainder 2. 6/3 = 2, remainder 0. 7/3 = 2, remainder 1. 8/3 = 3, remainder 2. And so on. Since we want the remainder range to be 0 – 2, we can use 3 as the divisor (second number) and the value of index as the dividend (first number). Now we simply put that calculation in square brackets after classNames to retrieve the corresponding class from the object variable as the .each() method steps through the matched set of rows: $(document).ready(function() { var classNames = { 0: 'first', 1: 'second', 2: 'third' }; $('table.striped tbody tr').not('[th]').each(function(index) { $(this).addClass(classNames[index % 3]); }); }); With this code in place, we now have the table striped with three alternating background colors:
We
could of course extend this pattern to four, five, six, or more background
colors by adding key-value pairs to the object variable and increasing the value
of the divisor in
classNames[index
%
n]. Alternating TripletsSuppose we want to use two colors, but have each one display three rows at a time. For this, we can employ the odd and even classes again, as well as the modulus operator. But we'll also reset the class each time we're presented with a row containing <th> elements. If we don't reset the alternating row class, we may be faced with unexpected colors after the first group of rows is striped. So far, our example table has avoided such problems because the first group consists of 12 rows, which, conveniently, is divisible by both 2 and 3. For the triplet striping scenario, we'll remove two rows, leaving us with 10 in the first group, to emphasize the class resetting. We begin this striping technique by setting two variables, rowClass and rowIndex. We'll use the .each() method this time as well, but rather than relying on the built-in index, we'll use a custom rowIndex variable so that we can reset it on the rows with <th>: $(document).ready(function() { var rowClass = 'even'; var rowIndex = 0; $('table.striped tbody tr').each(function(index) { $(this).addClass(rowClass); }); }); Notice that since we have removed the :not([th]) selector, we'll have to account for those subheading rows within the .each(). But first, let's get the triplet alternation working properly. So far, every <tr> will become <tr class="even">. For each row, we can check to see if the rowIndex % 3 equals 0. If it does, we toggle the value of rowClass. Then we increment the value of rowIndex: $(document).ready(function() { var rowClass = 'even'; var rowIndex = 0; $('table.striped tbody tr').each(function(index) { if (rowIndex % 3 == 0) { rowClass = (rowClass == 'even' ? 'odd' : 'even'); }; $(this).addClass(rowClass); rowIndex++; }); });
A ternary, or conditional, operator is used to set the changed value of rowClass because of its succinctness. That single line could be rewritten as: if (rowClass == 'even') { rowClass = 'odd'; } else { rowClass = 'even'; } In either case, the code now produces table striping that looks like this:
#content tbody th { background-color: #6f93ce; padding-left: 6px; } tr.even { background-color: #eee; } tr.odd { background-color: #ddd; } Nevertheless, because the rowIndex numbering does not account for these subheading rows, we have mis-classed rows from the start; this is evident because the first striping color change occurs after two rows rather than three. We need to include another condition, checking if the current row contains a <th>. If it does, we'll set the value of rowClass to subhead and set rowIndex to -1: $(document).ready(function() { var rowClass = 'even'; var rowIndex = 0; $('table.striped tbody tr').each(function(index) { if ($('th', this).length) { rowClass = 'subhead'; rowIndex = -1; } else if (rowIndex % 3 == 0) { rowClass = (rowClass == 'even' ? 'odd' : 'even'); }; $(this).addClass(rowClass); rowIndex++; }); }); With
rowIndex
at
-1
for the subheading rows, the variable will be incremented to
0
for the next row—precisely where we want it to start for each group of striped
rows. Now we can see the striping with each year's articles beginning with three
light colored rows and alternating three at a time between lighter and darker:
A final note about this striping code—while the ternary operator is indeed concise, it can get confusing when the conditions get more complex. The sophisticated striping variations can be more easily managed by using basic if-else conditions instead: $(document).ready(function() { var rowIndex = 0; $(tbody tr').each(function(index) { if ($('th',this).length) { $(this).addClass('subhead'); rowIndex = -1; } else { if (rowIndex % 6 < 3) { $(this).addClass('even'); } else { $(this).addClass('odd'); } }; rowIndex++; }); }); Now
we've achieved the same effect as before, but also made it easier to include
additional
else
if
conditions. Row HighlightingAnother visual enhancement that we can apply to our news article table is row highlighting based on user interaction. Here we'll respond to clicking on an author's name by highlighting all rows that have the same name in their author cell. Just as we did with the row striping, we can modify the appearance of these highlighted rows by adding a class: #content tr.highlight { background: #ff6; } It's important that we give this new highlight class adequate specificity for the background color to override that of the even and odd classes. Now we need to select the appropriate cell and attach the .click() method to it: $(document).ready(function() { var column = 3; $('table.striped td:nth-child(' + column + ')' ) .click(function() { // Do something on click. }); }); Notice that we use the :nth-child(n) pseudo-class as part of the selector expression, but rather than simply including the number of the child element, we pass in the column variable. We'll need to refer to the same nth-child again, so using a variable allows us to change it in only one place if we later decide to highlight based on a different column.
Unlike JavaScript indices, the CSS-based :nth-child(n) pseudo-class begins numbering at 1, not 0.
When
the user clicks a cell in the third column, we want the cell's text to be
compared to that of the same column's cell in every other row. If it matches,
the
highlight class
will be toggled. In other words, the class will be added if it isn't already
there and removed if it is. This way, we can click on an author cell to remove
the row highlighting if that cell or one with the same author has already been
clicked: $(document).ready(function() { $('table.striped td:nth-child(' + column + ')' ) .click(function() { var thisClicked = $(this).text();
$('table.striped
td:nth-child(' + column + ')') if (thisClicked == $(this).text()) { $(this).parent().toggleClass('highlight'); }; }); }); }) The code is working well at this point, except when a user clicks on two authors' names in succession. Rather than switching the highlighted rows from one author to the next as we might expect, it adds the second clicked author's rows to the group that has class="highlight". To avoid this behavior, we can add an else statement to the code, removing the highlight class for any row that does not have the same author name as the one clicked: $(document).ready(function() { $('table.striped td:nth-child(' + column + ')' ) .click(function() { var thisClicked = $(this).text(); $('table.striped td:nth-child(' + column + ')' ) .each(function(index) { if (thisClicked == $(this).text()) { $(this).parent().toggleClass('highlight'); } else { $(this).parent().removeClass('highlight'); }; }); }); }) Now when we click on Rey Bango, for example, we can see all of his articles much more easily:
If we then click on John Resig's name in any one of the cells, the highlighting will be removed from Rey Bango's rows and added to John's. TooltipsAlthough the row highlighting might be
a useful feature, so far it's not apparent to the user that the feature even
exists. We can begin to remedy this situation by giving all author cells a
clickable
class, which will change the cursor to a pointer when a user hovers the mouse
cursor over them: $(document).ready(function() { $('table.striped td:nth-child(' + column + ')' ) .addClass('clickable') .click(function() { var thisClicked = $(this).text(); $('table.striped td:nth-child(' + column + ')' ) .each(function(index) { if (thisClicked == $(this).text()) { $(this).parent().toggleClass('highlight'); } else { $(this).parent().removeClass('highlight'); }; }); }) }) The clickable class is a step in the right direction, for sure, but it still doesn't tell the user what will happen when the cell is clicked. As far as anyone knows (without looking at the code, of course) that clicking could send the user to another page. Some further indication of what will happen upon clicking is in order. Tooltips are a familiar feature of many software applications, including web browsers. We can simulate a tooltip with custom text, such as Click to highlight all rows authored by Rey Bango, when the mouse hovers over one of the author cells. This way, we can alert users to the effect their action will have. We're going to create three functions—showTooltip, hideTooltip, and positionTooltip—outside any event handlers and then call or reference them as we need them. Let's start with positionTooltip, which we'll reference when the mouse moves over any of the author cells: var positionTooltip = function(event) { var tPosX = event.pageX - 5; var tPosY = event.pageY + 20; $('div.tooltip').css({top: tPosY, left: tPosX}); }; Here we use the pageX and pageY properties of event to set the top and left positions of the tooltip. When we reference the function in the .mousemove() method, tPosX will refer to 5 pixels to the left of the mouse cursor while tPosY will refer to 20 pixels below the cursor. We can attach this method to the same chain as the one being used already for .click(): $(document).ready(function() { var positionTooltip = function(event) { var tPosX = event.pageX - 5; var tPosY = event.pageY + 20; $('div.tooltip').css({top: tPosY, left: tPosX}); };
$('table.striped td:nth-child(' + column + ')' ) .addClass('clickable') .click(function() { // ...Code continues... }) .mousemove(positionTooltip); }); So, we've positioned the tooltip already, but we still haven't created it. That will be done in the showTooltip function. The first thing that we do in the showTooltip function is remove all tooltips. This may seem counterintuitive, but if we are going to show the tooltip each time the mouse cursor hovers over an author cell; we don't want a proliferation of these tooltips appearing with each new cell hovered over: var showTooltip = function(event) { $('div.tooltip').remove(); }; Now we're ready to create the tooltip. We can wrap the entire <div> and its contents in a $() function and then append it to the document's body: var showTooltip = function(event) { $('div.tooltip').remove(); var $thisAuthor = $(this).text(); $('<div
class="tooltip">Click to highlight all articles .appendTo('body'); }; When the mouse cursor hovers over an author cell with Rey Bango in it, the tooltip will read, Click to highlight all articles written by Rey Bango. Unfortunately, the tooltip will appear at the bottom of the page. That's where the positionTooltip function comes in. We simply place that at the end of the showTooltip function: var showTooltip = function(event) { $('div.tooltip').remove(); var $thisAuthor = $(this).text(); $('<div
class="tooltip">Click to highlight all articles .appendTo('body'); positionTooltip(event); };
.tooltip { position: absolute; z-index: 2; background: #efd; border: 1px solid #ccc; padding: 3px; } The style rule also gives the tooltip a z-index
higher than that of the surrounding elements to ensure that it is
layered on top of them, as well as sprucing it up with a background
color, a border, and some padding. Finally, we write a simple hideTooltip function: var hideTooltip = function() { $('div.tooltip').remove(); };
And
now that we have functions for showing, hiding, and positioning the
tooltip, we can reference them at the appropriate places in our code: $(document).ready(function() { var column = 3; // Position the tooltip. var positionTooltip = function(event) { var tPosX = event.pageX - 5; var tPosY = event.pageY + 20; $('div.tooltip').css({top: tPosY, left: tPosX}); }; // Show (create) the tooltip. var showTooltip = function(event) { $('div.tooltip').remove(); var $thisAuthor = $(this).text(); $('<div
class="tooltip">Click to highlight all articles written positionTooltip(event); }; // Hide (remove) the tooltip. var hideTooltip = function() { $('div.tooltip').remove(); }; $('table.striped td:nth-child(' + column + ')' ) .addClass('clickable') .click(function(event) { var thisClicked = $(this).text(); $('table.striped
td:nth-child(' + column + ')' if (thisClicked == $(this).text()) { $(this).parent().toggleClass('highlight'); } else { $(this).parent().removeClass('highlight'); }; }); }) .hover(showTooltip, hideTooltip) .mousemove(positionTooltip); }) Note that the .hover() and .mousemove() methods are referencing functions that are defined elsewhere. As such, the functions take no parentheses. Also, because positionTooltip(event) is called inside showTooltip, the tooltip is immediately positioned on hover; it then continues to be referenced as the mouse cursor is moved over the cell due to the function's placement inside the .mousemove() method. The tooltip now looks like this:
Everything works fine now, with a tooltip that appears when we hover over an author cell, moves with the mouse movement, and disappears when we move the mouse cursor out of the cell. The only problem is that the tooltip continues to suggest clicking on a cell to highlight the articles even after those articles have been highlighted:
What we need is a way to change the
tooltip if the row has the
highlight
class. Fortunately, we have the
showTooltip
function, in which we can place a conditional test to check for the class. If
the current cell's parent
<tr>
has the
highlight
class, we add
un-
in front of the word
highlight
when we create the tooltip: $(document).ready(function() { var highlighted = ""; // Code continues... var showTooltip = function(event) { $('div.tooltip').remove(); var $thisAuthor = $(this).text(); if ($(this).parent().is('.highlight')) { highlighted = 'un-'; } else { highlighted = ''; };
$('<div
class="tooltip"> Click to ' positionTooltip(event); }; }; Our tooltip task would now be finished
were it not for the need to trigger the tooltip‑changing behavior when a cell is
clicked as well. For that, we need to call the
showTooltip
function inside the
.click() event
handler: $(document).ready(function() { // Code continues... .click(function(event) { var thisClicked = $(this).text(); $('table.striped td:nth-child(' + column + ')' ).each(function(index) { if (thisClicked == $(this).text()) { $(this).parent().toggleClass('highlight'); } else { $(this).parent().removeClass('highlight') }; }); showTooltip.call(this, event); }) // Code continues... }); By using the JavaScript call() function, we can invoke showTooltip as if it were defined within the .click() handler. Therefore, this inherits the scope of .click(). Additionally, we pass in event so that we can use its pageX and pageY information for the positioning.
Now the
tooltip offers a more intelligent suggestion when the hovered row is already
highlighted. Collapsing and ExpandingWhen large sets of data are grouped in tables, as each year's set of articles are in our News page, collapsing, or hiding, a section's contents can be a convenient way to get a broad view of all of the table's data without having to scroll so much. To make the sections of the news
article table collapsible, we first prepend a minus‑symbol image to each
subheading row's first cell. The image is inserted with JavaScript, because if
JavaScript is not available for the row collapsing, the image might confuse
those who expect clicking on it to actually trigger some kind of event: $(document).ready(function() { var toggleMinus = '../icons/bullet_toggle_minus.png'; var togglePlus = '../icons/bullet_toggle_plus.png'; var $subHead = $('tbody th:first-child'); $subHead.prepend('<img
src="' + toggleMinus + '" }); Note that we set variables for the location of both a minus‑symbol and a plus‑symbol image. This way we can change the image's src attribute when the image is clicked and the rows are collapsed or expanded. Next we use the .addClass() method to make the newly created images appear clickable: $(document).ready(function() { var toggleMinus = '../icons/bullet_toggle_minus.png'; var togglePlus = '../icons/bullet_toggle_plus.png'; var $subHead = $('tbody th:first-child'); $subHead.prepend('<img
src="' + toggleMinus + '" $('img', $subHead).addClass('clickable'); }); Finally, we can add code inside a .click() method to do the collapsing and expanding. A condition will check the current value of the clicked image's src attribute. If it equals the file path represented by the toggleMinus variable, then all of the other <tr> elements within the same <tbody> will be hidden, and the src attribute will be set to the value of the togglePlus variable. Otherwise, these <tr> elements will be shown and the src will change back to the value of toggleMinus: $(document).ready(function() { var toggleMinus = '../icons/bullet_toggle_minus.png'; var togglePlus = '../icons/bullet_toggle_plus.png'; var $subHead = $('tbody th:first-child'); $subHead.prepend('<img
src="' + toggleMinus + '" $('img', $subHead).addClass('clickable') .click(function() { var toggleSrc = $(this).attr('src'); if ( toggleSrc == toggleMinus ) { $(this).attr('src', togglePlus) .parents('tr').siblings().fadeOut('fast'); } else{ $(this).attr('src', toggleMinus) .parents('tr').siblings().fadeIn('fast'); }; }); }) With this code in place, clicking on the minus-symbol image next to 2007 makes the table look like this:
The 2007 news articles aren't removed; they are just hidden until we click the plus‑symbol image that now appears in that row. Table
rows present particular obstacles to animation, since browsers use different
values (table-row
and
block)
for their visible
display
property. The
.hide()
and
.show()
methods, without animation, are always safe to use with table rows. As of jQuery
version 1.1.3,
.fadeIn()
and
.fadeOut()
can be used as well. FilteringEarlier we examined sorting and paging as techniques for helping users focus on relevant portions of a table's data. We saw that both could be implemented either with server‑side technology or with JavaScript. Filtering completes this arsenal of data arrangement strategies. By displaying to the user only the table rows that match a given criterion, we can strip away needless distractions. We have already seen how to perform a type of filter, highlighting a set of rows. Now we will extend this idea to actually hiding rows that don't match the filter. We can begin by creating a place to put our filter buttons. In typical fashion, we insert these controls using JavaScript so that people without scripting available do not see the options: $(document).ready(function() { $('table.filterable').each(function() { var $table = $(this); $table.find('th').each(function (column) { if ($(this).is('.filter-column')) { var
$filters = $('<div class="filters"><h3>Filter by ' $filters.insertBefore($table); } }); }); }); We get the label for the filter box from the column headers, so that this code can be reused for other tables quite easily. Now we have a heading awaiting some buttons:
Filter OptionsNow we can move on to actually implementing a filter. To start with, we will add filters for a couple of known topics. The code for this is quite similar to the author highlighting example from before: var keywords = ['conference', 'release']; $.each(keywords, function (index, keyword) { $('<div
class="filter"></div>').text(keyword).bind('click', $table.find('tbody tr').each(function() { if
($('td', this).filter(':nth-child(' + (column + 1) + $(this).show(); } else if ($('th',this).length == 0){ $(this).hide(); } });
$(this).addClass('active').siblings().removeClass('active'); }).addClass('clickable').appendTo($filters); }); Starting with a static array of keywords to filter by, we loop through and create a button for each. Just as in the paging example, we need to use the data parameter of .bind() to avoid accidental closure problems. Then, in the click handler, we compare each cell against the keyword and hide the row if there is no match. We must check whether the row is a subheader, to avoid hiding those in the process. Both of the buttons now work as advertised:
Collecting Filter Options from ContentNow we need to expand the filter options to cover the range of available topics in the table. Rather than hard-coding all of the topics, we can gather them from the text that has been entered in the table. We can change the definition of keywords to read: var keywords = {};
$table.find('tbody tr
td').filter(':nth-child(' + (column + 1) + keywords[$(this).text()] = $(this).text(); }); This code relies on two tricks:
Reversing the FiltersFor completeness, we need a way to get back to the full list after we have filtered it. Adding an option for all topics is pretty straightforward: $('<div class="filter">all</div>').click(function() { $table.find('tbody tr').show(); $(this).addClass('active').siblings().removeClass('active'); }).addClass('clickable active').appendTo($filters); This
gives us an all button that simply shows
all rows of the table again. For good measure we mark it as active to begin
with. Interacting with Other CodeWe
learned with our sorting and paging code that we can't treat the various
features we write as islands. The behaviors we build can interact in sometimes
surprising ways; for this reason, it is worth revisiting our earlier efforts to
examine how they coexist with the new filtering capabilities we have added. Row StripingThe advanced row striping we put in place earlier is confused by our new filters. Since the tables are not re-striped after a filter is performed, rows retain their coloring as if the filtered rows were still present. To account for the filtered rows, the striping code needs to be able to find them. We can add a class on the rows when they are filtered: $(document).ready(function() { $('table.filterable').each(function() { var $table = $(this);
$table.find('th').each(function (column) { if ($(this).is('.filter-column')) { var
$filters = $('<div class="filters"><h3>Filter by ' + var keywords = {};
$table.find('tbody
tr td').filter(':nth-child(' + (column + keywords[$(this).text()] = $(this).text(); });
$('<div class="filter">all</div>').click(function() { $table.find('tbody tr').show().removeClass('filtered'); $(this).addClass('active').siblings().removeClass('active'); $table.trigger('stripe'); }).addClass('clickable active').appendTo($filters);
$.each(keywords, function (index, keyword) {
$('<div class="filter"></div>').text(keyword).bind('click', $table.find('tbody tr').each(function() {
if ($('td', this).filter(':nth-child(' + (column + 1) + $(this).show().removeClass('filtered'); } else if ($('th',this).length == 0) { $(this).hide().addClass('filtered'); } });
$(this).addClass('active').siblings().removeClass('active'); $table.trigger('stripe'); }).addClass('clickable').appendTo($filters);
}); $filters.insertBefore($table); } }); }); });
$(document).ready(function() { $('table.striped').each(function() { $(this).bind('stripe', function() { var rowIndex = 0; $('tbody tr:not(.filtered)', this).each(function(index) { &nbs |