jQuery Table Manipulation: Part 2

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

Read Part One of jQuery Table Manipulation here.

Advanced Row Striping

Row 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:

Learning jQuery 1.3

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:

Learning jQuery 1.3

Three-color Alternating Pattern

There 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:

Learning jQuery 1.3

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].

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:

Alternating Triplets

Suppose 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:

Learning jQuery 1.3

Perhaps surprisingly, the subheading rows have retained their proper formatting. But let's not be fooled by appearances. The 2007 subheading row is now set in the HTML as <tr class="odd"> and the 2006 row has <tr class="even">. In the stylesheet, however, the greater specificity of the element's rule outweighs that of the two classes:

#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:

Learning jQuery 1.3

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 Highlighting

Another 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 + ')')
.each(function(index) {
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:

Learning jQuery 1.3

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.

Tooltips

Although 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
written by ' + $thisAuthor + '</div>')
.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
written by ' + $thisAuthor + '</div>')
.appendTo('body');
positionTooltip(event);
};
Learning jQuery 1.3 Better Interaction Design and Web Development with Simple JavaScript Techniques
Published: February 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

The tooltip still won't be positioned correctly, though, unless we free it from its default postion:static property. We can do that in the stylesheet:

.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
                     by ' + $thisAuthor + '</div>').appendTo('body');

    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 + ')'
                                             ).each(function(index) {

      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:

Learning jQuery 1.3

 

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:

Learning jQuery 1.3

 

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 '
                 + highlighted + 'highlight all articles written by '
                          + $thisAuthor + '</div>').appendTo('body');

    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.

Learning jQuery 1.3

 

Now the tooltip offers a more intelligent suggestion when the hovered row is already highlighted.

Collapsing and Expanding

When 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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="' + toggleMinus + '"
                                    alt="collapse this section" />');

});

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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="' + toggleMinus + '"
                                    alt="collapse this section" />');

  $('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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="' + toggleMinus + '"
                                    alt="collapse this section" />');

  $('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:

Learning jQuery 1.3

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.

Filtering

Earlier 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 '
                                {literal}  + $(this).text() + ':</h3></div>');

        $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:

 

Learning jQuery 1.3

Filter Options

Now 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',
                              {'keyword': keyword}, function(event) {

    $table.find('tbody tr').each(function() {

      if ($('td', this).filter(':nth-child(' + (column + 1) +
                              ')').text() == event.data['keyword']) {

        $(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:

 

Learning jQuery 1.3

Collecting Filter Options from Content

Now 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) +
                                               ')').each(function() {

  keywords[$(this).text()] = $(this).text();

});

This code relies on two tricks:

  1. By using a map rather than an array to hold the keywords as they are found, we eliminate duplicates automatically.

  2. jQuery's $.each() function lets us operate on arrays and maps identically, so no later code has to change. Now we have a full complement of filter options:

 

Learning jQuery 1.3

Reversing the Filters

For 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 Code

We 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 Striping

The 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 ' +
                                     $(this).text() + ':</h3></div>');

        var keywords = {};

 

        $table.find('tbody tr td').filter(':nth-child(' + (column +
                                          1) + ')').each(function() {

          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',
                              {'keyword': keyword}, function(event) {

            $table.find('tbody tr').each(function() {

              if ($('td', this).filter(':nth-child(' + (column + 1) +
                               ')').text() == event.data['keyword']) {

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

      }

    });

  });

});


Whenever the current filter changes, we trigger the
stripe event. This uses the same trick we implemented when making our pager aware of sorting—adding a new custom event. We have to rewrite the striping code to define this event:

$(document).ready(function() {

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

    $(this).bind('stripe', function() {

      var rowIndex = 0;

      $('tbody tr:not(.filtered)', this).each(function(index) {

        if ($('th',this).length) {

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

          rowIndex = -1;

        } else {

          if (rowIndex % 6 < 3) {

            $(this).removeClass('odd').addClass('even');

          }

          else {

            $(this).removeClass('even').addClass('odd');

          }

        };

        rowIndex++;

      });

    });

    $(this).trigger('stripe');

  });

});

The selector to find table rows now skips filtered rows. We also must remove obsolete classes from rows, as this code may now be executed multiple times. With both the new event handler and its triggers in place, the filtering operation respects row striping:

 

Learning jQuery 1.3

Expanding and Collapsing

The expanding and collapsing behavior added earlier also conflicts with our filters. If a section is collapsed and a new filter is chosen, then the matching items are displayed, even if in the collapsed section. Conversely, if the table is filtered and a section is expanded, then all items in the expanded section are displayed regardless of whether they match the filter.

Since we have added the filtered class to all rows when they are removed by a filter button, we can check for this class inside our collapser's click handler:

var toggleSrc = $(this).attr('src');

if ( toggleSrc == toggleMinus ) {

  $(this).attr('src', togglePlus)

  .parents('tr').siblings().addClass('collapsed').fadeOut('fast');

} else{

  $(this).attr('src', toggleMinus)

  .parents('tr').siblings().removeClass('collapsed')
                                    .not('.filtered').fadeIn('fast');

};

While we are collapsing or expanding rows, we add or remove another new class on the rows. We need this class to solve the other half of the problem. The filtering code can use the class to ensure that a row should be shown when the filter changes:

$table.find('tbody tr').each(function() {

  if ($('td', this).filter(':nth-child(' + (column + 1) + ')').text()
                                              == e.data['keyword']) {

    $(this).removeClass('filtered').not('.collapsed').show();

  }

  else if ($('th',this).length == 0) {

    $(this).addClass('filtered').hide();

  }

});


Now our features play nicely, each able to hide and show the rows independently.

The Finished Code

Our second example page has demonstrated table row striping, highlighting, tooltips, collapsing/expanding, and filtering. Taken together, the JavaScript code for this page is:

$(document).ready(function() {

  var highlighted = "";

  var column = 3;

 

  var positionTooltip = function(event) {

    var tPosX = event.pageX;

    var tPosY = event.pageY + 20;

    $('div.tooltip').css({top: tPosY, left: tPosX});

  };

  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 ' + highlighted +
                    'highlight all articles written by ' +
                            $thisAuthor + '</div>').appendTo('body');

    positionTooltip(event);

  };

  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 + ')' )

     .each(function(index) {

      if (thisClicked == $(this).text()) {

        $(this).parent().toggleClass('highlight');

      } else {

        $(this).parent().removeClass('highlight');

      };  

    })

    showTooltip.call(this, event);

  })

  .hover(showTooltip, hideTooltip)

  .mousemove(positionTooltip);

});

 

$(document).ready(function() {

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

    $(this).bind('stripe', function() {

      var rowIndex = 0;

      $('tbody tr:not(.filtered)', this).each(function(index) {

        if ($('th',this).length) {

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

          rowIndex = -1;

        } else {

          if (rowIndex % 6 < 3) {

            $(this).removeClass('odd').addClass('even');

          }

          else {

            $(this).removeClass('even').addClass('odd');

          }

        }

        rowIndex++;

      });

    });

    $(this).trigger('stripe');

  });

})

 

$(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 ' +
                                    $(this).text() + ':</h3></div>');

        var keywords = {};

 

        $table.find('tbody tr td').filter(':nth-child(' + (column +
                                          1) + ')').each(function() {

          keywords[$(this).text()] = $(this).text();

        })

 

        $('<div class="filter">all</div>').click(function() {

          $table.find('tbody tr').removeClass('filtered')
                                           .not('.collapsed').show();

          $(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',
                              {'keyword': keyword}, function(event) {

            $table.find('tbody tr').each(function() {

              if ($('td', this).filter(':nth-child(' + (column + 1)
                            + ')').text() == event.data['keyword']) {

                $(this).removeClass('filtered').not('.collapsed')
                                                             .show();

              }

              else if ($('th',this).length == 0) {

                $(this).addClass('filtered').hide();

              }

            });

 

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

            $table.trigger('stripe');

          }).addClass('clickable').appendTo($filters);

         

        });

        $filters.insertBefore($table);

      }

    });

  });

});

 

$(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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="' + toggleMinus + '" alt="collapse
                                                  this section" />');

 

  $('img', $subHead).addClass('clickable') 

  .click(function() {

    var toggleSrc = $(this).attr('src');

    if ( toggleSrc == toggleMinus ) {

      $(this).attr('src', togglePlus)

      .parents('tr').siblings().addClass('collapsed').fadeOut('fast');

    } else {

      $(this).attr('src', toggleMinus)

      .parents('tr').siblings().removeClass('collapsed')
                             .not('.filtered').show().fadeIn('fast');

    };

  });

})

Summary

In this article, we have explored some of the ways to slice and dice the tables on our sites, reconfiguring them into beautiful and functional containers for our data. We have covered sorting data in tables, using different kinds of data (words, numbers, dates) as sort keys along with paginating tables into easily-viewed chunks. We have learned sophisticated row striping techniques and JavaScript-powered tooltips. We have also walked through expanding and collapsing as well as filtering and highlighting of rows that match the given criteria.

We've even touched briefly on some quite advanced topics, such as sorting and paging with server-side code and AJAX techniques, dynamically calculating page coordinates for elements, and writing a jQuery plug-in.

As we have seen, properly semantic HTML tables wrap a great deal of subtlety and complexity in a small package. Fortunately, jQuery can help us easily tame these creatures, allowing the full power of tabular data to come to the surface.

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

jQuery UI 1.7: The User Interface Library for jQuery
jQuery UI 1.7: The User Interface Library for jQuery

jQuery 1.3 with PHP
jQuery 1.3 with PHP

AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

Apache MyFaces 1.2 Web Application Development
Apache MyFaces 1.2 Web Application Development

Ext JS 3.0 Cookbook
Ext JS 3.0 Cookbook

Joomla! with Flash
Joomla! with Flash

Spring Persistence with Hibernate
Spring Persistence with Hibernate

Blender 3D 2.49 Incredible Machines
Blender 3D 2.49 Incredible Machines

Your rating: None Average: 4.9 (7 votes)
nice job Jon by
Nice job, what a massive writeup! Geez.. I found a somewhat similar and shorter row manipulation script here http://www.dougnorfolk.com.au/website-design-forster-notes/jquery-add-table-row-with-ajax/
CSS by
Please also add your css classes in order to run this example properly.

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
a
9
B
a
j
v
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