Today I got a call from a customer and we were looking over an older application that uses a lot of tables to display financial and other assorted data. The application is mostly meta-data driven with lots of layout formatting automatically driven through meta data rather than through explicit hand coded HTML layouts. One of the problems in this apps are tables that display a non-fixed amount of data. The users of this app don't want to use paging to see more data, but instead want to display overflow data using a scrollbar. Many of the forms are very densely populated, often with multiple data tables that display a few rows of data in the UI at the most. This sort of layout does not lend itself well to paging, but works much better with scrollable data.

Unfortunately scrollable tables are not easily created. HTML Tables are mangy beasts as anybody who's done any sort of Web development knows. Tables are finicky when it comes to styling and layout, and they have many funky quirks, especially when it comes to scrolling both of the table rows themselves or even the child columns. There's no built-in way to make tables scroll and to lock headers while you do, and while you can embed a table (or anything really) into a scrolling div with something like this:

<div style="position: relative; overflow: hidden; 
            overflow-y: scroll; height: 200px; width: 400px;">
    <table id="table"
            style="width: 100%"
            class="blackborder" >
        <thead>
            <tr class="gridheader">
                <th>Column 1</th>
                <th>Column 2</th>
                <th>Column 3</th>
                <th>Column 4</th>
            </tr>
        </thead>

        <tbody>
            <tr>
                <td>Column 1 Content</td>
                <td>Column 2 Content</td>
                <td>Column 3 Content</td>
                <td>Column 4 Content</td>
            </tr>
            <tr>
                <td>Column 1 Content</td>
                <td>Column 2 Content</td>
                <td>Column 3 Content</td>
                <td>Column 4 Content</td>
            </tr>
        </tbody>
    </table>
</div>

that won't give a very satisfying visual experience:

ScrollTable_NoGood[4]

Both the header and body scroll which looks odd. You lose context as soon as the header scrolls off the top and when you reach the bottom of the list the bottom outline of the table shows which also looks off. The the side bar shows all the way down the length of the table yet another visual miscue. In a pinch this will work, but it's ugly.

What's out there?

Before we go further here you should know that there are a few capable grid plug-ins out there already. Among them:

But in the end none of them fit the bill of what I needed in this situation. All of these require custom CSS and some of them are fairly complex to restyle. Others are AJAX only or work better with AJAX loaded data. However, I need to actually try (as much as possible) to maintain the original styling of the tables without requiring extensive re-styling.

Building the makeTableScrollable() Plug-in

To make a table scrollable requires rearranging the table a bit. In the plug-in I built I create two <div> tags and split the table into two: one for the table header and one for the table body. The bottom <div> tag then contains only the table's row data and can be scrolled while the header stays fixed. Using jQuery the basic idea is pretty simple: You create the divs, copy the original table into the bottom, then clone the table, clear all content append the <thead> section, into new table and then copy that table into the second header <div>. Easy as pie, right?

Unfortunately it's a bit more complicated than that as it's tricky to get the width of the table right to account for the scrollbar (by adding a small column) and making sure the borders properly line up for the two tables. A lot of style settings have to be made to ensure the table is a fixed size, to remove and reattach borders, to add extra space to allow for the scrollbar and so forth.

The end result of my plug-in is a table with a scrollbar. Using the same table I used earlier the result looks like this:

TableScrolling

To create it, I use the following jQuery plug-in logic to select my table and run the makeTableScrollable() plug-in against the selector:

$("#table").makeTableScrollable( { cssClass:"blackborder"} );

Without much further ado, here's the short code for the plug-in:

(function ($) {

$.fn.makeTableScrollable = function (options) {
    return this.each(function () {
        var $table = $(this);

        var opt = {
            // height of the table
            height: "250px",
            // right padding added to support the scrollbar
            rightPadding: "10px",
            // cssclass used for the wrapper div
            cssClass: ""
        }
        $.extend(opt, options);

        var $thead = $table.find("thead");
        var $ths = $thead.find("th");
        var id = $table.attr("id");
        var cssClass = $table.attr("class");

        if (!id)
            id = "_table_" + new Date().getMilliseconds().ToString();

        $table.width("+=" + opt.rightPadding);
        $table.css("border-width", 0);

        // add a column to all rows of the table
        var first = true;
        $table.find("tr").each(function () {
            var row = $(this);
            if (first) {
                row.append($("<th>").width(opt.rightPadding));
                first = false;
            }
            else
                row.append($("<td>").width(opt.rightPadding));
        });

        // force full sizing on each of the th elemnts
        $ths.each(function () {
            var $th = $(this);
            $th.css("width", $th.width());
        });

        // Create the table wrapper div
        var $tblDiv = $("<div>").css({ position: "relative",
            overflow: "hidden",
            overflowY: "scroll"
        })
                                    .addClass(opt.cssClass);
        var width = $table.width();
        $tblDiv.width(width).height(opt.height)
                .attr("id", id + "_wrapper")
                .css("border-top", "none");
        // Insert before $tblDiv
        $tblDiv.insertBefore($table);
        // then move the table into it
        $table.appendTo($tblDiv);

        // Clone the div for header
        var $hdDiv = $tblDiv.clone();
        $hdDiv.empty();
        var width = $table.width();
        $hdDiv.attr("style", "")
                .css("border-bottom", "none")
                .width(width)
                .attr("id", id + "_wrapper_header");

        // create a copy of the table and remove all children
        var $newTable = $($table).clone();
        $newTable.empty()
                    .attr("id", $table.attr("id") + "_header");

        $thead.appendTo($newTable);
        $hdDiv.insertBefore($tblDiv);
        $newTable.appendTo($hdDiv);

        $table.css("border-width", 0);
    });
}
})(jQuery);

Oh sweet spaghetti code :-)

The code starts out by dealing the parameters that can be passed in the options object map:

height

The height of the full table/structure. The height of the outside wrapper container. Defaults to 200px.

rightPadding

The padding that is added to the right of the table to account for the scrollbar.
Creates a column of this width and injects it into the table. If too small the rightmost
column might get truncated. if too large the empty column might show.

cssClass

The CSS class of the wrapping container that appears to wrap the table. If you want a border
around your table this class should probably provide it since the plug-in removes the table
border.

The rest of the code is obtuse, but pretty straight forward. It starts by creating a new column in the table to accommodate the width of the scrollbar and avoid clipping of text in the rightmost column. The width of the columns is explicitly set in the header elements to force the size of the table to be fixed and to provide the same sizing when the THEAD section is moved to a new copied table later. The table wrapper div is created, formatted and the table is moved into it. The new wrapper div is cloned for the header wrapper and configured. Finally the actual table is cloned and cleared of all elements. The original table's THEAD section is then moved into the new table. At last the new table is added to the header <div>, and the header <div> is inserted before the table wrapper <div>.

I'm always amazed how easy jQuery makes it to do this sort of re-arranging, and given of what's happening the amount of code is rather small.

Disclaimer: Your mileage may vary

A word of warning: I make no guarantees about the code above. It's a first cut and I provided this here mainly to demonstrate the concepts of decomposing and reassembling an HTML layout :-) which jQuery makes so nice and easy.

I tested this component against the typical scenarios we plan on using it for which are tables that use a few well known styles (or no styling at all). I suspect if you have complex styling on your <table> tag that things might not go so well. If you plan on using this plug-in you might want to minimize your styling of the table tag and defer any border formatting using the class passed in via the cssClass parameter, which ends up on the two wrapper div's that wrap the header and body rows.

There's also no explicit support for footers. I rarely if ever use footers (when not using paging that is), so I didn't feel the need to add footer support. However, if you need that it's not difficult to add - the logic is the same as adding the header.

The plug-in relies on a well-formatted table that has THEAD and TBODY sections along with TH tags in the header. Note that ASP.NET WebForm DataGrids and GridViews by default do not generate well-formatted table HTML. You can look at my Adding proper THEAD sections to a GridView post for more info on how to get a GridView to render properly.

The plug-in has no dependencies other than jQuery.

Even with the limitations in mind I hope this might be useful to some of you. I know I've already identified a number of places in my own existing applications where I will be plugging this in almost immediately.

Resources