Using JQuery to create a table with folding details rows

I improved an implementation of a table in which each row is clickable to toggle the visibility of a detail row below it. The previous implementation gave each row a numbered id, and applied a separate onClick function to each row. I realised a more efficient approach would be to apply appropriate classes to the table, and use a function that runs across the table and applies the appropriate event handlers.

The table is structured approximately like this (I’ve left out a header row and other complications).

<table id="summarydetailtable">
    <tr class="summary">
        <td>Value1</td>
        <td>Value2</td>
    </tr>
    <tr class="hide">
        <td colspan="2">Additional details</td>
    </tr>
    ... more rows here ...
</table>

Each summary row of the table has the class summary. The class hide is used to hide the details row – all the details rows start with this class set. When a summary row is clicked the first time, the class selected is applied to that row, and the class hide removed from its detail row (the row below it). When the row is clicked again, the class selected is removed from that row, and the class hide applied to the detail row.

I realised that the jQuery selector next() would make my job easy:

var select = function(control) {
    $(control)
        .addClass("selected")
        .next()
        .removeClass("hide");
}
var unselect = function(control) {
    $(control)
        .removeClass("selected")
        .next()
        .addClass("hide");
}

So to select a summary row, we apply the style to that row, move to the next control which is the next row, and show it.

To actually wire this up looks like this:

namespace.registerFoldoutList = function(tableId) {
    var tableSelector = "#" + tableId;
    var rowSelector = tableSelector + " tr.summary";

    var select = function(control) {
        $(control)
            .addClass("selected")
            .next()
            .removeClass("hide");
    }
    var unselect = function(control) {
        $(control)
            .removeClass("selected")
            .next()
            .addClass("hide");
    }

    $(document).ready(function() {
        $(rowSelector).click(function(e) {
            var control = $(this);
            if (control.hasClass("selected")) {
                unselect(control);
            } else {
                select(control);
            }
        });
    });
}

I’ve seen people saying things like “why use jQuery, when you can just use javascript”, and as javascript support gets more mature, this is a fair point. For me, the simplicity and ease of jQuery style coding appeals – I especially enjoy the way function calls are chained.

The next challenge was a change request to add an Expand All button to the header above the table. The markup is approximately like

<div class="heading_box">
    <span class="results">
        <a class="open expand_all" href="#">Expand all</a>
    </span>
    <h2>Table heading</h2>
</div>
<table id="summarydetailtable">
    <tr class="summary">
        <td>Value1</td>
        <td>Value2</td>
    </tr>
    <tr class="hide">
        <td colspan="2">Additional details</td>
    </tr>
    ... more rows here ...
</table>

The class of the Expand all link should toggle between open and close, and all the detail rows should be hidden or shown as appropriate.

Extending my original function to deal with Expand All proved pretty simple:

namespace.registerFoldoutList = function(tableId, registerExpandAll) {
    var tableSelector = "#" + tableId;
    var headingSelector = ".heading_box";
    var expandAllSelector = ".expand_all";
    var rowSelector = tableSelector + " tr.summary";

    var select = function(control) {
        $(control)
            .addClass("selected")
            .next()
            .removeClass("hide");
    }
    var unselect = function(control) {
        $(control)
            .removeClass("selected")
            .next()
            .addClass("hide");
    }

    $(document).ready(function() {
        if (registerExpandAll) {
            var expandAll = $(tableSelector)
                .prevAll(headingSelector).find(expandAllSelector);

            if (expandAll.length > 0) {
                $(expandAll).click(function() {
                    var control = $(this);
                    if (control.hasClass("open")) {
                        control
                            .removeClass("open")
                            .addClass("close")
                            .text("Collapse all");

                        $(rowSelector)
                            .each(function() { select(this); });
                    } else {
                        control
                            .removeClass("close")
                            .addClass("open")
                            .text("Expand all");

                        $(rowSelector)
                            .each(function() { unselect(this); });
                    }
                });
            }
        }

        $(rowSelector).click(function(e) {
            if ($.inArray(e.target.tagName, noFoldTags) < 0) {
                var control = $(this);
                if (control.hasClass("selected")) {
                    unselect(control);
                } else {
                    select(control);
                }
            }
        });
    });
}

In the selector to find the Expand All control, $(tableSelector).prevAll(headingSelector).find(expandAllSelector), I use prevAll because the header is not always the element immediately before the table. It’s pretty easy to see what’s going on. If an Expand All element is found, its click event is wired up.

On a click, the event handler works out if the Expand All control is currently open, updates its CSS class and text accordingly, and then selects or unselects all the summary rows as appropriate. The only bit of this code I don’t really like is having the text “Collapse all” and “Expand all” hardwired into the javascript. If I did this now, I’d consider attaching the text using HTML 5 data attributes, or find some similar scheme to include the text in the HTML of the table and header.

So the next time you’re looking at adding some javascript to a certain type of element on a page, rather than individually wiring up the elements, consider if you can apply an approach like this, where you add behaviour to elements that match a certain CSS selector. It’s much easier to understand, and makes for a better experience for your website users. Rather than lots of javascript being generated everytime your page is loaded, you can include the javascript in your common javascript file. The common javascript file will be cached, and only a small piece of javascript to run your function will be needed for each page. And rather than having javascript scattered throughout the pages of your site, it lives in a centralised location.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s