Finished jQuery Tablesorter mod for Collapsible Table Rows
I've been pretty quite the past two weeks. I've been really plugging away doing a lot of client-side UI programming in jQuery. I'm working on a complete revamp of some portions of our application and than means modernizing the UI.
One of the tasks I needed to accomplish was to create a UI component that would allow for sortable, collapsible table rows. jQuery already has a very good (an official) plug-in for sorting column rows called tablesorter.js. The Tablesorter is part of the jQuery UI project and allows for sorting of multiple columns—which is a really nice feature.
However, the current tablesorter.js codebase does not have anything in place to allow for "children" rows—which is needed to provide a collapsible architecture. Essentially I need a way to tell certain rows that they belong with the row above.
After playing around with some different variations I finally came up with a solution that worked elegantly and had minimal impact on the Tablesorter codebase. The way I solved the problem is to add a class to "children" rows, which tells the Tablesorter plug-in to include the row as part of the last row that does not have the "child" class. This allows me to actually have multiple children rows, that are all grouped together and ignored by the sorting algorithms.
I'm working with Christian Bach to get the mods added to the official codebase—and it looks like that might happen as early as next week. I've upload my Tablesorter mod and an example of implementing collapsible rows.
The example uses the "Pager" add-on for Tablesorter, just to show my mod attempted not to break backwards compatibility.
If you have any comments, please leave them. They will only help me and Christian in the long run.
Comments
The majority of overhead is actually in initializing the core table sorting code. My mod to support child rows does not seem to have a huge impact on performance (but I was testing only on a couple hundred rows of data.)
DOM parsing is always going to be problematic when you get in to tens of thousands of elements. I'm not sure what you consider "lots", but if you're getting into 100's of lines of tabular data, you probably want to start thinking about implementing that data in a progressive manor. I mean even just having a browser try to render a table with hundreds or thousands of rows is time consuming--parsing that DOM tree is obviously only going to add more overhead.
So, if you're talking about a reasonable dataset then performance is fine. If you're wanting to load thousands of rows of data, then you really don't want to preload all that data anyway (because that's a ton of data to push to the user.) You really want a solution that will load the data progressively (i.e. via AJAX.) That's going to be more efficient all the way around.
Thanks for sharing your code!
One question:
It seems that your tablesorter-script does not accept header-inline (meta) information.
Meaning: <.... class="{sorter:false}" ....
is not working.
Using the header-Option in the ready-container is in my case not an option.
You have have a solution for this, please let us know :-)
Thanks again!
Cheers, QT
The metadata works fine, I'm using it on the project I originally wrote the mod for. Are you sure you've included the metadata plugin?
Thanks for getting back so quick.
Yes, a metadata-plugin was included (emphasis on *a*).
I noticed that I was using one from Joern ( * Revision: $Id: metadata.js 3465 2007-09-23 21:15:52Z joern.zaefferer $) and not the one from the jquery-repository.
Now it workes like a charm!
Thanks again.
Cheers, QT
two quick questions.
1) how does one not use the +-... Say I want a link to expand my rows...
2) the 'js file makes reference to FX - and I've tried to put show("slow") in the context of the call, but it bombs...
.collapsible( "td.collapsible", { collapse:true },fx: {hide("slow"),show("slow")} )
thanks again.
If you notice, the FX don't work correctly in IE6 (which is why they're just "show" and "hide".) Also, the "fx" argument just takes a string (like "show" or "slideDown") you currently can't specify a speed. However, since IE6 doesn't like to animate table row elements (TR) I never really went forward with completely adding the FX.
As far as using a link, the way the code will work is it'll attach the "expand/collapse" function to the first "link" it finds in the collapsible cell. If there is no link, it creates the a link for you. So, just add a link with whatever verbiage you want to the collapsible cell and the behavior will automatically be attached.
Thanks a million for sharing your update and for closing the deal on completely converting someone over to jquery. I just starting using it (I used the tab UI) and was upgrading my db display to include dependant rows, include sortability and pagination - and what do you know? Found it here with your upgrade and jquery table sort! Koodos - works like a charm, is fast and lightweight!
New fan!
Thanks!
Yeah, you'd need to write a widget. I'm pretty sure I've seen a filtering widget someone wrote for the tablesorter plugin. I'd suggest checking out the jQuery mailing lists.
Thanks
You can do this to toggle the current state for all rows:
$("td.collapsible a").click();
If you want to collapse all do:
$("td.collapsible a").addClass("expanded").click();
If you want to expand all rows do:
$("td.collapsible a").removeClass("expanded").click();
For others interested this is what I have used:
// Expand/Collapse all
$('a#expandCollapse').click(function(){
var linkLabel = this.innerHTML;
switch(linkLabel)
{
case "Expand All":
$("td.collapsible a").removeClass("expanded").click();
this.innerHTML= "Collapse All"
break;
case "Collapse All":
$("td.collapsible a").addClass("expanded").click();
this.innerHTML= "Expand All"
break;
}
return false;
});
Has anyone had any success adding filtering? I have not been able to find a widget to do so, but I do know that filtering was being worked on for the next release of table sorter. Here's the link to his demo: http://motherrussia.polyester.se/pub/jquery/demos/...
I'm not sure where Christian's at as far as filtering goes. It's definitely a feature that's needed to complete with other table sorting modules.
If I use it on it's own works great, if I use it in a cflayout it gives an error (cflayout: Error processing JavaScript in markup for element cfdiv...) and the table sort is all broken.
Thank you for your work,
Mike
I've never used cflayout, so I can't say. I prefer implicitly controlling the behavior of my applications and there's just to much JS overhead in the current implementation of some of the CF DHTML tags (at the moment) for my liking.
From the error, it sounds like it's a problem with cflayout processing the script tags inside your content. You might try adding a the defer="true" to your script tags that you're loading inside cflayout:
<script type="text/javascript" defer="true">
// code here
</script>
You'd need to modify the $a.bind("click", function (){}) function to trigger the "click" handler for every anchor that's not the current anchor. You'd need something like:
$("td.collapsible a").filter(function (){return this !== $a[0]}).addClass("expanded").click();
This should find all the anchors, but the currently clicked item and toggle it's status to collapsed. (I did not actually test this code. If doesn't work exactly, it should be pretty close.)
Note that this method could be really slow on large tables though.
Unfortunately it didn't fix it :)
I agree with you that ajax implementation in cf8 is on the bloated side.
I guess I will be looking for other js libraries that can offer similar functionality with cflayout.
Thanks again
Mike
My guess is if you load the JS libraries in the main container (and not within the template you call with <cflayout>) you can get things to work.
I suspect there definitely is a way to get things working and you may run into similar problems with other libraries as well.
It's a cflayout problem I know. Will dig a little bit more but at this point there is not a lot I can do.
Thanks again and keep up the good work
Mike
You'll need to trigger code when the cflayout window is opened that will actually attach the behavior to the table. There should be some JS variables that are exposed which allow you to run JS code when the layout window gets opened.
The plugin is excellent. Really well put together. Have you considered adding row highlighting to the mod or heard of anyone successfully adding it?
Thanks,
Ryan
Thanks! At this time, I don't require the functionality in the project I'm working on, so I have no plans to add it. You could definitely write something to do row highlighting. I know there are several jQuery plug-ins out there already that do row/column highlighting. You'd probably need to modify those plug-ins to support multiple rows being seen as a single row, but they'd give you a good head start.
http://www.justanaverageyo.com/index.cfm/2008/1/17...
When one uses the append function for Ajax one can pass a function "()" instead of a URL "string". This JS function would return a dynamic URL.
Each time you will need to clear data set and perform a reset with the new ajax return string.
$("#ajax-append").click(function() { $.get("assets/ajax-content.html", function(html) { // append the "ajax'd" data to the table body $("table tbody").append(html); // let the plugin know that we made a update $("table").trigger("update");
Mod
$("#ajax-append").click(function() { $.get(GetHTML(), function(html) { // append the "ajax'd" data to the table body $("table tbody").append(html); // let the plugin know that we made a update $("table").trigger("update");
jQuery(".tablesorter:has(tbody tr)")
/*
* td.collapsible = collapse to the first table row and show +/-
* td.collapsible_alt = anchor to order number
*/
.collapsible("td.collapsible", {
collapse: true
}).trigger("update");
// How to make row highlighting and toggle one row expand/collapse
// Make sure to give your first tr row a class='first_row_class'
// row hilite
$("tr.first_row_class").hover(
function () {
$(this).children('td').attr("style","background-color:#e6edc1"); // row hilite colour
},
function () {
$(this).children('td').removeAttr("style");
}
);
// row toggle click
$("tr. first_row_class").click(function(){
$(this).find('td.collapsible a').click();
//return false;
});
// PS. Great plug-in!
$('#table-products1').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
$('#table-products2').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
$('#table-products3').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
$('#table-products4').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
$('#table-products5').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
$('#table-products6').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
You can combine selectors by using a comma to separator each unique selector:
$('#table-products1,#table-products2,#table-products3,#table-products4,#table-products5,#table-products6').columnManager({listTargetID:'targetcol1', hideInList: [1,2,3,4], colsHidden: [5]}); var opt = {hide: function(c){ $(c).fadeOut(200); }, show: function(c){ $(c).fadeIn(500); }};
However, as a CF programmer I am quite new to Jquery. I got Tablesorter to work with CF pulling data from mySQL but I can't seem to get the Expand/Collapse ( Kab ) and the Row Highlight ( David ) to work. It's the HTML call to the functions I suspect. I would really appeciate any help in getting these working.
Nice one Dan. Great peice of coding.
I started to play with your mod to the tablesorter. It is really great.
Does your mod work with having a child sortable table? I had a problem with it trying to sort the parent table.
In the end I am hoping to have additional child tables that can have child tables themselves with the ability to be sortable, etc.
Example of what I'm trying to accomplish is similar to this:
http://demos.telerik.com/aspnet-ajax/Grid/Examples...
I think you may need change the tablesorter code further to make sure that only tr that are direct descendants of the table/tbody take are sorted (ie. "> tr".) I'm not sure how well the tablesorter code works with nested tables, because I've never tried it.
Thank you for the quick reply. I quickly looked into your jquery.tablesorter.mod.js and found the following code:
function buildHeaders(table) {
...
$tableHeaders = $("thead th",table);
...
}
I changed the selection to:
$tableHeaders = $("thead:first th",table);
Now my nested sortable tables sort only the rows within their own table.
With that I plan to play with this more, but so far this is great!!!
I noticed I had download jquery.tablesorter.collapsible.js and jquery.tablesorter.mod.js
You stated in this blog that you were working with the author to integrate your changes. Is that still in the works? This is great none the less.
I believe Christian implemented most of my changes in his source code, but I don't think he's ever released a new version of the code.
I looked at this a little more this weekend. I found Christian's svn version:2.0.5b. Which incorporated your efforts into his code. Just for others you still need to use Dan's jquery.tablesorter.collapsible.js. This time I didn't need to make any changes to the buildHeader function like I stated in a previous post. When clicking the header to sort it correctly only affected its current table.
I did have issues with providing the table with initial sort order information or using the tablesorter.trigger("sorton", [[[0,1],[1,1]]]);
It binds the event: .bind("sorton",function(e,list)
By default it propagates to matching parent elements, which was bubbling up to all the parent tables to re-do the initial sort order. I just added e.stopPropagation(); for the first line in the function to tell it not to bubble up. That fixed that.
I also made a selector change on his code for using the zebra widget:
$("#" + table.id + " tbody>tr:visible:not('.expand-child')").each(function(i) { ...
This fixed it from trying to apply zebra to any child tr rows. Depending on usage you might want, but my child table's themselves were having tablesorter applied.
It all appears to be working perfectly for me so I thought I would share if anyone else wants to venture into having detail tables as the child row.
I have a small query. I need a table, i have a column in that table containing a link. On clicking on that link the rows should expand between that row and the next row.
can anyone help?
This mod should handle that for you, but you'll need to refer to the source code to see how to do it.
I am still working on my table (since november). The goal is to present a table and display the last column only if the user is privileged to see it. If the user is credentialed then a input button will display which allows the user to toggles on and off the last column.
I have something which works on IE but not on FF.
Has anyone successfully done this? Dan - would you be able to look at the code if i send it? Desperate...
Thanks...
I don't have time to look at it, but in FF it should be as simple as hiding the last TD:
$("#table > tr > td:last").hide();
however, after putting it to use in a number of places, i have encountered a problem that i can't resolve. basically i have a table with a number of tabs above it (manifested by a <ul>). these tabs when clicked repopulate the table by pulling some data via AJAX. however, once a tab has been clicked the functionality of table sorter is lost - even if i go back to the "original" tab, i.e. the one that is selected on page load. the only way to get the functionality back is to refresh the page in the browser.
any idea what might be causing this, and any possible solutions? i've been recommended livequery but i couldn't get that to solve the problem. THANKS!
You need to rebind the events. See this page for an example of rebinding the tablesorter:
http://tablesorter.com/docs/example-ajax.html
Dan's mod should resemble the same functionality that you can achieve with the original tablesorter.
Look at: http://tablesorter.com/docs/example-ajax.html
You probably need to do the following:
$("table").trigger("update");
You need to look at how you are modifying your table on each tab click or you will have to re-initialize the tablesorter by calling the table.tablesorter(configSettings) on every tab click.
THANKS SO MUCH! not only for your help but a great modification to the tablesorter :-)
Assuming you're using a <tbody /> to store your data, this would expand the first row:
$("tbody tr:first td.collapsible a").click();
All you need to do is trigger the click handler for the <a /> element inside the td.collapsible cell inside the appropriate row.
I like your works a lot. I've used the collapsible and very happy with it.
I current have a issue with collapsed row on nested tables. It works fine with a first rows, but when I nested tables within rows, when I click on the (+) of a first row all rows inside expanded, I have to double click on the (+) to get it collapse back.
=== jquery===
<script type="text/javascript">
$(document).ready(
function (){
$(".tablesorter")
//td.collapsible = collapse to the first table row and show +/- td.collapsible_alt = anchor to order number
.collapsible("td.collapsible", { collapse: true })
// do sorting
.tablesorter({
// set default sort column
sortList: [[2,0]], headers: {0: {sorter: false}} //, widgets: ['zebra']
, onRenderHeader: function (){ this.wrapInner("<span></span>");}
, debug: false
})
}
);
</script>
=== html part ===
<table class=tablesorter><thead><tr><th>name</th><th>address</th></tr></thead><tbody>
<tr><td rowspan=2 class=collapsible></td><td>name_1</td><td>address_1</td><tr>
<tr class='expand-child'><td colspan=5>
<table ><thead><tr><th>class</th><th>description</th><th>teacher</th><tr></thead><tbody>
<tr><td rowspan=2 class=collapsible></td><td>class_1</td><td>description_1</td><td>teacher_1</td><tr>
<tr><td rowspan=2 class=collapsible></td><td>class_2</td><td>description_2</td><td>teacher_2</td><tr>
</tbody></table>
<//tbody></table>
The problem is when I click on the (+) next to name_1, I see both class_1 and class_2 rows and the (+) sign. when I double click on (+), it is collapsed. Do you have any idea?
Thanks,
I've never needed nested table support, so I don't know how far this will get you, but you will definitely need more specific selectors.
When you use the selector "td.collapsible", it's going to look inside the table for all matches--which will include the deeply nested matches of your inner tables.
Instead you'll need to use something that has more specificity like:
"> tbody > tr > td.collapsible"
This would make sure to only get the direct descendants you're after (i.e. the outer most "td.collapsible" elements in your table.
You may also need to modify the collapsible code to be use the direct descendant selector (ie. ">") so that it's not grabbing all td elements when trying to reveal the collapsed data.
Thank you very much for your comments. Here is what I have changed on
<script type="text/javascript">
$(document).ready(
function (){
$(".tablesorter")
//td.collapsible = collapse to the first table row and show +/- td.collapsible_alt = anchor to order number
.collapsible(">tbody >tr >td.collapsible", { collapse: true })
.collapsible(">tbody >tr >td >tbody >tr >td.collapsible", { collapse: true })
// do sorting
.tablesorter({
// set default sort column
sortList: [[2,0]], headers: {0: {sorter: false}} //, widgets: ['zebra']
, onRenderHeader: function (){ this.wrapInner("<span></span>");}
, debug: false
})
// do paging
.tablesorterPager({container: $("#pager"), positionFixed: false}) ;
}
);
</script>
.collapsible(">tbody >tr >td.collapsible", { collapse: true }) ==> is for outer most table
.collapsible(">tbody >tr >td >tbody >tr >td.collapsible", { collapse: true }) ==>inner table
The outer rows are working OK, the inner row are not collapsed.
As I said, you'll probably need to modify the source code of the collapsible plug-in as well. Look for the selectors used and make sure they're be very specific about the <tr> and <td> elements their selecting.
I know I've made comments in here of some things I did and I did have to alter the source code in certain places. I actually used Christian's tablesorter svn version:2.0.5b that incorporated a lot of Dan's work. You still requires jquery.tablesorter.collapsible.js as well.
Here is a snap shot showing collapsible tables that are sortable and themselves can nest other collapsible tables. http://picasaweb.google.com/lh/photo/S58apsVqEFfKQ... Look at my previous posts in here to see if it helps, but if I have time and when I get to my workstation I'll see if I can post some code for you.
The snap shot look great, it is what I would like to do. How did you change code on the 'jquery.tablesorter.collapsible.js'. Thanks for the comments.
I've tried to change something in the 'jquery.tablesorter.collapsible.js' file. It is not get better, it is not working at all. because I am new with jquery, maybe.
<table ...>
<tr><td class=collapsible></td> ... </tr>
<tr class='expand-child'><td>
<table ...>
<tr><td class=collapsible></td> ... </tr>
<tr class='expand-child'><td>
<table ...>
<tr><td class=collapsible></td> ... </tr>
<tr class='expand-child'><td>
.....
</td></tr>
</table>
</td></tr>
</table>
</td></tr>
</table>
if I have several nested rows/tables like this. How would I change on the plug-in source code? would you please give me a hint? Thanks.
Linh
Thanks,
Ajay
There are a couple of examples in the comments, like this one:
http://blog.pengoworks.com/index.cfm/2008/3/28/Fin...
Thanks for a quick reply. It works like a champ. However, it expand and collapse all rows in tables. I have two tables, the inner table inside <tr><td> of a outer one. how to collapse/expand all rows of a inner table while keep outer table stay still. is there an additional modification on the source code?
Again, thanks very much for sharing your work.
Ajay
Just make your selectors more specific like:
$("#tableId > tbody > tr > td.collapsible a").removeClass("expanded").click();
Which would only collapse at the first level of table cells. You're going to need to adapt the selector for you environment, but the basic idea is to make sure that you only grab the specific cells you want to expand/collapse.
Thanks so much. It works on the outer table cells (only one table). However, I like collaped/expanded all in the inner tables cells only. so I do like I've done on the outer table. It works only on the 'first/top' inner table cells. This is strange. Do you have any clue?
Without seeing an example, it's impossible for me to give you any clues, other than it's probably just a matter of getting your selectors right.
Thanks for your comments. All selectors look right, it just display incorrectly. I have the HTML and javascript @ http://sites.google.com/site/ajaynguyen/ . I don't have a public DNS, so you cannot see a real display. but I will post the snapshot later. Thanks for the help.
Ajay
Your HTML is malformed--so that can definitely affect things. The "id" attribute must be unique and all your inner tables have identical values for the "ids." jQuery will only ever grab the first one.
Also, your inner tables are missing the <tbody> tags.
There are other errors in the HTML as well. I'd recommend running your code through an HTML validator. Once you fix your HTML, you may find it resolves the issues you're having.
Thank you very much for your help. I validated the HTML, change table ids to unique ids, but it is not working properly. I post the html/js code and snapshots @ http://sites.google.com/site/ajaynguyen/ . All data are generate dynamically from DB. I think I almost get a result. your help would be very appreciated.
Thanks again.
Ajay
I got a problem getting tablesorter to work in my page.
I use jQuery 1.3.2 and try to add sort to two tables that are contained within a main table.
The selector for jQuery works and the debug info also seems correct but I do not get any result.
Built headers:,2ms
[]
column:0 parser:text column:1 parser:text column:2 parser:digit
Building cache for 3 rows:,0ms
Built headers:,0ms
[]
column:0 parser:text column:1 parser:text column:2 parser:digit
Building cache for 3 rows:,1ms
Pageurl: http://lab.dnet.nu/bugs/?view=todo.todo
All css and js files looks to be loading fine and I get no js errors.
Can any one see any obvious errors?
I'm not sure if I understand what you're saying, but if the children that you really want sorted (and is really where the tabular data is) then I'd only apply the tablesorter to the children.
If you start reading at Michael comment (link below) you'll see conversations about doing what you want. I've still never tried it, but Michael got it working for himself:
http://blog.pengoworks.com/index.cfm/2008/3/28/Fin...
There's also other dialog in there about getting child sortable tables working.
I am quite new to JS, and I have managed to get your mod working but I ran into a problem when appending data using the AJAX example on the tablesorter website. The tablesorter function still works after appending the data, however the new TD's with the "collapsible" class are not getting set as collapsible (no +/- button appended to the TD.
I am calling $(.tablesorter).trigger("update");, is there something else I need to do as well?
The tablesorter mod was not intended to work w/tables populated via AJAX. It only allows the behavior to be bound to a table once, so once you append data to the table via AJAX, there's not a good way to re-bind the behavior.
To accomplish what you want, you'd have to customize my mod further to allow re-binding of new data.
h[list[i][0]]
jquery.tablesorter.mod.js (line 411)
Also, I am not getting the +/- images like in your example. I don't think the JS is adding anchors to my <td class="collapsible"></td>.
TIA
h[list[i][0]] is not defined
jquery.tablesorter.mod.js (line 411)
This Mod is great, use it a lot, one question I had is, "Is it possible to have nested rows to be expandable? For example if I wanted a layout as:
[+] Row
[+] Row
How would I accomplish this? I seem to be able to only do one expandable row for each TR and thats it, is it possible to have more than one without nesting another table in a <TD> ?
[+] Row
.....[+] Row
It's great plug-in... thanks. Have one question: How to track the click event of the current [+] as expand (or) collapse?
Thanks,
Ranga
State is all stored in the className of the +/- anchors. You can determine the state by examining which class is assigned the anchor.

How's the performance with lots of rows of data? Have you seen a 'limit', before slowness occurs?