HTML Scrolling Table With Fixed Headers jQuery Plugin

I have been searching for a nice simple jQuery plugin to convert a HTML table element to a scrolling element with a fixed header. The following demo page includes three tables. The top one has the desired effect including CSS styles. The middle one has the CSS styles, but has not had the jQuery goodness applied. And the bottom table is the HTML before applying CSS and the jQuery.

Demo Download

imageThe screen shot above is an example of the scrollable table with data in it.

There are a lot of jQuery grids / tables on the internet. Here is a Stack Overflow question with recommendations for the best options. I tried a number of these plugins and the configuration seemed a bit complex. I venture to guess the complexity comes with trying to support many options (paging, JSON, XML…etc). This plugin is not being created to compete at that level, but rather provide a simpler alternative. I wanted to be able to create a scrolling table by applying the following jQuery

$("table#person").createScrollableTable({
    width: '800px',
    height: '400px'
});

to a semantic HTML table like the following

<table id="person" class="tbl1">
    <thead>
        <tr>
            <th>Id</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Birthdate</th>
            <th>Motto</th>
        </tr>
    </thead>

    <tbody>
       <tr>
            <td class="id">0</td>
            <td class="fname">Tim</td>
            <td class="lname">Brown</td>
            <td class="bdate">1/18/1947</td>
            <td class="motto">Id ligula sodales et tincidunt sagittis lectus maecenas tincidunt lorem.</td>
        </tr>
        <tr>
            <td class="id">1</td>
            <td class="fname">Dana</td>
            <td class="lname">Washington</td>
            <td class="bdate">9/2/1994</td>
            <td class="motto">Porta interdum in posuere donec duis ullamcorper nibh amet nunc ligula rutrum ante ac.</td>
        </tr>

        <!-- Additional rows removed for brevity. -->
    </tbody>
</table>

The HTML can be generated using what ever type of server language that you favor. The important things to note:

  1. The JavaScript is simple. Use a jQuery selector and call the function with desired width & height.
  2. The HTML is semantic. The HTML is very bare-bones. The ‘tbl1’ class only adds style to the table elements. The class elements on the HTML td elements is not required, but is used to set column widths via CSS for the table.

Concept:

I first tried to create the minimal and cleanest HTML & CSS necessary to create the effect. I ended up with the following HTML and demo:

<div id="person_table_wrap" style="border: solid 1px #888; overflow: hidden; display: inline-block;">
    <div id="person_head_wrap" style="overflow: hidden; width: 800px;">
        <table id="person" class="tbl1">
            <thead>
                <tr>
                    <th>Id</th>
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Birthdate</th>
                    <th>Motto</th>
                </tr>
            </thead>
        </table>
    </div>
    <div id="person_body_wrap" style="overflow: auto; width: 800px; height: 400px;">
        <table id="person" class="tbl1">
            <tbody>
                <tr>
                    <td>0</td>
                    <td>Tim</td>
                    <td>Brown</td>
                    <td>1/18/1947</td>
                    <td>Id ligula sodales et tincidunt sagittis lectus maecenas tincidunt lorem.</td>
                </tr>
                <tr>
                    <td>1</td>
                    <td>Dana</td>
                    <td>Washington</td>
                    <td>9/2/1994</td>
                    <td>Porta interdum in posuere donec duis ullamcorper nibh amet nunc ligula rutrum ante ac.</td>
                </tr>

                <!-- Additional rows removed for brevity. -->
        </table>
    </div>
</div>

Demo 2

So what have we done? The biggest transformation has been to split the table header and body into separate HTML table elements. Each of these tables includes a wrapper HTML ‘div’ element (a head wrap and a body wrap). The result is placed inside another HTML ‘div’ container (a table wrap). It is simple enough to do this transformation with jQuery, but we still have a problem.

imageNotice from the demo or the above screen shot, the columns do not align. This problem arises because each table is allowed to set column widths independently. Using CSS to force all but the last (need to allow it to be the ‘expando’) columns to be the same width will fix this problem and you will get the following:

image This is very close to what we want, except for the case where we want the columns to auto-size. Using a bit more jQuery, we can automatically set the column widths in the table header to the same as the widths in the table body.

That is the recipe for this jQuery plugin. Now, let’s get to the plugin code.

Plugin Code:

Here is the complete jQuery plugin:

(function($) {
    $.fn.createScrollableTable = function(options) {

        var defaults = {
            width: '400px',
            height: '300px',
            border: 'solid 1px #888'
        };
        var options = $.extend(defaults, options);

        return this.each(function() {
            var table = $(this);
            prepareTable(table);
        });

        function prepareTable(table) {
            var tableId = table.attr('id');

            // wrap the current table (will end up being just body table)
            var bodyWrap = table.wrap('<div></div>')
                                    .parent()
                                    .attr('id', tableId + '_body_wrap')
                                    .css({
                                        width: options.width,
                                        height: options.height,
                                        overflow: 'auto'
                                    });

            // wrap the body
            var tableWrap = bodyWrap.wrap('<div></div>')
                                    .parent()
                                    .attr('id', tableId + '_table_wrap')
                                    .css({
                                        overflow: 'hidden',
                                        display: 'inline-block',
                                        border: options.border
                                    });

            // clone the header
            var headWrap = $(document.createElement('div'))
                                    .attr('Id', tableId + '_head_wrap')
                                    .prependTo(tableWrap)
                                    .css({
                                        width: options.width,
                                        overflow: 'hidden'
                                    });

            var headTable = table.clone(true)
                                    .attr('Id', tableId + '_head')
                                    .appendTo(headWrap)
                                    .css({
                                        'table-layout': 'fixed'
                                    });

            var bufferCol = $(document.createElement('th'))
                                    .css({
                                        width: '100%'
                                    })
                                    .appendTo(headTable.find('thead tr'));

            // remove the extra html
            headTable.find('tbody').remove();
            table.find('thead').remove();

            // size the header columns to match the body
            var allBodyCols = table.find('tbody tr:first td');
            headTable.find('thead tr th').each(function(index) {
                var desiredWidth = getWidth($(allBodyCols[index]));
                $(this).css({ width: desiredWidth + 'px' });
            });
        }

        function getWidth(td) {
            if ($.browser.msie) { return $(td).outerWidth() - 1; }
            if ($.browser.mozilla) { return $(td).width(); }
            if ($.browser.safari) { return $(td).outerWidth(); }
            return $(td).outerWidth();
        };

    };

})(jQuery);

There are only three configurable options that set the width, height and border. The width is applied to the body and head wrapper HTML ‘div’ elements. The height is only applied to the body wrap. The border is applied to the table wrap.

The ‘prepareTable’ function is where the semantic HTML table is transformed into two tables. First a HTML ‘div’ is wrapped around the table to form the body wrap. This table currently contains ‘thead’ and ‘tbody’ elements but not for long. Then a HTML ‘div’ is wrapped around this whole thing to form the table wrap. Then a new HTML ‘div’ element is prepended to the table wrap to become the head wrap. A clone of the table is then appended to the head wrap.

To enable the automatic column width setting an empty trailing column, ‘bufferCol’, is added to the table header. This is the “secret sauce” in the plugin. This element acts like an ‘expando’ and allows the other elements to have a fixed width. Finally, the HTML ‘tbody’ element in the head wrapper and the HTML ‘thead’ in the body wrapper are removed.

This completes the HTML transformation. Along the way the required CSS is added to the elements. The final bit of jQuery goodness sets the width of the columns in the header to the width of the columns in the footer. There is a bit of browser variation that is taken care of in the ‘getWidth’ function.

Table CSS:

Although not necessary for the jQuery plugin, I thought that I would provide the CSS that I am using to style the tables.

/* CSS Reset
----------------------------------------------------------*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td
{
    margin: 0;
    padding: 0;
    border: 0;
    outline: 0;
    font-size: 100%;
    vertical-align: baseline;
    background: transparent;
}
table
{
    border-collapse: collapse;
    border-spacing: 0;
}
/* h1 style
----------------------------------------------------------*/
h1
{
    font-size: 20px;
    text-align: center;
    background-color: #ffffdd;
    border: solid 1px #888;
    margin: 0 0 10px 0;
    padding: 5px;
}

/* Table style
----------------------------------------------------------*/
table.tbl1
{
    width: 100%;
}

table.tbl1 thead
{
    background: #A3C2EA url('../images/tbl_head_bg.png') repeat-x scroll 0 0;
    color: #333;
    width: 100%;
}

table.tbl1 tbody
{
    width: 100%;
}

table.tbl1 tr td,
table.tbl1 tr th
{
    border: solid 1px #888;
    margin: 0;
    padding: 4px 0;
}

table.tbl1 tr td
{
    text-align: center;
    background-color: #fff;
    color: #444;
}

table.tbl1 tbody tr.alt td
{
    background-color: #e4ecf7;
}

table.tbl1 tr.over td,
table.tbl1 tbody tr.alt.over td
{
    background-color: #bcd4ec;
    color: #000;
}

table.tbl1 a
{
    color: #2a4a73;
    text-decoration: underline;
}

table.tbl1 a:hover
{
    text-decoration: none;
}

table.tbl1 a:visited
{
    color: #bccedc;
}

/* Define column widths
----------------------------------------------------------*/
table.tbl1 td.id
{
    width: 100px;
}
table.tbl1 td.fname
{
    width: 100px;
}
table.tbl1 td.lname
{
    width: 100px;
}
table.tbl1 td.bdate
{
    width: 100px;
}

Summary:

The plugin works using Internet Explorer 8, FireFox 3, Chrome 4, and Safari 4. Please let me know if you find issues or have suggestions for improvements.

Comments
  1. Rob Simmermon
    • rcravens
  2. sjunianto
    • rcravens
  3. Daniel Mühlbachler
    • rcravens
      • Anton Zaroutski
        • rcravens
  4. Joan
    • rcravens
  5. Alan
    • rcravens
  6. Matt
    • rcravens
      • Timmy
  7. Arun
    • rcravens
  8. Luis
  9. priya
  10. bredbånd test
  11. Sven Andersen
  12. Stu
  13. rama
  14. Faruk Demir
  15. Fabien LADRAt
  16. DENIS
  17. Henrique
  18. sam
  19. R Moran
  20. prasad
  21. Zahid

Leave a Reply

Your email address will not be published. Required fields are marked *

*