Book Excerpt – Table Manipulation

read 11 comments

Packt Publishing has just posted an excerpt of our new book Learning jQuery: Better Interaction Design and Web Development with Simple JavaScript Techniques on their site. The excerpt covers table sorting, pagination, row highlighting/striping, and basic tooltips. It also gives a hat tip to Roger Johansson of 456 Berea Street and Christian Bach of jQuery Table Sorter fame (by the way, Christian has just released version 2.0 of the plugin, and it’s awesome!). Here is the summary:

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.

In this article, we will use jQuery to apply techniques for increasing the readability, usability, and visual appeal of tables, though we are not dealing with tables used for layout and design. In fact, as the web standards movement has become more pervasive in the last few years, table-based layout has increasingly been abandoned in favor of CSS‑based designs. Although tables were often employed as a somewhat necessary stopgap measure in the 1990s to create multi-column and other complex layouts, they were never intended to be used in that way, whereas CSS is a technology expressly created for presentation.

But this is not the place for an extended discussion on the proper role of tables. Suffice it to say that in this article we will explore ways to display and interact with tables used as semantically marked up containers of tabular data. For a closer look at applying semantic, accessible HTML to tables, a good place to start is Roger Johansson’s blog entry, Bring on the Tables at www.456bereastreet.com/archive/200410/bring_on_the_tables/.

Some of the techniques we apply to tables in this article can be found in plug‑ins such as Christian Bach’s Table Sorter. For more information, visit the jQuery Plug‑in Repository at jQuery.com/plugins.

You can read the full article/excerpt or check out the table-manipulation demos here and here.

comment feed

11 comments

  1. ScottDavis

    Any chance this doesn’t run “out of the box” against jQuery 1.2.1? I’m getting error “$table.find(“tbody tr”).show().lt is not a function” on line 85 of table.js, but not if I run against your copy of jquery.js. Perhaps I haven’t included enough code…

  2. Hi Scott,
    It’s possible that you haven’t included enough code. Whenever we store a jQuery object as a variable, we precede the variable with “$”. It could be that you don’t have the $table variable defined anywhere. Without looking back at the code, I think we had something like var $table = $(this);.

    The only reason it might fail in 1.2.1 as opposed to an earlier version is if we used an XPath selector there, but that doesn’t seem to be the case.

    Hope that helps.

  3. Joe

    Interesting – so far I’m doing well with the book, but got my first real stumper on the next line of the same script:


    if ($(this).is.('.sortAlpha')) {

    where I get a “XML filtering predicate operator called on incompatible Function” error in Firebug.

    is .is xpath?

  4. Hi Joe,

    The .is() method is not xpath. Is the period after “is” in your code above a typo? It shouldn’t be there. We have a working demo that you might want to look at for comparison.

  5. teresa hurley

    Hi Guys,

    Thanks for a great book to help me learn jQuery! I’ve been out of the js loop for a few years, doing more administrative type stuff at my job… but lately I’ve been asked to get back into helping with actual coding. So I have a lot of catching up to do, and this is a great help.

    I’m working through the book, and happen to be on this chapter. I’d like to submit a jQuery 1.2.1 problem and its fix as well as a Safari problem I just can’t seem to get around.

    First, the jQuery 1.2.1 problem/fix:

    In the pagination code in tables.js, I was getting an error that $table.find('tbody tr').show().lt() is not a function. After a bunch of searching, I found that jQuery 1.2 doesn’t support .lt() or .gt() anymore. We now must use .slice(0, n) in place of .lt() and .slice(n+1) in place of .gt(), thus:
    $(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 was removed from jquery, use slice instead
    //.lt(currentPage * numPerPage)
    //new syntax:
    .slice(0, currentPage * numPerPage)
    .hide()
    .end()
    //gt was removed from jquery, use slice instead
    //.gt((currentPage + 1) * numPerPage - 1)
    //new syntax:
    .slice((currentPage + 1) * numPerPage - 1)
    .hide()
    .end();
    });
    //...code continues

    Hopefully, that’ll help anyone else new and struggling as I am.

    Now for my Safari vs. collapsing table rows problem.

    I’m using Safari 2.0.4. It appears that “opacity” is not a property of <tr> (or thead, tbody, tfoot, for that matter). So the collapse code just makes the rows disappear, but not fade out prettily. Then, when I un-collapse, the spacing is all messed up: too much space between table rows, table row height shrinking, etc. Using hide/show instead of fadeOut/fadeIn works as expected (w/o the fade effect, obviously, but also with no funkiness w/regard to spacing).

    The most recent thing I tried was to fade out the table cells instead of the rows. The first fade out appears to work fine. Fade in, however, does not: it seems to actually add table cells to each row (and they don’t fade, in, they just appear). Then, subsequent fade outs are flaky: the rows are not completely absent. This also breaks firefox on the fade out, in that it leaves a space between the <th> row above and below the collapsed rows. Example here: http://hurleyhouse.com/learning_jquery/chapter_7/news/

    I’ve searched and searched, and haven’t been able to find anyone else having the problem… Am I alone? Does anyone know of a workaround? Or is it possible I’ve just done something completely wrong here?
    $(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+'" 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')
    // .hide(); //works fine in safari

    .children() //need this for next to work in safari, because safari doesn't allow tr opacity
    /*
    problem is, it leaves a big space in firefox (mac, anyway) also fadein is broken
    see fadein note below
    */
    .fadeOut('slow');
    }
    else
    {
    $(this)
    .attr('src', toggleMinus)
    .parents('tr')
    .siblings()
    .not('.filtered')
    .removeClass('collapsed')
    // .show(); //works fine in safari, when hide is used above

    /*
    the following totally breaks in safari... each time it's called, it adds more table cells
    first, it's one td for each existing td, then subsequent, it's one less than the original
    number of tds... so you have four at first, then eight, then eleven, then fourteen, etc.
    */
    .children()
    .fadeIn('slow');
    }
    });
    });

    thanks so much!
    tree

  6. Corrado Fiore

    Hi,

    justed want to say “Thanks!” for such an invaluable book. I’m one of the many developers to whom Jquery (and your book) opened the doors of rich client scripting.

    That said, I have a little question about table filtering. I was able to set up filtering for my table using some <select>, using the following code:

    $(document).ready(function() {
      $('table.filterable').each(function() {
        var $table = $(this);
        $table.find('th').each(function (column) {
          if ($(this).is('.filter-column')) {
            var $filters = $('<select class="filters"> ' + $(this).text() + ' </select>');
            var keywords = {};
            $table.find('tbody tr td').filter(':nth-child(' + (column + 1) + ')').each(function() {
              keywords[$(this).text()] = $(this).text();
            })
            $('<option class="filter">All</option>').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) {
              $('<option class="filter"></option>').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);
          }
        });
      });
      	// This ensures that the first option of each select remains visible
      	$("select.filters").each(function() {
      		$(":first-child").attr({ selected: "selected" })
    	});
    });

    The above code will generate a <select> for each column marked as filterable. But, as soon as another select gets clicked, the previous filtering will be “forgotten” by the script. In other words, only the last clicked select is considered.

    Now, I wish to push filtering a step further, i.e. combining multiple filters. The user should be able to filter rows using one select and then refine his/her search using other ones.

    Any advice will be appreciated.

    Thanks,
    Corrado Fiore

  7. Hi Corrado,

    I’m so glad you like the book! To keep filtering, try replacing these lines …

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

    with these …

                  if ($('th',this).length == 0) {
                    $(this).addClass('filtered').hide();
                  }
  8. Matthew

    Hey guys,

    I am trying to do what Corrado Fiore is doing above, but I would love to see an example with dropdowns instead of div tags to filter.

    Ive copied Corrado’s code and used the example table, but it doesnt filter or do anything for me.

    Thank you,

    Matthew

  9. Sunit Joshi

    I’m trying to get the files for Ch-7 (especially the .css files) where the Table is used with JQuery. It’s not there in the downloadable source code. Any ideas ??

    thanks
    Sunit

  10. Eddie Foreman

    Hi Guys

    I’ve had the book for a while now and have always found it to be a valuable resource. I’m currently working on a project which needs pagination, and have used your example in chapter 7. Works great, except for when the pages first loads. All tables rows are displayed, and are only updated once the pager links are used.

    
    $(document).ready(function() {
      $('table').each(function() {
      var currentPage = 0;
      var numPerPage = 5;
      var $table = $(this);
    //Quick fix to show correct number of rows  when page first loads
      $table.find('tbody tr').hide()
      .slice(currentPage * numPerPage,(currentPage + 1) * numPerPage).show();
      $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 = $('<ul class="pagination"></ul>');
      for (var page = 0; page < numPages; page++) {
        $('<li></li>').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.insertAfter($table)
        .find('li:first').addClass('active');
      });
    });
    

    Has anyone else had this problem ? As a quick fix, I’ve duplicated a couple of lines to show the correct number of rows on page load. Was wondering if this is how the book example is suppose to work?

    Was also wondering how easy it would be to update the paging so that it also had previous and next buttons. Any help or pointers on this would be greatly appreciated.

    Thanks in advance

    Eddie

Sorry, but comments for this entry are now closed.