Projects
  Site > Home > Projects > jQuery > The Pseudo-Tables Package   
The Pseudo-Tables Package

Overview

One of the reasons for the trend toward "semantic markup" is accessibility. If the tags say what you're doing (or trying to do), it makes it a lot easier for screen readers for the blind to make sense of the page. That's the reason why we're all supposed to be getting rid of <table> tags for layout alone. In the brave new world of accessible semantic markup, tables are used only to represent tabular data:

<table summary="An example of the only accessible use of tables, to represent tabular data.">
<tr> <th>#</th> <th>First Name</th> <th>Last Name</th>   </tr>
<tr> <td>1</td> <td>Aaron</td>      <td>Aardvark</td>    </tr>
<tr> <td>2</td> <td>Betty</td>      <td>Boop</td>        </tr>
<tr> <td>3</td> <td>Charlie</td>    <td>Chatmeister</td> </tr>
... 
</table>

That wasn't the most accessible table example possible, but this page isn't about how to use <table> to represent truly tabular data. It's merely an example of what the term "tabular data" means. Tabular data typically has table headings (<th> tags) at the top of columns, and the corresponding table data (<td> tags) contain data that match the column headings. Screen readers for the blind can usually make sense of tables that contain tabular data, and there are numerous HTML and CSS techniques that make the data as comprehensible for the blind as for sighted users.

But in the early days of the World Wide Web, tables solved a vexing layout problem: How do I force 2 screen elements to be side-by-side?

So very early on, web developers and designers started to use tables for layout. The practice became so common that WYSIWYG web page text editors actually generated tables for pixel-perfect alignment of screen elements. That's how tables-for-layout became ubiquitous.

We're not supposed to do that anymore.

Screen readers for the blind treat tables as tabular data, and in tabular data, row and column are significant. So they read out loud "row 2, column 5", which is great if row 2, column 5, actually represented something important. When it doesn't, reading it aloud gets in the way of understanding the page, rather than helping. And the screen readers read out row and column on every ... stinking ... cell ... till ... it ... drives ... you ... crazy!! So that's why we're not supposed to use tables for layout anymore.

In the United States, accessibility is the law, Section 508 of the Americans with Disabilities Act. But even in purely selfish terms, it just makes sense. It never hurts to have more customers, or more readers of your blog, right?

Pseudo-tables also make <fieldset> tags work right in forms. If you've never been able to get <fieldset> to work right when you align your form using <table>, <table> is the reason why. You can see how that's not true with pseudo-tables here. (Opens a new window.)

CSS To the Rescue, Part 1, Inline Blocks

Inline blocks were the first major breakthrough to allow us to position screen elements side-by-side without tables. An inline block is not automatically as wide as its container allows. (That would force a line break before the next thing after it.) Instead, it's only as wide as it needs to be, and it does not force a line break. Except for the fact that it's a rectangle, it's just as inline as text. Here's how to define an inline-block class for divs called "ib":

<style>
div.ib          {display:   inline-block;}
</style>
<!--[if lt IE 9]>
    <style>
    div.ib      {display:   inline;}
    </style>
<![endif]-->

The first problem with this solution was the fact that Microsoft Internet Explorer (MSIE) didn't support a display property of "inline-block". Microsoft's version of the same idea was to allow "inline" on elements that are normally blocks, such as <div>, and to treat them as rectangles. This necessitates MSIE's "conditional comments" syntax, as shown above, to redefine the class as display:inline if the MSIE version is less than 9.

Another problem was that space between the tags gets rendered, which messes up positioning and alignment of screen elements. To make divs truly side-by-side, you have to use the "delayed greater-than hack", so that, to the browser, adjacent divs would not be separated by any white space:

<div class="ib"> ... </div
><div class="ib"> ... </div>

Last but not least, inline blocks aren't really forced to be side-by-side. When inline blocks reach the end of their container, they wrap, just like text.

CSS To the Rescue, Part 2, Table Display Properties (and Floats)

The second major breakthrough was the creation of table display properties, which work in all of the "Big Five" browsers except MSIE 6 and 7. They solve the extraneous white space problem and the line break problem, simply by behaving the same way as tables. (Anything that isn't inside a table cell isn't imaged in the table.) If a table reaches the end of its container, but needs more room, it simply overflows its container, and the overflow property of the container kicks in (auto, hidden, scroll, etc). As with inline blocks, MSIE versions 6 and 7 don't support these new display properties, and techniques have arisen to simulate them, using floats:

<style>
div.t           {display:   table;}
div.r           {display:   table-row;}
div.c           {display:   table-cell;}
</style>
<!--[if lt IE 8]>
    <style>
    div.r       {zoom:      1;}
    div.r:after {
                content:    ".";
                display:    block;
                height:     0px;
                clear:      both;
                visibility: hidden;
                }
    div.c       {float:     left;}
    </style>
<![endif]-->

The MSIE 6 and 7 simulation using floats is nestable, just the way table display properties are, the rectangles are adjacent (no extraneous white space requiring the delayed greater-than hack) and the rectangles are variable width in the absence of a width property. But unfortunately, floats also wrap when they reach the end of their container. Short of CSS-Positioning, that's the state of the art of "pseudo-tables" (simulating tables without the <table> tag).

The Last Unsolved Problems of Pseudo-Tables (MSIE 6 and 7)

This is what you probably still have to put up with:

  1. Extraneous white space if you're using inline blocks, but consider the delayed greater-than hack too ugly to use.
  2. Both inline blocks and floats wrap when they reach the end of their container. There's no way to solve that problem in MSIE 6 and 7 without using CSS-Positioning. The weary concensus of the developer community seems to be to require a minimum screen resolution, make sure the pseudo-tables don't wrap in a maximized window at that resolution, and if a sighted user shrinks the window and it wraps, at least the user knows what caused it (their own actions). That much, at least, is tolerable.
  3. Both inline blocks and floats can vary in size. As with extraneous white space, this can result in alignment problems that are pretty close to intolerable. No one wants to produce a web page that's downright ugly because nothing lines up. It means that you have to do one of two very unpleasant things: explicitly control the height and width of every cell, or suffer the fact that rows and columns don't line up neatly.

The Pseudo-Tables Package of jQuery plug-ins was written to deal with both remaining MSIE 6 and 7 problems, so that you don't have to give up on CSS entirely for side-by-side layout and stick with tables. It's designed to work even if the pseudo-tables are nested.

How to Use the Pseudo-Tables Package

The plug-ins require jQuery 1.3 or higher (the version where .closest() was introduced.)

First, include findClosest and alignPseudoTables. Both are required, because alignPseudoTables calls findClosest. If you want to align nested tables, include findNestedFirst too:

<script src="path/jquery.findClosest.js></script>
<script src="path/jquery.alignPseudoTables.js></script>
<script src="path/jquery.findNestedFirst.js></script><!-- if needed -->

Then determine jQuery selectors that match all of the pseudo tables, rows and cells of your implementation. These plug-ins are a toolbox. They don't tell you how to code or define particular HTML or CSS you must use. But they do rely on your being able to tell them jQuery selectors that mean "this is what I consider a table/row/cell". In the plug-in parameter lists, these selectors are named pSelectorTable, pSelectorRow and pSelectorCell. In the example CSS code above, they correspond to jQuery selectors ".t", ".r" and ".c", respectively. But you can call them whatever you want.

Next, decide which tables you want to align. You don't have to align them all. Using the example classes above, you might want to align only the pseudo table with id="FormTable" ("#FormTable"), or that one and every table it contains ("#FormTable, #FormTable .t"), or all tables (".t").

If you want to align nested tables, select the tables you want to align in nested order. That's why the Pseudo-Tables Package includes findNestedFirst. The reason is, the sizes of outer pseudo-tables depend upon the adjusted sizes of inner pseudo-tables they contain.

Last, chain the selected tables to alignPseudoTables. So, combining the examples just cited (with FormTable and all the nested tables it contains), the chain might look something like this:

$(document).findNestedFirst("#FormTable, #FormTable .t").alignPseudoTables(".t",".r",".c");

To save on unnecessary script loads, you're probably going to want to put all of the above into MSIE conditional comments, so that they won't be done in the case of browsers that support table display properties (if that's the technique you're using). Putting them all together, it might look something like this:

<!--[if lt IE 8]>
<script src="path/jquery.findClosest.js></script>
<script src="path/jquery.alignPseudoTables.js></script>
<script src="path/jquery.findNestedFirst.js></script>
<script>
$(document).ready(function()
    {
    $(document).findNestedFirst("#FormTable, #FormTable .t").alignPseudoTables(".t",".r",".c");
    });
</script>
<![endif]-->

Although I wrote the plug-ins not to care what technique you use for pseudo-tables, the table display properties and floats technique is what I recommend, for the reason so visible in this code example. You only have to call .alignPseudoTables() in the cases of MSIE 6 and 7.

As with all jQuery plug-ins, they have to be included after including jQuery.

How the Pseudo-Tables Package Works

.findClosest() is a chainable function. It does the same thing as standard jQuery function .closest(), but in the downward direction. To do this, it needs a selector to get back to the starting DOM element, but without using a unique selector (*). In other words, the first selector must answer the question, what's the "logical ancestor" of the element I'm finding? And it must do so from the context of the found element. In the case of alignPseudoTables, the logical ancestor is easy to identify. The table is the logical ancestor of the row, so when this is a table, we do $(this).findClosest(".t",".r");). And the row is the logical ancestor of the cell, so when this is a row, we do ($(this).findClosest(".r",".c");).

(*) It's pretty important NOT use a unique selector. Using a unique selector results in .findClosest() not stopping at the closest found element. If you do that, .findClosest() produces the same result as .find(). In other words:

    Never do this: $(this).findClosest(this, ...)
    And this is just as bad: $(this).findClosest("#something", ...).

.alignPseudoTables() is also a chainable function. It aligns each of the elements of the collection, which it assumes is a pseudo-table, and aligns each one. To do this, it uses .findClosest() to get from tables to rows and from rows to cells, so that the rows and cells it finds belong only to the current table, not to any nested table. For each table, it makes 2 passes through the rows. The first pass accumulates the maximum heights and widths of the cells, relative to the row and column they're in, and it caches the cells for reuse in the second pass. The second pass sets the cells to the maximum height of the row and maximum width of the column in which they reside. It also adjusts the dimensions of the row, which will typically grow, according to whether the browser is in W3C box model mode (border, margin and padding outside the box) or MSIE 6 and 7 QuirksMode (border, margin and padding inside the box).

To solve the problem of forcing cells in the same row to be side-by-side, .alignPseudoTables() uses the only technique that works, CSS absolute positioning. The row is converted to position:relative (if it's static), which makes it the frame-of-reference for any absolutely-positioned elements it contains. It then converts the cells in the row to position:absolute and positions them according to W3C box model / QuirksMode, as appropriate. This second step also implicitly solves the extraneous white space problem of inline blocks, and it also solves the alignment problem of inline blocks and floats that are of varying heights and widths. All "Last Unsolved" MSIE 6 and 7 problems solved.)

.findNestedFirst() is a chainable function that alters the current collection. (To undo its effects, call .end().) Internally, it uses a tree structure of arrays to represent the nesting of DOM elements. Then, instead of returning the found elements in DOM order, it returns them with nested elements first, ahead of the elements that contain them. Within any element of the tree of arrays (that is, within any context of nesting), the use of arrays assures that nested elements are returned in DOM order (same order as .find()), in case that matters to someone else's code.

Testing

I have tested that alignPseudoTables works correctly with all of the Big Five browsers, even those that support table display properties. The browser and versions that matter most are MSIE 6 and 7 for Windows, of course. As mentioned elsewhere, I have no way to test MSIE 6 anymore, but I've tested MSIE 7.

For all browsers, I tested in both standards-compliant mode (<!DOCTYPE html>) and QuirksMode. I also tested it against the two most popular pseudo-table techniques, inline blocks and floats. And I also tested both Mac and Windows. This resulted in 8 tests per browser, except MSIE, which had only 4 tests (no Mac version). Within each browser, the results of all tests were essentially the same. That said, the following summarizes 36 tests:

Inline Blocks

  • All Browsers:
    • Before calling .alignPseudoTables(): Rows and columns don't line up. In most cases, shrinking window reveals that cells wrap (*). A real horror show.
      (*) Some of them honor nowrap and enforce side-by-side, usually with a strict DOCTYPE. Lost my notes as to which ones honored nowrap. Will repeat tests and post more details.
    • After calling .alignPseudoTables(): Everything fixed. Looks as good as real tables.

Table Display Properties (Non-MSIE and MSIE 8+) and Floats (MSIE 6 and 7)

  • Firefox 3.6.13 (Win/Mac):
    • Before calling .alignPseudoTables(): Doesn't honor style attribute's height and width. Sizes cells according to their contents.
    • After calling .alignPseudoTables(): Doesn't hurt anything. Pseudo-table looks identical. (Perhaps it would be more accurate to say, "doesn't improve anything". I would love to report that having consistent row heights and consistent column widths across all cells resulted in a pseudo-table that honors those values, but that didn't happen. At least it looks no worse.)
  • MSIE 7 (Win):
    • Before calling .alignPseudoTables(): Rows and columns don't line up. Shrinking window reveals that cells wrap. A real horror show.
    • After calling .alignPseudoTables(): Everything fixed. Looks as good as Google Chrome, Opera and Safari.
  • Google Chrome 9, Opera 11 and Safari 5.0.3 (Win/Mac):
    • Before calling .alignPseudoTables(): Looks great.
    • After calling .alignPseudoTables(): Doesn't hurt anything. Pseudo-table looks identical (except for row border being visible, as intended).

My test files are here (inline blocks, any browser) and here (table display properties and floats, only meaningful in MSIE 6 or 7). Both open a new window. Both contain code in comments to allow you to test other features (if you grab the file and modify it).

View Source

The following .js hotlinks open a new window containing the source code. Then you can do a File > Save As ... to save the source to your hard drive, if you like. The .zip file can probably be saved directly without opening it (depending on your browser).

If you prefer to get your plugins from the plugins.jquery.com site, so that you can see and verify the MD5 hash, for example, the following hotlink opens a new window to the plugins.jquery.com page for release 1.0, which has a hotlink to their copy of the zip file, with MD5 hash:

Planned for Version 2.0

Colspan and Rowspan

The next version, if I do it, will support data-colspan and data-rowspan attributes on cells. But that feature would immediately run smack-dab into a combinatorial explosion of possible use cases. It could also run counter to how future versions of CSS deal with colspan/rowspan. So I think I might hold off on that feature for a while.

In the meantime, you can use the jQuery-UI .position() function to position a div at the top-left corner of the top-left cell where you want the div to span. Then size the div to extend to the bottom-right corner of the bottom-right cell where you want the div to span. That's the hard part, obviously. But using this package too will at least assure that you have only one sum of row heights and only one sum of column widths to come up with the spanning div's size.

If that's too complex, you can also simulate colspan and rowspan by nesting pseudo-tables.

Cell Containing Nested Table, Slight Alignment Problem

In MSIE 7 QuirksMode, cells that contain nested tables sometimes overlap their row's border a bit, by a few pixels. This seems to be an inconsistent browser bug, or perhaps a bug in jQuery's .height() and .width() routines in that situation. W3C box model mode is fine.

Can't think of a way to fix it except to resize the containing cell while aligning the table it contains. So far, I haven't been doing that, because the containing cell could contain other markup. In other words, resizing it would probably make matters worse for some developers using this package.

Right now, you probably won't even see it unless the containing cell and its row both have a border. (Even then, only in MSIE 7 QuirksMode.) So for now, I'm just pointing it out and hoping someone more familiar with MSIE layout internals (or jQuery .height() and .width() internals) can figure out why it's happening.

In the meantime, alignment is perfect in W3C boxModel mode. If you're not using any markup that requires QuirksMode, you can eliminate the slight overlap problem by simply changing the DOCTYPE.