Chapter 8. Fun with Tables and Lists

When (X)HTML tables were first introduced, they evoked a variety of emotions in different developers: fear, confusion, satisfaction, excitement, and even loathing. They were confusing, yes, but they gave developers layout control they never had before. As time went on, tables began to handle the bulk of the work when it came to providing structures to display data on the client to the user. (X)HTML lists, although not inducing the love-hate relationship that tables sometimes did, also provided the developer with a means of structuring data. Until the idea of dynamic content came around, these tables and lists were workhorses for this static display.

But then came dynamic tables. Using CSS, rows and items could be highlighted when clicked on or moused over, and on-the-fly sorting became popular. The table and list became integral parts of the web site or application, with fancier and more sophisticated looks thanks to the CSS rules that can be applied to them.

Now we have Ajax, and many developers can see ways to utilize these structures to create functionality in web applications that, until now, were limited to desktop and Windows applications. Lists provide ways to display hierarchical details and data. Tables can not only be sorted, but also added to, updated, and deleted without refreshing the browser.

Tables and lists are not the mysterious entities they once were; now they are useful everyday tools put to work in web applications. With Ajax being applied to these elements, tables and lists can not only be exciting, useful objects, but they can also now be fun.

Layout Without Tables

Tables in an application enable the developer to display tabular data to the user in an organized fashion. This is what tables should be used for, but this has not always been the case. Even after CSS rules made it easier for the developer to lay out a site, tables were still used prevalently in web design for the purposes of page layout. This not only breaks the practice of tables for data/CSS for layout, but it also hurts accessibility.

Besides the issue of accessibility on a site that uses tables for layout, consider the following problems associated with using tables:

  • Tables do not always function the way they should in all browsers, meaning pages might look different than expected.

  • Table layouts require many more text characters to produce a table, increasing page sizes and download times.

All of the major browsers had a number of issues at one time or another when it came to rendering a table. Columns did not align correctly, gaps were placed between rows, and the thickness of rows and columns would fluctuate. This put the developer in the same position she would be in if she had chosen to use CSS instead. No matter which way the page was laid out, the developer had to test the layout in all browsers for compatibility issues.

You need many more characters to lay out a site with a table than with CSS rules. Not only does the text size increase, but the complexity of the Document Object Model (DOM) document increases as well. This leads to slower rendering on slower machines, and slower processing of the DOM document by any JavaScript that may need to process it.

Old Layouts

In the old days of the Web, design tables were used in page layout because there was no alternative. Tables could align text and images in the desired ways, but more important, tables could produce layouts that had two-, three-, and even four-column designs. To make web design even more complicated, tables were nested within tables that were sometimes three or four levels deep. For a simple example, examine the following:

<table>
    <tr>
        <!-- this is the left-side column for the page -->
        <td>
            <table>
                <tr>
                    <td>Section One</td>
                </tr>
                <tr>
                    <td>
                        <table>
                            <tr>
                                <td>Section Two.One</td>
                                <td>Section Two.Two</td>
                            </tr>
                            <tr>
                                <td colspan="2">Section Two.Three</td>
                            </tr>
                        </table>
                    </td>
                </tr>
                <tr>
                    <td>Section Three</td>
                </tr>
            </table>
        </td>
        <!-- this is the right-side column for the page -->
        <td valign="top">
            <table>
                <tr>
                    <td>
                        <p>Main page content.</p>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
</table>

Tables gave the designers columned layouts, complicated picture links (as an alternative to an image map), simple form alignments, and other uses as well. Take the simple layout of a login page that provides inputs for a username and password, along with a Submit button, such as the one shown in Figure 8-1.

A simple username/password login page example

Figure 8-1. A simple username/password login page example

You could easily lay out this page using the following table design:

<table>
    <tr>
        <td align="right">Email:</td>
        <td>
            <input type="text" name="username" value="" />
        </td>
    </tr>
    <tr>
        <td align="right">Password:</td>
        <td>
            <input type="password" name="password" value="" />
        </td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <input type="submit" value="Login" />
        </td>
    </tr>
</table>

These examples are simple in nature, yet they illustrate the complexity and bloat associated with using tables for page layout.

Using CSS

It is the wise developer who uses CSS for all of the presentation and layout of an Ajax web application instead of relying on tables. Besides the reasons I gave in the preceding section, CSS allows the developer to separate the presentation layer from the structure or data layer. I cannot emphasize this enough.

The first layout example using tables is one of many problems you can solve with some CSS and a little forethought. For example:

<div id="mainContent">
    <p>Main page content.</p>
</div>
<div id="leftColumn">
    <div id="sectionOne">
        <h3>Section One</h3>
    </div>
    <div id="sectionTwo">
        <p><span>Section Two.One</span><span>Section Two.Two</span></p>
        <p>Section Two.Three</p>
    </div>
    <div id="sectionThree">
        <h3>Section Three</h3>
    </div>
</div>

This structure needs just a few CSS rules to make the layout like that of the table:

body {
    margin: 0;
    padding: 0;
}

#mainContent {
    margin-left: 230px;
    width: 530px;
}

#leftColumn {
    left: 0;
    overflow: hidden;
    position: absolute;
    top: 0;
    width: 220px;
}
#sectionTwo span {
    padding-right: 2em;
    white-space: nowrap;
}

The structure is easier to read when using CSS, and it is more accessible to screen readers and text-only browsers because the main content comes first in these browsers. This makes browsing a page faster and less frustrating.

Tip

Separating presentation from structure properly satisfies the following Web Accessibility Initiative-Web Content Accessibility Guidelines (WAI-WCAG) 1.0 guidelines:

  • Priority 2 checkpoint 3.3: Use stylesheets to control layout and presentation.

  • Priority 2 checkpoint 5.3: Do not use tables for layout unless the table makes sense when linearized.

The CSS rules lay out a design that works for all browsers with a screen size of 800 × 600 or better. Absolute positioning aligns the left column where it needs to go (before the main content) even though in the structure itself it comes last. In this example, the overflow from the column is set to hidden. This becomes an issue of preference as to how you want your site to render.

The other big use for tables in layout design is in aligning form controls, as the second example in the preceding section showed. The following example shows how you can accomplish this same layout without tables:

<div id="login">
    <div>
        <span>Email:</span> <input type="text" name="username" value="" />
    </div>
    <div>
        <span>Password:</span> <input type="password" name="password" value="" />
    </div>
    <div class="center">
        <input type="submit" value="Login" />
    </div>
</div>

This structure then uses the following CSS rules:

#login {
    width: 280px;
}

#login div span {
    float: left;
    text-align: right;
    width: 100px;
}

div.center {
    text-align: center;
}

This accomplishes the same layout and adds flexibility to page implementation. Here, an absolute width is set for the form inputs, as are the <span> elements that are used to hold the input labels. The labels are aligned to the right, but the width of the span will be ignored for <span> elements because they are displayed inline. By making them float to the left, we force them to be displayed as block, and the width is then recognized.

CSS was designed for layout and presentation, and you should use it whenever possible. The WAI-WCAG guidelines specifically state that you should avoid tables for layout whenever possible, and that making Ajax applications that are still somewhat accessible is a high priority. The two examples shown here may be simple, but I hope they illustrate how easy it is to use CSS for all of your layout needs.

Accessible Tables

So far, I have only talked about what you should not use tables for, not really what you should use them for. Before exploring the tricks a developer can use to manipulate tables dynamically with Ajax, I want to take a brief look at the proper way to build a table in XHTML to make it accessible.

Most of the time, a user can look at a table with data and determine the table’s purpose without much difficulty. However, people who are blind and use a page reader, for instance, do not have this luxury. In these and other cases, giving the user a caption for the table (much like every table title in this book) allows the user to quickly identify the table’s purpose without having to look at the table. You use the <caption> element to give a table a caption, like this:

<table>
    <caption>Current Ajax Books from O'Reilly Media</caption>
    <tr>
        <td>Ajax and Web Services</td>
        <td>Mark Pruett</td>
        <td>August 2006</td>
    </tr>
    <tr>
        <td>Ajax Design Patterns</td>
        <td>Michael Mahemoff</td>
        <td>June 2006</td>
    </tr>
    ...
    <tr>
        <td>Your Life in Web Apps</td>
        <td>Giles Turnbull</td>
        <td>June 2006</td>
    </tr>
</table>

Giving a table a caption aids normal browser users, but to go further down the path to accessible tables, the developer should also provide a summary for the table. The summary attribute in the <table> element is used for this purpose, as this example shows:

<table summary="This table provides a list of current Ajax books from O'Reilly Media,
        broken down by title, author, and release date">
    <caption>Current Ajax Books from O'Reilly Media</caption>
    <tr>
        <td>Ajax and Web Services</td>
        <td>Mark Pruett</td>
        <td>August 2006</td>
    </tr>
    <tr>
        <td>Ajax Design Patterns</td>
        <td>Michael Mahemoff</td>
        <td>June 2006</td>
    </tr>
    ...
    <tr>
        <td>Your Life in Web Apps</td>
        <td>Giles Turnbull</td>
        <td>June 2006</td>
    </tr>
</table>

Tip

Adding a summary to the table properly satisfies the following WAI-WCAG 1.0 guideline:

  • Priority 3 checkpoint 5.5: Provide summaries for tables.

A table with tabular data should have a header for every column of data. This header should be defined as such, and not as another <td> element with a style attached to it to make it stand out. Use of the <th> element is recommended for all tables that are not to be used for layout. For example:

<table summary="This table provides a list of current Ajax books from O'Reilly
        Media, broken down by title, author, and release date">
    <caption>Current Ajax Books from O'Reilly Media</caption>
    <tr>
        <th id="h1">Book Title</th>
        <th id="h2">Author</th>
        <th id="h3">Release Date</th>
    </tr>
    <tr>
        <td headers="h1">Ajax and Web Services</td>
        <td headers="h2">Mark Pruett</td>
        <td headers="h3">August 2006</td>
    </tr>
    <tr>
        <td headers="h1">Ajax Design Patterns</td>
        <td headers="h2">Michael Mahemoff</td>
        <td headers="h3">June 2006</td>
    </tr>
    ...
    <tr>
        <td headers="h1">Your Life in Web Apps</td>
        <td headers="h2">Giles Turnbull</td>
        <td headers="h3">June 2006</td>
    </tr>
</table>

Warning

Remember that if you use a table for layout, do not use the <th> element to add something in bold and to center it. This would break the WAI-WCAG 1.0 Guideline Priority 2 checkpoint 5.4: if a table is used for layout, do not use any structural markup for the purpose of visual formatting.

Notice that the <th> element is given a unique id attribute, and that attribute value is used to associate headers with data by means of the headers attribute on a <td> element. Now, defining a table header is great, as it aids screen readers in parsing through a table. But with the header defined, this is how the screen reader would output the earlier example:

"This table provides a list of current Ajax books from O'Reilly Media, broken down
by title, author, and release date"

"Current Ajax Books from O'Reilly Media"

"Book Title: Ajax and Web Services   Author: Mark Pruett       Release Date: August
2006"
"Book Title: Ajax Design Patterns    Author: Michael Mahemoff  Release Date: June
2006"
...
"Book Title: Your Life in Web Apps   Author: Giles Turnbull    Release Date: June
2006"

Repeating the headers—especially if they are long and there is a lot of data and plenty of rows—can get wearisome. To remedy these situations, you can abbreviate the headers so that the screen reader will not have to output as much. You use the abbr attribute to do this. For example:

    <tr>
        <th id="h1" abbr="title">Book Title</th>
        <th id="h2">Author</th>
        <th id="h3" abbr="date">Release Date</th>
    </tr>

Now when the screen reader outputs the table, it will look like this:

"title: Ajax and Web Services   Author: Mark Pruett       date: August 2006"
"title: Ajax Design Patterns    Author: Michael Mahemoff  date: June 2006"
...
"title: Your Life in Web Apps   Author: Giles Turnbull    date: June 2006"

This little change can make a big difference to someone with an assistive technology that accesses the table. For big tables, cutting down on the number of words a screen reader or something similar must output can be a great benefit for the person using it.

Tip

Adding the <th> element and header attributes to identify the header columns and row elements, and creating abbreviations for the header, satisfies the following WAI-WCAG 1.0 guidelines:

  • Priority 1 checkpoint 5.1: For data tables, identify row and column headers.

  • Priority 3 checkpoint 5.6: Provide abbreviations for header labels.

To clarify the structure and relationship of the rows contained in a table, you can group the rows according to their use. The elements <thead>, <tfoot>, and <tbody> provide this functionality.

To use <tbody> elements, you must precede them with a <thead> element. (The <tfoot> element is optional, but if you add it, it also must go before the <tbody> elements.) As you may have noticed from the pluralizations just used, there can be only one <thead> and <tfoot> element, but there may be multiple <tbody> elements. Example 8-1 shows a WAI-WCAG 1.0 table that is fully compliant.

Example 8-1. A table showing off all WAI-WCAG 1.0 checkpoints that are compliant

<table summary="This table provides a list of current Ajax books from O'Reilly Media,
        broken down by title, author, and release date">
    <caption>Current Ajax Books from O'Reilly Media</caption>
    <thead>
        <tr>
            <th id="h1">Book Title</th>
            <th id="h2">Author</th>
            <th id="h3">Release Date</th>
        </tr>
    </thead>
    <tbody id="pdfBooks">
        <tr>
            <td headers="h1">Ajax and Web Services</td>
            <td headers="h2">Mark Pruett</td>
            <td headers="h3">August 2006</td>
        </tr>
        <tr>
            <td headers="h1">Dynamic Apache with Ajax and JSON</td>
            <td headers="h2">Tracy Brown</td>
            <td headers="h3">September 2006</td>
        </tr>
        <tr>
            <td headers="h1">Your Life in Web Apps</td>
            <td headers="h2">Giles Turnbull</td>
            <td headers="h3">June 2006</td>
        </tr>
    </tbody>
    <tbody id="books">
        <tr>
            <td headers="h1">Ajax Design Patterns</td>
            <td headers="h2">Michael Mahemoff</td>
            <td headers="h3">June 2006</td>
        </tr>
        <tr>
            <td headers="h1">Ajax Hacks</td>
            <td headers="h2">Bruce W. Perry</td>
            <td headers="h3">March 2006</td>
        </tr>
        <tr>
            <td headers="h1">Head Rush Ajax</td>
            <td headers="h2">Brett McLaughlin</td>
            <td headers="h3">March 2006</td>
        </tr>
        <tr>
            <td headers="h1">Learning JavaScript</td>
            <td headers="h2">Shelley Powers</td>
            <td headers="h3">October 2006</td>
        </tr>
        <tr>
            <td headers="h1">Programming Atlas</td>
            <td headers="h2">Christian Wenz</td>
            <td headers="h3">September 2006</td>
        </tr>
    </tbody>
</table>

Figure 8-2 shows this table. A nice feature that browser makers could add to all of the existing clients on the Internet is to allow the table bodies (<tbody> elements) to scroll independent of the table’s header and possible footer. This currently does not happen, and the developer has to go through many hoops to implement this sort of functionality, but this is a hack, at best. What most modern browsers have already implemented in regard to table display is the ability to print both the header and the footer on every page.

The accessible table from (with some style rules applied)

Figure 8-2. The accessible table from Example 8-1 (with some style rules applied)

Tip

Adding the <thead>, <tfoot>, and <tbody> elements to a table to separate the differences in content satisfies the following WAI-WCAG 1.0 guideline:

  • Priority 1 checkpoint 5.2: For data tables that have two or more logical levels of row or column headers, use markup to associate data cells and header cells.

We have only one thing left to talk about with regard to WAI-WCAG compliance, and that is what to do about tables that may not read properly with a screen reader because they are not linearized properly. For example, if a table were output to the client like this:

Come back to Saint Louis University with    There will be a variety of events for
your family and friends for a whole         everyone, including a golf cart parade,
weekend of True Blue Biliken Spirit at      campus tours and live entertainment.
Homecoming 2006, Sept. 29 - Oct. 1.

a screen reader may interpret it like this:

Come back to Saint Louis University with There will be a variety of events for
your family and friends for a whole everyone, including a golf cart parade,
weekend of True Blue Biliken Spirit at campus tours and live entertainment.
Homecoming 2006, Sept. 29 - Oct. 1.

Though it is not very difficult to linearize a table, it does require a little bit of forethought. Usually you will need to translate the table data into such a scheme. One way to do this is to use the dir attribute to specify the column layout order for the browser.

As browsers advance, this will become less of an issue, but until then it should at least be in the back of your mind to attempt a serialized version of any table that could be misinterpreted.

Tip

Attempting to have a serialized version of the table satisfies the following WAI-WCAG 1.0 guideline:

  • Priority 3 checkpoint 10.3: Until user agents (including assistive technologies) render side-by-side text correctly, provide a linear text alternative (on the current page or some other) for all tables that lay out text in parallel, word-wrapped columns.

The point of this introduction to accessible tables is to remind you how easy it is to make a data table accessible to people who do not use one of the common browsers. Now that you should have a thorough understanding of the structure of an XHTML table, we need to tackle how to interact with it dynamically.

Interacting with Tables

When I say “interacting with tables,” I really mean dynamically creating, deleting, and updating tables using JavaScript. Danny Goodman wrote a terrific article on this subject, which you can find in O’Reilly’s Web DevCenter. Located at http://www.oreillynet.com/pub/a/javascript/2003/05/06/dannygoodman.html, “Dynamic HTML Tables: Improving Performance” examines different methods of creating dynamic content.

In his article, Danny discusses the following ways to dynamically make tables, using DOM methods in some and proprietary methods in others:

Method one

Use the methods insertRow( ) and insertCell( ) with the innerHTML property.

Method two

Use the methods insertRow( ) and insertCell( ) with DOM text nodes.

Method three

Use the createElement( ) method with a DOM DocumentFragment element and the innerHTML property.

Method four

Use the createElement( ) method with a DOM DocumentFragment element and DOM text nodes.

Method five

Assemble all of the content that will be contained within the <tbody> element as a string, and then assign this to the innerHTML property of the element.

Method six

Assemble the entire table as a string, and then assign this to the innerHTML of an outer <div> element.

The first method uses the methods insertRow( ) and insertCell( ) to give the developer access to the DOM document when items are added. The result of calling these methods is a reference to a newly created DOM element. All that’s left is to put data into the table, and this first method uses the innerHTML property for this task. For example:

/* This is the element that will have data added to it */
var tbodyElement = $('tbodyOne'),
/* These are the new elements that will be created */
var newTrElement = null, newTdElement = null;

/*
 * This technique loops through the rows of data in the array, creating a new
 * row for the table for each row of data, and then looping through the columns
 * of data in the array, creating a new column that corresponds to each column
 * of data.  The column of data is then inserted into the table with the
 * /innerHTML/ property.
 */
for (var i = 0, il = dataArray.length; i < il; i++) {
    newTrElement = tbodyElement.insertRow(tbodyElement.rows.length);
    newTrElement.setAttribute('id', 'row_' + tbodyElement.rows.length);
    for (var j = 0, jl = dataArray[i].length; j < jl;) {
        newTdElement = newTrElement.incertCell(newTrElement.cells.length);
        newTdElement.setAttribute('id', 'r_' + tbodyElement.rows.length +
            'col_' + newTrElement.cells.length);
        newTdElement.innerHTML = dataArray[i][j++];
    }
}

The second method also uses the insertRow( ) and insertCell( ) methods to create the rows and columns. The difference is in how the data is populated into the table. This method sticks with DOM methods for the task, using createTextNode( ) and appendChild( ), as this example shows:

/* This is the element that will have data added to it */
var tbodyElement = $('tbodyOne'),
/* These are the new elements that will be created */
var newTrElement = null, newTdElement = null, newTxtNode = null;

/*
 * This technique loops through the rows of data in the array, creating a
 * new row for the table for each row of data, just as the last example had.
 * The looping through the columns of data in the array, creating a new
 * column that corresponds to each column of data, is also the same as the
 * last example.  The column of data is first created as a textNode and
 * then appended to the existing table.
 */
for (var i = 0, il = dataArray.length; i < il; i++) {
    newTrElement = tbodyElement.insertRow(tbodyElement.rows.length);
    newTrElement.setAttribute('id', 'row_' + tbodyElement.rows.length);
    for (var j = 0, jl = dataArray[i].length; j < jl;) {
        newTdElement = newTrElement.incertCell(newTrElement.cells.length);
        newTdElement.setAttribute('id', 'r_' + tbodyElement.rows.length +
            'col_' + newTrElement.cells.length);
        newTxtNode = document.createTextNode(dataArray[i][j++]);
        newTdElement.appendChild(newTxtNode);
    }
}

When comparing innerHTML to the DOM methods, you will find that different browsers perform better or worse with the different methods. Internet Explorer does better with the innerHTML property, which is not a surprise since Microsoft invented it. Mozilla-based browsers, however, perform better using DOM methods with this technique.

The third and fourth methods look to improve upon the speed and efficiency of the first two methods. They do so by building up data outside the DOM tree using a documentFragment and the createElement( ) method. There are definite speed advantages to not bothering the existing DOM document tree unless only one or two changes are made to it. In these cases, the documentFragment comes in handy. This example uses innerHTML to apply the data to the DOM document tree after everything is created:

/* This is the table that will have the data added to it */
var tableElement = $('theTable'),
/* These are the new elements that will be created */
var newTrElement = null, newTdElement = null;
/* This is the element that will be constructed in memory */
var newTbodyElement = document.createElement('tbody'),
/* This is the documentFragment that will be used to construct the new element */
var fragment = document.createDocumentFragment( );

/*
 * This technique loops through the rows of data in the array, creating a
 * new row for the table for each row of data, and then looping through the
 * columns of data in the array, creating a new column that corresponds
 * to each column of data.  The column of data is then inserted into the
 * table with the innerHTML method and appended to the new row.  When the
 * inner loop completes, the new row is appended to the fragment.  Once
 * the outer loop completes, the fragment is appended to the new tbody
 * element, which is in turn appended to the existing table.
 */
for (var i = 0, il = dataArray.length; i < il;) {
    newTrElement = document.createElement('tr'),
    newTrElement.setAttribute('id', 'row_' + i);
    for (var j = 0, jl = dataArray[i].length; j < jl;) {
        newTdElement = document.createElement('td'),
        newTdElement.setAttribute('id', 'r_' + i + 'col_' + j);
        newTdElement.innerHTML = dataArray[i++][j++];
        newTrElement.appendChild(newTdElement);
    }
    fragment.appendChild(newTrElement);
}
newTbodyElement.appendChild(fragment);
tableElement.appendChild(newTbodyElement);

This example, on the other hand, uses the DOM methods createTextNode( ) and appendChild( ):

/* This is the table that will have the data added to it */
var tableElement = $('theTable'),
/* These are the new elements that will be created */
var newTrElement = null, newTdElement = null, newTxtNode = null;
/* This is the element that will be constructed in memory */
var newTbodyElement = document.createElement('tbody'),
/* This is the documentFragment that will be used to construct the new element */
var fragment = document.createDocumentFragment( );

/*
 * This technique loops through the rows of data in the array, creating a
 * new row for the table for each row of data, and then looping through the
 * columns of data in the array, creating a new column that corresponds to
 * each column of data.   The column of data is first created as a /textNode/
 * and then appended to the existing table data element.  When the inner
 * loop completes, the new row is appended to the fragment.  Once the outer
 * loop completes, the fragment is appended to the new <tbody> element, which
 * is in turn appended to the existing table.
 */
for (var i = 0, il = dataArray.length; i < il;) {
    newTrElement = document.createElement('tr'),
    newTrElement.setAttribute('id', 'row_' + i);
    for (var j = 0, jl = dataArray[i].length; j < jl;) {
        newTdElement = document.createElement('td'),
        newTdElement.setAttribute('id', 'r_' + i + 'col_' + j);
        newTxtNode = document.createTextNode(dataArray[i++][j++]);
        newTdElement.appendChild(newTxtNode);
        newTrElement.appendChild(newTdElement);
    }
    fragment.appendChild(newTrElement);
}
newTbodyElement.appendChild(fragment);
tableElement.appendChild(newTbodyElement);

Again, there are differences in how fast the various browsers process each example. One thing that is common among all browsers is that methods three and four are significantly faster than methods one and two.

In all of the examples so far, I have been adding data to a <tbody> element within a table. Method five demonstrates perhaps the easiest solution: to simply replace the innerHTML of the <tbody>. But note that although this is an easy solution, the <tbody> element does not support the innerHTML element in Internet Explorer. If you are not concerned about cross-browser support, the following example is for you:

/*
 * This is the variable that will hold our string of data that we
 * dynamically build.
 */
var data = '';

/*
 * Loop through the /dataArray/ much like the other methods, but build the
 * table using a string and then move the string into the /innerHTML/ of
 * the <tbody> element.
 */
for (var i = 0, il = dataArray.length; i < il;) {
    data += '<tr id="row_' + i + '">';
    for (var j = 0, jl = dataArray[i].length; j < jl;)
        data += '<td id="r_' + i + 'col_' + j + '">' + dataArray[i++][j++] +
            '</td>';
    data += '</tr>';
}
$('tbodyOne').innerHTML = data;

Method five is much faster than the other methods introduced so far. However, no DOM-compliant method is comparable. It is also unfortunate that Internet Explorer does not support it.

The sixth method gets around the innerHTML/<tbody> issue with Internet Explorer. This method is the best cross-browser approach, as long as you are not concerned about World Wide Web Consortium (W3C) DOM compliance. This method simply creates the entire table in a string, and then adds these string contents to the innerHTML of a <div> element acting as a table wrapper, as shown in this example:

/*
 * This is the variable that will hold our string of data that we dynamically
 * build, only this time start by building the table element.
 */
var data = '<table>';

/*
 * Loop through the /dataArray/ much like the other methods, but build the table
 * using a string and then move the string into the /innerHTML/ of the table
 * wrapper element.
 */
for (var i = 0, il = dataArray.length; i < il;) {
    data += '<tr id="row_' + i + '">';
    for (var j = 0, jl = dataArray[i].length; j < jl;)
        data += '<td id="r_' + i + 'col_' + j + '">' + dataArray[i++][j++] +
            '</td>';
    data += '</tr>';
}
data += '</table>';
$('tableWrapper').innerHTML = data;

This method, as it turns out, is the fastest way to build a table dynamically. Performance is important when it comes to Ajax applications, so we will come back to this method for our examples.

Updating content in a row shares a common problem with deleting a row of data. You must first locate the row in question. Then, updating is simple enough, as this example shows:

/*
 * We will assume with this code that we searched through the rows of the table
 * to find a particular row to update the data in.  The code will then return
 * the row element as the variable /oldTrElement/, and the number for that row
 * as the variable /rowNumber/.
 */
...
/* These are the new elements that will be created */
var newTrElement = null, newTdElement = null;

/* Create the new row that will contain the updated data */
newTrElement = document.createElement('tr'),
newTrElement.setAttribute('id', 'row_' + rowNumber);
for (var i = 0, il = updateData.length; i < il;) {
    newTdElement = document.createElement('td'),
    newTdElement.setAttribute('id', 'r_' + rowNumber + 'col_' + i);
    newTdElement.innerHTML = updateData[i++];
    newTrElement.appendChild(newTdElement);
}
/* Update the record */
$('theTable').replaceChild(newTrElement, oldTrElement);

The options for updating are to walk the row and change the individual cells, or replace a whole row of data. Replacing the whole row is the better solution. All that is left is to show the simplest method for deleting data. This example shows the method in action for deleting a section of the table:

/* This is the element holding the data to delete */
var tbodyElement = $('tbodyOne'),
var il = tbodyElement.childNodes.length;
/* Loop until there are no more childNodes left */
while (il--)
    tbodyElement.removeChild(tbodyElement.firstChild);

As you can see, the simplest solution is to remove the firstChild from the section repeatedly until there are no firstChild nodes left. It would not make sense to try a different approach for this, as any tree traversal techniques are much slower than this method.

All of this discussion on dynamic table manipulation is important. It is hard to apply Ajax techniques to a table if you are unfamiliar with good ways to access tables and manipulate them. Different problems will require different solutions. Hopefully, the methods discussed here provide enough different solutions to get you on the right path, if nothing else.

Ajax and Tables

So the question is, how should Ajax and tables be combined? The answer, of course, depends on what you are doing with the table. If the Ajax request is to update one row, getting a fully formatted table is not the answer. But if a whole chunk of a table needs to be replaced, the formatted chunk of table does not sound so bad.

You also need to take into account whether the Ajax application needs to be browser-compliant. An application that must run on Internet Explorer cannot have data loaded into a <tbody> element’s innerHTML property, so you must take a different approach in this case.

Another thing to consider is how the data will be transported to the client. For loading tables with data, JavaScript Object Notation (JSON) can be an excellent solution for the updated data in a single row, whereas an XHTML document would be better for a whole table.

Thus far, we have examined how to pull Ajax data and how to manipulate a table. Now we will discuss the practical application of these techniques.

Sorting Tables

As applications become more commonplace on the Web, their response time and speed will have to improve, or they will be doomed to failure. A common object to find in an application is some sort of table filled with data that may or may not need to be manipulated. One thing that is expected is that the table should be self-sorting; that is, clicking on a column heading will sort the data in either ascending or descending order based on that column’s data.

You can accomplish this kind of functionality in two ways. The traditional method was to send back for the server on every column click, let the server do the sorting, and refresh the whole page with the newly sorted table. As developers became more sophisticated, they turned their sights toward letting the client do all the work. This method lets JavaScript do all the heavy lifting, thereby keeping the browser from flickering on the new page load and, in many cases, speeding up the action.

JavaScript Sorting

JavaScript sorting is aimed at keeping the load on the client and not on the server. Why would we want to do this? As more people hit a site or application, server speed and response suffer. To keep this to a minimum, developers try to keep most of the work on the client instead of burdening the server with more requests. This same line of thinking applies to any databases used to serve up the data in the tables.

The first part of sorting anything—whether it is an array, collection, table, or something similar—is to be able to compare two values and check for three different states: greater than, less than, and equal to. Before we can make these comparisons, however, we need a way to compare text within a table data element. Comparing text is essentially the same as comparing numbers, in a roundabout sort of way. Complicating matters are child nodes contained within the <td> element. For example:

<td>Ideally we would hope to get <span class="highlight">all</span> of this
text in our <b>search</b></td>

To avoid any errors or complications that could occur, first we need a way to normalize any data contained in the <td> elements we need to compare. Example 8-2 shows a simple normalize function.

Example 8-2. A function to normalize data in an element and its childNodes

/**
 * This function, normalizeElement, takes the passed /p_element/ and strips out
 * all element tags, etc. from the node and any /childNodes/ the element may
 * contain, returning a string with only the text data that was held in the
 * element.
 *
 * @param {Node} p_element The element that is to be normalized.
 * @return Returns the normalized string without element tags or extra whitespace.
 * @type String
 */
function normalizeElement(p_element) {
    /* The variable that will hold the normalized string. */
    var normalized = '';

    /* Loop through the passed element's /childNodes/ to normalize them as well */
    for (var i = 0, il = p_element.childNodes.length; i < il; i++) {
        /* The child element to check */
        var el = p_element.childNodes[i];

        /* Is this node a text node or a cdata section node? */
        if (el.nodeType == document.TEXT_NODE ||
                el.nodeType == document.CDATA_SECTION_NODE ||
                el.nodeType == document.COMMENT_NODE)
            normalized += el.NodeValue;
        /* Is this node an element node and a <br> element? */
        else if (el.nodeType = document.ELEMENT_NODE && el.tagName == 'BR')
            normalized += ' ';
        /* This is something to normalize */
        else
            normalized += normalizeElement(el);
    }
    return (stripSpaces(normalized));
}

/*
 * These regular expressions are used to strip unnecessary white space from
 * the string.
 */
var endWhiteSpace = new RegExp("^\s*|\s*$", "g");
var multWhiteSpace = new RegExp("\s\s+", "g");

/**
 * This function, stripSpaces, takes the passed /p_string/ variable and using
 * regular expressions, strips all unnecessary white space from the string
 * before returning it.
 *
 * @param {String} p_string The string to be stripped of white space.
 * @return Returns the passed string stripped of unnecessary white space.
 * @type String
 */
function stripSpaces(p_string) {
    p_string = p_string.replace(multWhiteSpace, ' '),
    p_string = p_string.replace(endWhiteSpace, ''),
    return (p_string);
}

Warning

The normalize code uses DOM standard constants that correspond to an element’s nodeType property. These constants, however, are not defined in some browsers (such as Internet Explorer), so the developer must define them. For example:

/* This code is necessary for browsers that do not define DOM constants */
if (document.ELEMENT_NODE == null) {
    document.ELEMENT_NODE = 1;
    document.ATTRIBUTE_NODE = 2;
    document.TEXT_NODE = 3;
    document.CDATA_SECTION_NODE = 4;
    document.ENTITY_REFERENCE_NODE = 5;
    document.ENTITY_NODE = 6;
    document.PROCESSING_INSTRUCTION_NODE = 7;
    document.COMMENT_NODE = 8;
    document.DOCUMENT_NODE = 9;
    document.DOCUMENT_TYPE_NODE = 10;
    document.DOCUMENT_FRAGMENT_NODE = 11;
    document.NOTATION_NODE = 12;
}

Now, we need to decide what kind of sorting algorithm we should use on the table. I’ll just skip most of the computer science involved in determining a good algorithm, and instead will make two simple comments. One, the quick sort is the fastest search algorithm that is relatively straightforward to implement. Two, for the Web, the insertion sort is the best choice of algorithms.

Why should we not use a quick sort for our sort algorithm if it is the fastest? The easiest answer for me to give is that generally, the data displayed in an Ajax application is not going to have hundreds or thousands of records that will need to be sorted. The quick sort is an excellent algorithm to implement on a desktop application, or wherever you have a large number of records to display. Using the quick sort for a table with 30 records, however, would be like attempting to crush a bug by backing over it with a car.

The insertion sort works using a basic algorithm, but it works quickly on smaller data sets and does not require any recursion. The algorithm would generally use two lists (a source list and a final list); however, to save memory, an in-place sort is used most of the time. This works by moving the current item being sorted past the items already sorted and repeatedly swapping it with the preceding item until it is in place. Figure 8-3 shows the algorithm in action for a small list of numbers.

The insertion sort in action on a small list of numbers

Figure 8-3. The insertion sort in action on a small list of numbers

Example 8-3 shows how you should implement this in code.

Example 8-3. A simple implementation of an insertion sort

/**
 * This function, insertionSort, takes an array of data (/p_dataArray/) and sorts
 * it using the /Insertion/ sort algorithm.  It then returns the sorted array.
 *
 * @param {Array} p_dataArray The array of values to sort.
 * @return Returns the sorted array of values.
 * @type Array
 */
function insertionSort(dataArray) {
    var j, index;

    /* Loop through the array to sort each value */
    for (var i = 0, il = dataArray.length; i < il; i++) {
        index = dataArray[i];
        j = i;
        /* Move the /dataArray/ index to the place of insertion */
        while ((j > 0) && (dataArray[j - 1] > index)) {
            dataArray[j] = dataArray[j - 1];
            j -= 1;
        }
        /* Move the current /dataArray/ index to the insertion location */
        dataArray[j] = index;
    }
    return (dataArray);
}

Keep in mind that this is a basic insertion sort algorithm that sorts only an array of values. Our code for sorting a table will be slightly more complicated due to the nature of moving table elements around dynamically with the DOM. This is also why an insertion sort is easier to implement than a shell sort, but complexity aside, the shell sort would be optimal for smaller data sets.

We now have the basics for putting together a table sort. Before we do this, though, we need to discuss how we should build a table when it will contain the functionality for dynamic sorting. The parts of the table that we want sorted are the <tbody> elements contained in the table. Only one <tbody> block should be sorted at a time, though; the line of thinking here is that <tbody> elements separate blocks of table data that are similar in nature, and it does not make sense to sort data that is not related to other data. We also never want to sort the <thead> and <tfoot> blocks of the table, as they should be constant in nature.

Now we need to decide what functionality our sort should provide. The most common approach is that the sort will activate on a user’s click of a <th> element in the table. The first click will sort the corresponding rows in ascending order, and a second click will sort the rows in descending order. All subsequent clicks will reverse the sort for that column of data. Example 8-4 takes the pieces we discussed and implements a basic table sort using the insertion sort.

Example 8-4. Sorting tables using the insertion sort

/* This code is necessary for browsers that do not define DOM constants */
if (document.ELEMENT_NODE == null) {
    document.ELEMENT_NODE = 1;
    document.ATTRIBUTE_NODE = 2;
    document.TEXT_NODE = 3;
    document.CDATA_SECTION_NODE = 4;
    document.ENTITY_REFERENCE_NODE = 5;
    document.ENTITY_NODE = 6;
    document.PROCESSING_INSTRUCTION_NODE = 7;
    document.COMMENT_NODE = 8;
    document.DOCUMENT_NODE = 9;
    document.DOCUMENT_TYPE_NODE = 10;
    document.DOCUMENT_FRAGMENT_NODE = 11;
    document.NOTATION_NODE = 12;
}

/**
 * This class, tableSort, is created as a class and not just an object so that the
 * page may have more than one table with the ability to sort, without having to
 * go to a lot of trouble in code to keep them separate within the logic itself.
 *
 * @requires Class#create
 * @see Class#create
 */
var tableSort = Class.create( );
tableSort.prototype = {
    /**
     * /multipleWS/ will hold the regular expression to eliminate multiple whitespace
     * in a string.
     * @private
     */
    multipleWS: '',
    /**
     * /endWS/ will hold the regular expresssion to eliminate whitespace at the end
     * of the string.
     * @private
     */
    endWS: '',
    /**
     * /tblElement/ will hold the table's <body> element that is to be sorted.
     * @private
     */
    tblElement: null,
    /**
     * /lastColumn/ will hold the last column sorted by the user.
     * @private
     */
    lastColumn: null,
    /**
     * /reverseSort/ will contain an array of columns of the table and will keep
     * track of the sort direction the table should take.
     * @private
     */
    reverseSort: new Array( ),
    /**
     * This method, initialize, is the constructor for the /tableSort/ class.  It
     * seeds values into the private members of the class based on the passed
     * <table> element /p_id/ variable and constant regular expressions.
     *
     * @param {String} p_id The id attribute of the table to sort.
     * @constructor
     */
    initialize: function(p_id) {
        this.tblElement = $(p_id).getElementsByTagName('tbody')[0];
        this.endWS = new RegExp("^\s*|\s*$", "g");
        this.multipleWS = new RegExp("\s\s+", "g");
    },
    /* Normalize the data contained in the element into a single string */
    /**
     * This method, normalizeElement, takes the passed /p_element/ and strips out
     * all element tags, etc. from the node and any /childNodes/ the element may
     * contain, returning a string with only the text data that was held in the
     * element.
     *
     * @param {Node} p_element The element that is to be normalized.
     * @return Returns the normalized string without element tags or extra
     *     whitespace.
     * @type String
     * @private
     * @see #sortColumn
     */
    normalizeElement: function(p_element) {
        /* The variable to hold the normalized string */
        var normalize = '';

        /*
         * Loop through the passed element's /childNodes/ to normalize them
         * as well.
         */
        for (var i = 0, il = p_element.childNodes.length; i < il;) {
            /* The child element to check */
            var el = p_element.childNodes[i++];

            /* Is this node a text node, CDATA node, or comment node? */
            if (el.nodeType == document.TEXT_NODE ||
                    el.nodeType == document.CDATA_SECTION_NODE ||
                    el.nodeType == document.COMMENT_NODE)
                normalize += el.nodeValue;
            /* Is this node an element node and a <br> element? */
            else if (el.nodeType = document.ELEMENT_NODE && el.tagName == 'BR')
                normalize += ' ';
            /* This is something to normalize */
            else
                normalize += this.normalizeElement(el);
        }
        return (this.stripSpaces(normalize));
    },
    /* Strip any unnecessary whitespace from the string */
    /**
     * This method, stripSpaces, takes the passed /p_string/ variable and using
     * regular expressions, strips all unnecessary whitespace from the string
     * before returning it.
     *
     * @param {String} p_string The string to be stripped of whitespace.
     * @return Returns the passed string stripped of unnecessary whitespace.
     * @type String
     * @private
     * @see #normalizeElement
     */
    stripSpaces: function(p_string) {
        p_string = p_string.replace(this.multipleWS, ' '),
        p_string = p_string.replace(this.endWS, ''),
        return (p_string);
    },
    /**
     * This method, compareNodes, takes two node values as parameters (/p1/ and
     * /p2/) and compares them to each other.  It sends back a value based on
     * the following:
     *
     *  1 - /p1/ is greater than /p2/
     *  0 - /p1/ is equal to /p2/
     * -1 - /p1/ is less than /p2/
     *
     * @param {String} p1 The first node value in the test.
     * @param {String} p2 The second node value in the test.
     * @return Returns a value based on the rules in the description of this method.
     * @type Integer
     * @private
     * @see #sortColumn
     */
    compareNodes: function(p1, p2) {
        /* Convert the values, if possible, to Floats */
        var f1 = parseFloat(p1), f2 = parseFloat(p2);

        if (!isNaN(f1) && !isNaN(f2)) {
        /* Are both values numbers? (they are faster to sort) */
            p1 = f1;
            p2 = f2;
        }
        /* Are the two values the same? */
        if (p1 == p2)
            return 0;
        /* Is the first value larger than the second value? */
        if (p1 > p2)
            return 1;
        return -1;
    },
    /**
     * This method, sortColumn, sorts the passed /p_column/ in a direction based on
     * the passed /p_defaultDirection/, moving data in all sibling columns when it
     * sorts.
     *
     * @param {Integer} p_column The column index that is to be sorted.
     * @param {Integer} p_defaultDirection The direction the sort should take (1 for
     *     !default).
     * @see #normalizeElement
     * @see #compareNodes
     */
    sortColumn: function(p_column, p_defaultDirection) {
        var tempDisplay = this.tblElement.style.display;
        var j, index;

        /* Set a default direction if one is not passed in */
        if (defaultDirection == null)
            defaultDirection = 0;
        /*
         * Has the passed column been sorted yet? - if not, set its initial sorting
         * direction.
         */
        if (this.reverseSort[p_column] == null)
            this.reverseSort[p_column] = p_defaultDirection;
        /*
         * Was the lastColumn sorted the passed column? - if it is, reverse the sort
         * direction.
         */
        if (this.lastColumn == p_column)
            this.reverseSort[p_column] = !this.reverseSort[p_column];
        this.lastColumn = p_column;
        /*
         * Hide the table during the sort to avoid flickering in the table and
         * misrendering issues found in Netscape 6
         */
        this.tblElement.style.display = 'none';

        /* Loop through each row of the table and compare it to other row values */
        for (var i = 0, il = this.tblElement.rows.length; i < il; i++) {
            index = this.normalizeElement(this.tblElement.rows[i].cells[p_column]);
            j = i;
            /*
             * Sort through all of the previous records and insert any of these rows
             * in front of the current row if such action is warranted.
             */
           while (j > 0) {
                var doSort = this.compareNodes(this.normalizeElement(
                    this.tblElement.rows[j - 1].cells[p_column]), index);
                /*
                 * Doing the opposite sort direction for alternating clicks is as
                 * simple as negating the current value for the sort order,
                 * turning Ascending to Descending and back again.
                 */
                if (this.reverseSort[p_column])
                    doSort = -doSort;
                if (doSort > 0)
                    this.tblElement.insertBefore(this.tblElement.rows[j],
                        this.tblElement.rows[j - 1]);
                j -= 1;
            }
        }

        /* Set the table's display to what it was before the sort */
        this.tblElement.style.display = tempDisplay;
        /* Do not let the onclick event know that it worked */
        return (false);
    }
};

The tableSort object requires that it be instantiated after the browser has created the table, such as:

var myTableSort = new tableSort('myTable'),

Each table header element must also contain an onclick event that calls the tableSort object’s sortColumn( ) method and passes it the index of the column. For example, to sort the first column the code would look like this:

<th id="col1" onclick="myTableSort.sortColumn(0)">Column One</th>

It is as simple as that. The client takes over the burden of sorting the table. The sort is fast, and best of all, the client does not have to refresh all of its content on every user request for a new sort. The drawback to implementing the insertion sort is that speed will degrade as the data set gets larger, which is something the developer should always keep in mind.

Sorting with Ajax

Sorting a table using an Ajax technique is similar to the old way of sorting tables, which, of course, was to refresh the whole table each time there was a sort request. The difference between the old technique and Ajax is that the whole page does not have to be refreshed with every request; only the table does. Why would a developer want to do this instead of using the JavaScript approach we just discussed? One word: performance. The client’s performance cannot match the server’s performance, especially when the client is using JavaScript while the server can use much more powerful scripting.

Using what we already discussed, we can send the table to the client with whatever sort order is requested. The client should send to the server the id of the table, the name of the column that should be sorted, and the direction of that sort (ascending or descending). The server can, in most cases, spit out a table (especially a larger table) much quicker than the client could have one sorted.

Assume that we are to sort the following table:

<table id="premLeague" summary="This table represents the 2005-06 Premier League
        standings.">
    <caption>2005-06 Premier League</caption>
    <thead>
        <tr>
            <th id="team">Team</th>
            <th id="points">Points</th>
            <th id="won">Won</th>
            <th id="drew">Drew</th>
            <th id="lost">Lost</th>
            <th id="gs" abbr="goals scored">GS</th>
            <th id="ga" abbr="goals against">GA</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td headers="team">Chelsea</td>
            <td headers="points">91</td>
            <td headers="won">29</td>
            <td headers="drew">4</td>
            <td headers="lost">5</td>
            <td headers="gs">72</td>
            <td headers="ga">22</td>
        </tr>
        <tr>
            <td headers="team">Manchester United</td>
            <td headers="points">83</td>
            <td headers="won">25</td>
            <td headers="drew">8</td>
            <td headers="lost">5</td>
            <td headers="gs">72</td>
            <td headers="ga">34</td>
        </tr>
        ...
        <tr>
            <td headers="team">Sunderland</td>
            <td headers="points">15</td>
            <td headers="won">3</td>
            <td headers="drew">6</td>
            <td headers="lost">29</td>
            <td headers="gs">26</td>
            <td headers="ga">69</td>
        </tr>
    </tbody>
</table>

Notice that the table has an id attribute that uniquely identifies it, and that the header elements have identified the column names. All we need to add to the header elements now is an onclick event that will call our Ajax request. Something like this should do:

onclick="sortTable('premLeague', this.id);"

Now we do a little housekeeping before the Ajax request can be sent:

$('premLeague').lastColumn = '';

Like the JavaScript sort, the lastColumn property is added to the table when the page loads, and then is checked and set with every onclick event. Once the parameters are set, the Ajax call can be performed, and our functions for the completion of the request should be built. Example 8-5 shows how the Ajax would be called.

Example 8-5. The sortTable( ) method modified for Ajax

/**
 * This function, sortTable, takes the passed /p_tableId/ and /p_columnId/
 * variables and sends this information to the server so it can do the
 * appropriate sort that is returned to the client.  The data from the server
 * is the whole table, because it is faster to build the whole table on the
 * server.
 *
 * @param {String} p_tableId The id of the table to sort.
 * @param {String} p_columnId The id of the column that is to be sorted.
 */
function sortTable(p_tableId, p_columnId) {
    /* Get the direction the sort should go in */
    var sortDirection = (($(p_tableId).lastColumn == p_columnId) ? 1 : 0);
    /* Create the queryString to send to the server */
    var queryString = 'tableId=' + p_tableId + '&columnId=' + p_columnId +
        '&sort=' + sortDirection;

    /* Record the column that is sorted */
    $(p_tableId).lastColumn = p_columnId;
    /*
     * Make the XMLHttpRequest to the server, and place the /responseText/ into
     * the /innerHTML/ of the table wrapper (/parentNode/).
     */
    new Ajax.Request('sortTable.php', {
        parameters: queryString,
        method: 'post',
        onSuccess: function(xhrResponse) {
            $(tableId).parentNode.innerHTML = xhrResponse.responseText;
        },
        onFailure: function(xhrResponse) {
            $(tableId).parentNode.innerHTML = xhrResponse.statusText;
        }
    }
}

The onFailure property is taken care of inline during the Ajax request, as is the onSuccess property. The onSuccess property needs the tableId that is passed to sortTable( ), and the rest is straightforward—it takes the responseText from the Ajax response and sets it to the innerHTML of the <div> element that is the parent of the table itself. All other building (the onclick events and the column names) is taken care of on the server side when the table is rebuilt.

The server would have to accept the parameters and then build a SQL request based on what was passed. The table would be rebuilt as a string and then sent back to the client as a regular text response. The server code would look something like Example 8-6.

Example 8-6. The PHP code that could be used to create a table for a response to a client

<?php
/**
 * Example 8-6.  The PHP code that could be used to create a table for a response
 * to a client.
 */

/**
 * The Zend Framework Db.php library is required for this example.
 */
require_once('Zend/Db.php'),
/**
 * The generic db.php library, containing database connection information such
 * as username, password, server, etc., is required for this example.
 */
require('db.inc'),

/* Were all of the necessary values passed from the client? */
if (isset($_REQUEST['tableId']) && isset($_REQUEST['columnId']) &&
        isset($_REQUEST['sort'])) {
    /* Create the parameter array to connect to the database */
    $params = array ('host' => $host,
                     'username' => $username,
                     'password' => $password,
                     'dbname' => $db);

    try {
        /* Create a connection to the database */
        $conn = Zend_Db::factory('PDO_MYSQL', $params);
        /* Build the SQL string based on the parameters that were passed */
        $sql = sprintf('SELECT team, points, won, drew, lost, gs, ga FROM '
            .'premLeague WHERE year = 2005%s',
            (($_REQUEST['columnId'] != '') ? ' ORDER BY '
            .$_REQUEST['columnId'].(($_REQUEST['sort'] == 1) ? ' DESC' :
            ' ASC') : ''));
        /* Get the results from the database query */
        $result = $conn->query($sql);
        /* Are there results with which to build a table? */
        if ($rows = $result->fetchAll( )) {
            $id = $_REQUEST['tableId'];

            /* Build the beginning and header of the table */
            $xml .= '<table id="'.$id.'" summary="This table represents the '
                   .'1995-96 Premier League standings.">';
            $xml .= '<caption>1995-96 Premier League</caption>';
            $xml .= '<thead>';
            $xml .= '<tr>';
            $xml .= '<th id="team" onclick="sortTable(''.$id.'', this.id);">'
                   .'Team</th>';
            $xml .= '<th id="points" onclick="sortTable(''.$id.'', this.id);">'
                   .'Points</th>';
            $xml .= '<th id="won" onclick="sortTable(''.$id.'', this.id);">'
                   .'Won</th>';
            $xml .= '<th id="drew" onclick="sortTable(''.$id.'', this.id);">'
                   .'Drew</th>';
            $xml .= '<th id="lost" onclick="sortTable(''.$id.'', this.id);">'
                   .'Lost</th>';
            $xml .= '<th id="gs" abbr="goals scored" onclick="sortTable(''
                   .$id.'', this.id);">GS</th>';
            $xml .= '<th id="ga" abbr="goals against" onclick="sortTable(''
                   .$id.'', this.id);">GA</th>';
            $xml .= '</tr>';
            $xml .= '</thead>';
            $xml .= '<tbody>';
            /*
             * Loop through the rows of the result set and build the table data
             * in the tbody element.
             */
            foreach($rows in $row) {
                $xml .='<tr>';
                $xml .= '<td headers="team">'.$row['team'].'</td>';
                $xml .= '<td headers="points">'.$row['points'].'</td>';
                $xml .= '<td headers="won">'.$row['won'].'</td>';
                $xml .= '<td headers="drew">'.$row['drew'].'</td>';
                $xml .= '<td headers="lost">'.$row['lost'].'</td>';
                $xml .= '<td headers="gs">'.$row['gs'].'</td>';
                $xml .= '<td headers="ga">'.$row['ga'].'</td>';
                $xml .='</tr>';
            }
            /* Finish up the table to be sent back */
            $xml .= '</tbody>';
            $xml .= '</table>';
        }
    } catch (Exception $e) {
        $xml .= '<div>There was an error retrieving the table data.</div>';
    }
}
/* Send the XHTML table to the client */
print($xml);
?>

And the Winner Is...

There is no way to definitively say which method is better or faster. Sometimes it will be better to use the Ajax method—maybe the server is old and could not handle all of the Ajax requests, or maybe the data sets are so small that it does not make sense to do an Ajax request. If the data sets are larger, the developer is faced with two choices. He can either rewrite the sort code and use a better algorithm, or use Ajax and let the server (which is much better equipped to handle large data sets) do all of the sorting and table building.

JavaScript sorting relies on heavy manipulation of the DOM document that holds the table. This can be expensive, and it could lock up the client for several seconds in the process. The alternative does have its benefits. Using Ajax to do the table sort means the call can be asynchronous to the server, and the user is free to do other things on the client until the sort is complete. Clearly, it is in the developer’s best interests to at least indicate to the user that the client is doing something. Besides that, however, there is really nothing else adverse about using Ajax over straight JavaScript sorting.

In the next several sections, it will become clearer why Ajax is the better choice for table sorting. Even after you read these sections, some of you will cling to your JavaScript sorting code, saying, “Look how beautiful this code is, and how slick it works on the client.” There is a coolness factor in doing the sort using JavaScript. Ajax is not about coolness in this case; it is about practicality, efficiency, and the speed of the application.

Tables with Style

Left to their own devices, tables are boring objects. We have all seen the old tables that were prevalent on the Web years ago, an example of which appears in Figure 8-4. Using CSS to change the shape, size, and color of the table borders helped to change the table’s general appearance. Going further with CSS, alternating colors for alternating rows and making the header and footer more distinct helped to make tables more readable on the client.

The default table from the browser is boring

Figure 8-4. The default table from the browser is boring

It became trickier to keep all of this style straight as the user dynamically altered the table in the client. Keeping alternating rows straight in an Ajax sorted table is simple. The whole table is regenerated so that any CSS that was placed on the rows before the sort is put on the new row order as though nothing has changed. Keeping the style of the table with a JavaScript sort is another story.

Keeping Style with Sorts

CSS has brought us a long way from tables such as the one in Example 8-3; consider the following CSS rules and imagine how they would style that table:

table {
    border-collapse: collapse;
    border: 3px solid #000;
    width: 500px;
}

caption {
    font-weight: bold;
}

tr {
    background-color: #fff;
    color: #000;
    margin: 0;
}

tr.alternate {
    background-color: #dde;
    color: #000;
}

th {
    background-color: #007;
    border: 1px solid #000;
    border-bottom: 3px solid #000;
    color: #fff;
    padding: 2px;
}

td {
    border: 1px solid #000;
    padding: 2px;
}

This CSS creates a 3-pixel-thick black border around the outside of the table and under the table header. All of the other rows and columns are divided by a 1-pixel-thick black border. The table header has its background set to a deep blue and the text to white. Finally, the default row is set to a white background with black text, and a row with the alternative class attribute set would have a gray-blue background with black text, as shown in Figure 8-5. Alternating the color for every other column keeps the rows easier to read. But there is a problem, right?

The Premier League table with CSS rules attached

Figure 8-5. The Premier League table with CSS rules attached

When we sort by any of the columns, the rows do not keep their alternating pattern. When the row is inserted in a new spot in the table body, everything about that row goes with it. So, how do we solve this problem? We need another method in our sortTable object to style the table, like this:

/**
 * This method, styleTable, loops through all of the table rows in the table and
 * removes the /alternate/ class from the row before checking to see whether the
 * row is one of the alternating rows that should have the class.
 *
 * @private
 */
styleTable: function( ) {
    /* Loop through all of the table rows */
    for (var i = 0, il = this.tblElement.rows.length; i < il; i++) {
        /* This is the current row */
        var tblRow = this.tblElement.rows[i];

        Element.removeClassName(tblRow, 'alternate'),
        /* Is this an alternate row? */
        if ((i % 2) != 0)
            Element.addClassName(tblRow, 'alternate'),
    }
}

The styleTable( ) method relies on two methods from the Prototype library Element object: removeClassName( ) and addClassName( ). I could have written code to add and remove classNames from the rows, but since Prototype already had these, I did not see the point. The styleTable( ) method needs to be called just before the display on the table is changed, like this:

/* Add the needed style to the table */
this.styleTable( );
/* Set the table's display to what it was before the sort */
this.tblElement.style.display = tempDisplay;

Now, our table keeps the CSS rules that were originally applied. The developer could get pretty fancy with the styleTable( ) method if she so desired. It would not take much to change the column that was selected so that it stood out more than the others. This would make it more accessible to a person trying to read the table.

As far as Ajax goes, the classes would just have to be added to the table as it was being built. The addition of only a couple of lines does the trick, like this:

$i = 0;
foreach ($rows in $row) {
    $xml = sprintf('%s<tr%s>', $xml, ((($i++ % 2) != 0) ? 'class="alternate"': ''));

This is yet another reason Ajax sorting may be a better choice—it requires even less processing on the client side. This, in particular, is nothing for the server to do, because it already is creating a bunch of strings to concatenate together.

Table Pagination

Table pagination follows the same principle we saw in Chapter 7 in the “Paged Navigation” section. A table that is longer than one page on a user’s screen is large by web standards, and you should probably break it up. The technique is the same as before: display only a certain number of rows to the user and provide a navigation list at the bottom of the table. Once again, the question is, should the table be broken up on the client or on the server?

In either case, we will utilize the XHTML <table> elements to make our job a little easier. The server will do more of the work, as it must keep track of how many records are in what grouping. The groupings are what’s important, and to make them, we will be using multiple <tbody> elements—one for every page, actually. For example:

<table id="premLeague" summary="This table represents the 2005-06 Premier
        League standings.">
    <caption>2005-06 Premier League</caption>
    <thead>
        <tr>
            <th id="team" onclick="premLeagueSort.sortColumn(0)">Team</th>
            <th id="points" onclick="premLeagueSort.sortColumn(1)">Points</th>
            <th id="won" onclick="premLeagueSort.sortColumn(2)">Won</th>
            <th id="drew" onclick="premLeagueSort.sortColumn(3)">Drew</th>
            <th id="lost" onclick="premLeagueSort.sortColumn(4)">Lost</th>
            <th id="gs" abbr="goals scored" onclick="premLeagueSort.sortColumn(5)">
                GS
            </th>
            <th id="ga" abbr="goals against" onclick="premLeagueSort.sortColumn(6)">
                GA
            </th>
        </tr>
    </thead>
    <tbody id="p1">
        <tr>
            <td headers="team">Chelsea</td>
            <td headers="points">91</td>
            <td headers="won">29</td>
            <td headers="drew">4</td>
            <td headers="lost">5</td>
            <td headers="gs">72</td>
            <td headers="ga">22</td>
        </tr>
        <tr class="alternate">
            <td headers="team">Manchester United</td>
            <td headers="points">83</td>
            <td headers="won">25</td>
            <td headers="drew">8</td>
            <td headers="lost">5</td>
            <td headers="gs">72</td>
            <td headers="ga">34</td>
        </tr>
        ...
    </tbody>
    <tbody id="p2">
        <tr>
            <td headers="team">Everton</td>
            <td headers="points">50</td>
            <td headers="won">14</td>
            <td headers="drew">8</td>
            <td headers="lost">16</td>
            <td headers="gs">34</td>
            <td headers="ga">49</td>
        </tr>
        ...
    </tbody>
</table>

The id attribute identifies the page that the <tbody> represents. JavaScript or Ajax must do the rest.

Making Pages with JavaScript

The same principle we applied to the paged navigation will apply here. The server will send the entire table to the client, and when the page has loaded, the first “page” of the table will be displayed through code. All of the <tbody> elements will start off hidden using CSS. For example:

table#premLeague tbody {
    display: none;
}

The JavaScript to make the first page appear is minor:

<body onload="$('p1').style.display = 'table-row-group';">

Note that I am setting the display to the value table-row-group and not the common block value. This is because if I set the <tbody> to block, it will no longer align to any <thead> or <tfoot> element that is present.

The part we need to add is the list of pages that will serve as navigation for the table. The server will have also taken care of this list’s initial state. The client’s job is to change its appearance, as well as the <tbody> that should show when it is clicked. Assuming that our table contains five pages of data, the navigation list would look like this:

<div id="tableListContainer">
    <ul id="tableList">
        <li id="l1" onclick="turnDataPage('premLeague',
            this.childNodes[0].nodeValue);">1</li>
        <li id="l2" onclick="turnDataPage('premLeague',
            this.childNodes[0].nodeValue);">2</li>
        <li id="l3" onclick="turnDataPage('premLeague',
            this.childNodes[0].nodeValue);">3</li>
        <li id="l4" onclick="turnDataPage('premLeague',
            this.childNodes[0].nodeValue);">4</li>
        <li id="l5" onclick="turnDataPage('premLeague',
            this.childNodes[0].nodeValue);">5</li>
    </ul>
</div>

The onclick event fires off to the turnDataPage( ) function, shown in Example 8-7.

Example 8-7. A function to simulate pages of table data

/**
 * This function, turnDataPage, acts on whatever table (/p_tableId/) is passed,
 * which is the beginning of making this "table independent".  It turns off
 * the display of all of the <tbody> elements associated with the table, and
 * then displays the required one based on the passed /p_pageNumber/ variable.
 * Finally, it changes the page number list item that is to be highlighted in
 * association with the table page.
 *
 * @param {String} p_tableId The id of the table to paginate.
 * @param {Integer} p_pageNumber The number of the page that should be displayed.
 * @see Element#setStyle
 * @see Element#removeClassName
 * @see Element#addClassName
 */
function turnDataPage(p_tableId, p_pageNumber) {
    /* Get a list of the <tbody> elements in the table */
    var tbodies = $(p_tableId).getElementsByTagName('tbody'),

    /* Loop through the list of <tbody> elements, and hide each one */
    for (var i = 0, il = tbodies.length; i < il; i++)
        tbodies[i].style.display = 'none';
    /* Display the <tbody> element with the correct page number */
    $('p' + p_pageNumber).style.display = 'table-row-group';
    /* Get a list of the <li> elements in the page navigation menu */
    var tableListElements = $('l' +
        p_pageNumber).parentNode.getElementsByTagName('li'),

    /* Loop through the list of <li> elements, and make each one inactive */
    for (var i = 0, il = tableListElements.length; i < il; i++)
        Element.removeClassName(tableListElements[i], 'active'),
    /* Activate the <li> element with the correct page number */
        Element.addClassName($('l' + p_pageNumber), 'active'),
}

With this function, we should change the onload event to the following so that the first page number is highlighted:

<body onload="turnDataPage('premLeague', 1);">

Of course, the turnDataPage( ) function expects another CSS rule to be defined as well:

li.active {
    font-weight: bold;
}

I will discuss the advantages and disadvantages of a straight JavaScript approach to table pagination after we discuss the Ajax method.

Warning

Internet Explorer version 6 and earlier do not support the CSS display value table-row-group that is used in Example 8-7. For these browsers, you should use the value of block instead.

The only problem with pagination such as this is that it is not accessible to browsers that do not support JavaScript. To make it accessible, we simply need to add links to our list of pages in the navigation list, like this:

<div id="tableListContainer">
    <ul id="tableList">
        <li id="l1"><a href="dataTable.php?page=1"
            onclick="turnDataPage('premLeague',
            this.parentNode.childNodes[0].nodeValue);">1</a></li>
        <li id="l2"><a href="dataTable.php?page=2"
            onclick="turnDataPage('premLeague',
            this.parentNode.childNodes[0].nodeValue);">2</a></li>
        <li id="l3"><a href="dataTable.php?page=3"
            onclick="turnDataPage('premLeague',
            this.parentNode.childNodes[0].nodeValue);">3</a></li>
        <li id="l4"><a href="dataTable.php?page=4"
            onclick="turnDataPage('premLeague',
            this.parentNode.childNodes[0].nodeValue);">4</a></li>
        <li id="l5"><a href="dataTable.php?page=5"
            onclick="turnDataPage('premLeague',
            this.parentNode.childNodes[0].nodeValue);">5</a></li>
    </ul>
</div>

Figure 8-6 shows this table with the paged navigation. Now when a client does not have JavaScript, the link will fire off and the data can still be accessed, provided that the server-side scripting is programmed to handle this scenario.

The Premier League table with paged navigation

Figure 8-6. The Premier League table with paged navigation

Ajax Table Pagination

Ajax table pagination also follows the same principles as Ajax paged navigation: namely, give the client only as much data as it needs to create one page of the table. As the pages are requested, the table builds up more <tbody> elements until an Ajax request is no longer needed and the straight JavaScript method can take over.

Example 8-8 shows the turnDataPage( ) function revised for Ajax.

Example 8-8. The turnDataPage( ) function modified for Ajax

/**
 * This function, turnDataPage, acts on whatever table (/p_tableId/) is passed,
 * which is the beginning of making this "table independent".  It turns off the
 * display of all of the <tbody> elements associated with the table, and then
 * makes an /XMLHttpRequest/ to pull the necessary <tbody> if it does not already
 * have /childNodes/ under it.  The success of the call adds the new content and
 * then displays the required one.  Finally, it changes the page number list item
 * that is to be highlighted in association with the table page.
 *
 * @param {String} p_tableId The id of the table to paginate.
 * @param {Integer} p_pageNumber The number of the page that should be displayed.
 * @see Element#setStyle
 * @see Element#removeClassName
 * @see Element#addClassName
 */
 function turnDataPage(p_tableId, p_pageNumber) {
    /* Get a list of the <tbody> elements in the table */
    var tbodies = $(p_tableId).getElementsByTagName('tbody'),

    /* Loop through the list of <tbody> elements, and hide each one */
    for (var i = 0, il = tbodies.length; i < il; i++)
        $(tbodies[i]).setStyle({ display: 'none' });
    /* Has this page been grabbed by an XMLHttpRequest already? */
    if ($('p' + p_pageNumber).childNodes.length == 0) {
        /* Get the data from the server */
        new Ajax.Request('getTable.php', {
            method: 'post',
            parameters: 'dataPage=' + p_pageNumber,
            onSuccess: function(xhrResponse) {
                var newNode, response = xhrResponse.responseXML;

                /* Is this browser not IE? */
                if (!window.ActiveXObject)
                    newNode = document.importNode(
                        response.getElementsByTagName('tbody')[0], true);
                else
                    newNode = importNode(
                        response.getElementsByTagName('tbody')[0], true);
                $(tableId).replaceChild(newNode, $('p' + p_pageNumber));
            },
            onFailure: function(xhrResponse) {
                alert('Error: ' + xhrResponse.statusText);
            }
        });
    }
    /* Display the <tbody> element with the correct page number */
    $('p' + p_pageNumber).setStyle({ display: 'table-row-group' });
    /* Get a list of the <li> elements in the page navigation menu */
    var tableListElements = $('l' +
        p_pageNumber).parentNode.getElementsByTagName('li'),

    /* Loop through the list of <li> elements, and make each one inactive */
    for (var i = 0, il = tableListElements.length; i < il; i++)
        Element.removeClassName(tableListElements[i], 'active'),
    /* Activate the <li> element with the correct page number */
    Element.addClassName($('l' + p_pageNumber), 'active'),
}

This is similar to what we already saw with our other Ajax calls using Prototype. Here, the main difference is how we check whether content is already in the <tbody>. Because this should be a cross-browser function, we cannot check the innerHTML property like we did in Chapter 7; Internet Explorer does not support the property. Instead, we check to see whether the <tbody> has any childNodes.

Instead of even fiddling with <tbody> elements and the innerHTML property, the client will expect the server to send the contents of the <tbody> (all of the rows and corresponding columns, including the <tbody> itself) and then import those elements into a new element. Once again, for Internet Explorer, we must rely on both the DOM importNode( ) method and the importNode( ) function I introduced in Example 7-8 in Chapter 7 for browsers that do not support importNode( ) natively. After the import, we just replace the empty <tbody> that is attached to the table with our new element.

Of course, we could have avoided having to check for childNodes and import and replace elements. If the server were to send a new table with each request—a table that contains only the data for the page that is requested—we could use the innerHTML property with the responseText from the server’s response on a wrapper <div> element. In the long run, the user may request a lot of data if a lot of browsing is done on the table. The developer must weigh whether simple (and potentially faster) code is better than fewer calls to the server requesting data.

There is one argument for constantly calling on the server to give the user data. Well, two arguments, really. I will get to the second one in the next section. The first argument is that if the user is working with an Ajax application that gets data from different users potentially at the same time, getting constant data refreshes from the server on every request is a better solution. This way, the user knows that the data being viewed is constantly up-to-date versus data that is loaded once and might become stale.

Sorting Paginated Tables

Now for the second argument: if you want the table to be broken up into pages as well as dynamically sortable, you may have a dilemma. Either you must modify the methods for sorting to cycle through every <tbody> element in the table, which requires digging deeper into the DOM tree, or you can simply have the server handle the sort and send the table data page that was requested. Letting the server handle things is certainly cleaner and easier, if not faster.

To accomplish this, we must add an extra parameter to the sortTable( ) method in Example 8-5 for the current page, and we must pass it to the server. Everything else is the same for the Ajax sort. The server will create the sorted table, sending the small part of the table that the client needs, which is then put into the table wrapper’s innerHTML property as the responseText. Example 8-9 shows the changes needed.

Example 8-9. The sortTable( ) method modified for pagination sorting with Ajax

/**
 * This function, sortTable, takes the passed /p_tableId/ and /p_columnId/
 * variables and sends this information to the server so it can do the
 * appropriate sort that is returned to the client.  The data from the server
 * is the whole table, because it is faster to build the whole table on the
 * server.
 *
 * @param {String} p_tableId The id of the table to sort.
 * @param {String} p_columnId The id of the column that is to be sorted.
 * @param {Integer} p_pageNumber The number of the page to display.
 */
function sortTable(p_tableId, p_columnId, p_pageNumber) {
    /* Get the direction the sort should go in */
    var sortDirection = (($(p_tableId).lastColumn == p_columnId) ? 1 : 0);
    /* Create the queryString to send to the server */
    var queryString = 'tableId=' + p_tableId + '&columnId=' + p_columnId +
        '&sort=' + sortDirection + '&page=' + p_pageNumber;

    /* Record the column that is sorted */
    $(p_tableId).lastColumn = p_columnId;
    /*
     * Make the XMLHttpRequest to the server, and place the /responseText/ into the
     * /innerHTML/ of the table wrapper (/parentNode/).
     */
    new Ajax.Request('sortTable.php', {
        parameters: queryString,
        method: 'post',
        onSuccess: function(xhrResponse) {
            $(tableId).parentNode.innerHTML = xhrResponse.responseText;
        },
        onFailure: function(xhrResponse) {
            $(tableId).parentNode.innerHTML = xhrResponse.statusText;
        }
    }
}

I think that is the easiest solution. As I said, if you choose to go the JavaScript route, there will be a loop through all of the <tbody> elements in the table in addition to the loops through the rows in a single <tbody> element. To simplify things, I recommend looping through everything while building an array of values, and sorting that array with a simpler implementation of the insertion sort (or maybe a shell sort now). Once everything is sorted, you are left with the difficult task of getting the table rows in the correct order—all while maintaining the correct number of rows in each <tbody> element. This will be complicated and messy.

In the end, the developer wants something that works, and works fast. Using the Ajax solution for sortable paginated tables does just that.

Lists 2.0

A great deal of Chapter 7 dealt with utilizing XHTML lists for different purposes. Most of these purposes really take lists to that new level—no longer relegating the list to the boring purpose of displaying information in a vertical manner using a circle or square to delineate the items. When you use CSS and XHTML smartly, lists can become a very powerful structure in an Ajax application.

Warning

When using lists, the main thing to watch for is the purpose of the list. Once the list is used, in any way, for presentation instead of structure, it breaks the following WAI-WCAG 1.0 guideline:

  • Priority 2 checkpoint 3.6: Mark up lists and list items properly.

As we saw in Chapter 7, lists are useful for a variety of widgets and objects in web applications. However, this barely scratches the surface of what they can do.

What We’ve Already Seen

We already saw that we can use lists for navigation functionality in an Ajax application—we did not use the lists as lists at all (for the most part). So, what have we covered?

  • Lists used for navigation bars

  • Lists used for buttons in navigation

  • Lists used for drop-down menus

  • Lists used for a file menu

  • Lists used for tabs

  • Lists used for breadcrumbs

  • Lists used for links at the bottom of a page

  • Lists used for the navigation in paged content

  • Lists used for navigation trees

  • Lists used for vertical list navigation

This is already a lot of uses for a simple structure, but lists have still more uses for dynamic content, if you can believe that.

Lists for All Seasons

What we’ll discuss next ranges from fairly common to rarely seen uses of XHTML lists. I’ll put a Web 2.0 spin on the common list applications, and will hopefully spawn new development for the rarer scenarios. In any case, I will throw some Ajax into the examples where it fits, and the other examples are intended for Ajax applications where Ajax has more to do with dynamic user interaction.

Table of Contents

Anyone who has had to create any kind of online documentation knows the hassle of updating the document. The hassle mainly involves changes that must be made to the table of contents whenever a section is moved, deleted, or added. When a section is moved or deleted, numbering must shift up for any headers below the point in question. When a section is added, numbering must shift down for any headers below the point in question.

In almost all cases, lists are already used for the structure of the table of contents, and why not? A table of contents consists of only ordered or unordered lists and sublists. Nothing needs to change with this—yet. First we must concentrate on the document itself, or rather how we need to structure the document so that we can easily and dynamically generate a table of contents.

The often misused header elements (<h1>-<h6>) are where we need to focus. The misuse usually occurs with the headers not following in immediate descending order. In other words, the headers that should follow an <h1> element are <h2> elements. No <h3> elements should be direct descendants of the <h1> element. This paradigm should continue throughout the document. Also true of header elements is that they are used to convey the document’s structure; they are not used for presentation within the document. If you need to create a section of code with larger text, use appropriate markup and do not simply throw in header elements to accomplish the task.

Tip

By following proper header order, not skipping any header levels, and using the headers for structure, the developer satisfies the following WAI-WCAG 1.0 guideline:

  • Priority 2 checkpoint 3.5: Use header elements to convey document structure and use them according to specification.

The following shows how this chapter would be structured with XHTML markup:

<div id="chapter">Fun with Tables and Lists</div>
<div id="toc"></div>
<div id="content">
    <p><!-- remarks --></p>
    <h1 id="noTableLayout">Layout Without Tables</h1>
    <blockquote>
        <p><!-- remarks --></p>
        <h2 id="oldLayouts">Old Layouts</h2>
        <blockquote>
            <p><!-- remarks --></p>
        </blockquote>
        <h2 id="usingCSS">Using CSS</h2>
        <blockquote>
            <p><!-- remarks --></p>
        </blockquote>
    </blockquote>
    <h1 id="accessibleTables">Accessible Tables</h1>
    <blockquote>
        <p><!-- remarks --></p>
        <h2 id="interactingTables">Interacting with Tables</h2>
        <blockquote>
            <p><!-- remarks --></p>
        </blockquote>
        <h2 id="ajaxTables">Ajax and Tables</h2>
        <blockquote>
            <p><!-- remarks --></p>
        </blockquote>
    </blockquote>
    ...
    <h1 id="listsSeasons">Lists for All Seasons</h1>
    <blockquote>
        <p><!-- remarks --></p>
        <h2 id="tableContents">Table of Contents</h2>
        <blockquote>
            <p><!-- remarks --></p>
        </blockquote>
    </blockquote>
</div>

The document requires that all the headers have unique id attributes so that the table of contents can identify them properly. Once we know our document is properly structured, we must think about how to create a dynamic list that reflects that structure. Example 8-10 shows the JavaScript function that will do this job.

Example 8-10. A function to dynamically create a list to be used as a table of contents

/**
 * This function, createTOC, parses through an XHTML document, taking all header
 * elements within the /content/ of the document, and creating a clickable table
 * of contents to the individual headers that it finds.
 */
function createTOC( ) {
    /*
     * The table of contents is only concerned with elements contained within
     * this element.
     */
    var content = $('content'),
    /* Get a node list of all <h1> elements */
    var head1 = content.getElementsByTagName('h1'),
    var toc = '<ul>';

    /* Loop through the list of <h1> elements */
    for (var i = 0, il = head1.length; i < il; i++) {
        /* Try to get to the <blockquote> element following the header element */
        var h1 = head1[i].nextSibling;

        /*
         * Is the node text (whitespace in this case)?  If so, try the next element
         * after this...it should be the <blockquote> element
         */
        if (h1.nodeType == 3)
            h1 = h1.nextSibling;
        /* Get a node list of all <h2> elements under the <blockquote> element */
        var head2 = h1.getElementsByTagName('h2'),

        toc += '<li><a href="#' + head1[i].getAttribute('id') + '">' +
            head1[i].childNodes[0].nodeValue + '</a>';
        /* Are there any <h2> elements? */
        if (head2.length > 0) {
            toc += '<ul>';
            /* Loop through the list of <h2> elements */
            for (var j = 0, jl = head2.length; j < jl; j++) {
                /*
                 * Try to get to the <blockquote> element following the header
                 * element.
                 */
                var h2 = head2[j].nextSibling;

                /*
                 * Is the node text (whitespace in this case)?  If so, try the
                 * next element after this...it should be the <blockquote>
                 * element.
                 */
                if (h2.nodeType == 3)
                    h2 = h2.nextSibling;
                /*
                 * Get a node list of all <h3> elements under the <blockquote>
                 * element.
                 */
                var head3 = h2.getElementsByTagName('h3'),

                toc += '<li><a href="#' + head2[j].getAttribute('id') + '">' +
                    head2[j].childNodes[0].nodeValue + '</a>';
                /* Are there any <h3> elements? */
                if (head3.length > 0) {
                    toc += '<ul>';
                    /* Loop through the list of <h3> elements */
                    for (var k = 0, kl = head3.length; k < kl; k++) {
                        /*
                         * Try to get to the <blockquote> element following the
                         * header element.
                         */
                        var h3 = head3[k].nextSibling;

                        /*
                         * Is the node text (whitespace in this case)?  If so,
                         * try the next element after this...it should be the
                         * <blockquote> element
                         */
                        if (h3.nodeType == 3)
                            h3 = h3.nextSibling;
                        /*
                         * Get a node list of all <h4> elements under the
                         * <blockquote> element.
                         */
                        var head4 = h3.getElementsByTagName('h4'),

                        toc += '<li><a href="#' + head3[k].getAttribute('id') +
                            '">' + head3[k].childNodes[0].nodeValue + '</a>';
                        /* Are there any <h4> elements? */
                        if (head4.length > 0) {
                            toc += '<ul>';
                            /* Loop through the list of <h4> elements */
                            for (var l = 0, ll = head4.length; l < ll; l++)
                                toc += '<li><a href="#' +
                                    head4[l].getAttribute('id') +
                                    '">' + head4[l].childNodes[0].nodeValue +
                                    '</a></li>';
                            toc += '</ul>';
                        }
                        toc += '</li>';
                    }
                    toc += '</ul>';
                }
                toc += '</li>';
            }
            toc += '</ul>';
        }
        toc += '</li>';
    }
    toc += '</ul>';
    $('toc').innerHTML = toc;
}

As you can see from this example, the function dynamically creates an XHTML list as a string as it walks the DOM’s document tree. It walks it through a series of loops that go deeper and deeper into levels, stopping after going four levels deep. I stopped here mainly because O’Reilly frowns on the use of even a fourth level of header—you can feel free to go to all six header levels if you want.

Once the list is created, it is passed to the innerHTML property of our wrapper <div> element, along with some header text for the table of contents. Put simply, you can create a dynamic table of contents that always follows whatever content is in the document, all modifications included.

Say that I am creating a document and I want to use numbers to identify each section. As such, the sections would have an order of 1, 1.1, 1.2, 1.2.1, and so on. We need to change nothing—I repeat, nothing—in what we have already done to accomplish this. All we need are some CSS rules regarding how to style the table of contents and headers. Example 8-11 shows these rules.

Example 8-11. The CSS rules needed to create a numbering system for the document and table of contents

#content {
    counter-reset: h1;
}

#content h1:before {
    content: counter(h1) "0202014020";
    counter-increment: h1;
}

#content h1 {
    counter-reset: h2;
    font-size: 1.6em;
}

#content h2:before {
    content: counter(h1) "." counter(h2) "0202014020";
    counter-increment: h2;
}

#content h2 {
    counter-reset: h3;
    font-size: 1.4em;
}

#content h3:before {
    content: counter(h1) "." counter(h2) "." counter(h3) "0202014020";
    counter-increment: h3;
}

#content h3 {
    counter-reset: h4;
    font-size: 1.2em;
}

#content h4:before {
    content: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4)
        "0202014020";
    counter-increment: h4;
}

#toc ul {
    counter-reset: item;
    font-weight: bold;
}

#toc li {
    display: block;
}

#toc li:before {
    content: counters(item, ".") "0202014020";
    counter-increment: item;
}

Tip

Adding a table of contents to an application or site satisfies the following WAI-WCAG 1.0 guideline:

  • Priority 2 checkpoint 13.3: Provide information about the general layout of a site (i.e., a site map or table of contents).

The only drawback to our table of contents is that it requires JavaScript and CSS to create, which may not work and ultimately may defeat this accessibility requirement.

Figure 8-7 shows how the table of contents and the headers look with the style rules from Example 8-11 applied.

The document with style rules applied, as viewed with Firefox

Figure 8-7. The document with style rules applied, as viewed with Firefox

Warning

The CSS rules in Example 8-11 liberally use CSS2 rules to achieve the desired look. Specifically, the :before pseudoclass does all the heavy lifting. Certain browsers do not yet implement this pseudoclass—or much CSS2 in general. In these browsers, the table of contents will function normally, but none of the numbering will be displayed.

Sortable Lists

You can sort list items in a variety of ways. One of the easiest for the user to understand (at least visually) is to place checkboxes to the left of each item in the list. Then provide buttons that indicate movement up and down and that, when pressed, shift the checked items into a new position in the list. This is not the most elegant solution, but it gets the job done. For an Ajax web application, however, we are striving for more than just a working solution. We want to provide users with interfaces that remind them of the interfaces in desktop applications.

To do this, and to provide more of a Web 2.0 feel to the functionality, dragging and dropping list items to reposition them is the way to go. Most of the JavaScript libraries we covered in Chapter 4 provide methods for sorting lists via drag-and-drop solutions. For the examples in this section, I have chosen to use script.aculo.us for my drag-and-drop solution, mainly because it is built on top of the Prototype Framework (which we already have had some experience with).

For thoroughness, I also will provide the methods the Dojo Toolkit uses, after we explore the script.aculo.us interface. It is a good idea to become familiar with several of these JavaScript libraries and toolkits because none of them have all the functionality needed for a complete Ajax solution.

Like Prototype, script.aculo.us is divided into different areas based on functionality. For dragging and dropping list items on the screen, it provides the Sortable object. The Sortable object allows you to sort items in a list by dragging them to the desired position in the list and dropping them into place. To instantiate an XHTML list, do the following:

Sortable.create('id_of_list', [options]);

Creating a sortable list is as simple as that. Table 8-1 provides the options available to pass in the object parameter of the Sortable.create( ) method.

Table 8-1. Options available to pass in the object parameter of Sortable.create( )

Option

Default value

Description

tag

li

Sets the type of element for the child elements of the container that will be made sortable. For <ul> and <ol> element containers, the default works, but for any other kind of element, this must be specified.

only

None

Restricts the child elements for sorting further so that only those elements with the specified CSS class will be used. An array of CSS classes (as strings) can also be passed so that the restriction is on any of the classes.

overlap

vertical

Specifies in which direction the overlapping of elements occurs. The possible values are vertical and horizontal; vertical lists use vertical and horizontal lists use horizontal.

constraint

vertical

Specifies whether a constraint is put on the dragging to vertical or horizontal directions. Possible values are vertical, horizontal, and false.

containment

(only within container)

Enables dragging and dropping between Sortables. This takes an array of elements or element ids for the containers.

handle

None

Sets a handle for the draggable object so that dragging only occurs using the handle. Is an element reference, an element id, or a CSS class. With the CSS class, the first child element found with the passed value will be used as the handle.

hoverclass

None

Gives an additional CSS class to the list items when an item being dragged is hovered over the other items.

ghosting

false

When set to true, the dragged element will be cloned such that the list does not shift until the dragged element is dropped into a new place. Possible values are true and false.

dropOnEmpty

false

When set to true, the Sortable container will be made into a Droppable that can receive elements as soon as it no longer has any child elements. Possible values are true and false.

scroll

None

If the Sortable is in a container and that element has overflow: scroll set, this value can be set to the container’s id. Once this is set, the position of the container will follow along with the position of the Sortable element.

scrollSensitivity

20

The scrollSensitivity value tells the Sortable how sensitive it should be to the scrolling boundaries of the container. The higher the number, the more sensitive the Sortable will be.

scrollSpeed

15

The speed of the scrolling of the surrounding container is controlled with this value. The higher the number, the faster the scroll as it follows the Sortable.

tree

false

When this value is set to true, sortable functionality to elements listed in treeTag is set. Values are true and false.

treeTag

ul

This identifies the element type in which the tree nodes are contained.

Warning

The Sortable object will function correctly in your application only if you include the following line of code before the code to create the Sortable:

Position.includeScrollOffsets = true;

Let’s pretend we are building an administrative widget for an online contact application, and the functionality required is to change the order in which contact fields are presented. Assume the following list is used for the possible fields to sort:

<ul id="sortList">
    <li><span>Full Name</span></li>
    <li><span>Company</span></li>
    <li><span>Business Phone</span></li>
    <li><span>Business Fax</span></li>
    <li><span>Home Phone</span></li>
    <li><span>Mobile Phone</span></li>
    <li><span>Job Title</span></li>
    <li><span>Business Address</span></li>
    <li><span>Email address</span></li>
    <li><span>Icon</span></li>
    <li><span>Flag Status</span></li>
</ul>

Next there is the small matter of styling the list so that it looks a little more presentable than its default value. These CSS rules should do the trick:

#sortList {
    margin: 0;
    padding: 0;
}

#sortList li {
    color: #0c0;
    cursor: move;
    margin: 0;
    margin-left: 20px;
    padding-left: 20px;
    padding: 4px;
}

#sortList li span {
    color: #070;
}

Finally, to make this list sortable, we would use the following JavaScript to initialize it and make it functional:

<script type="text/javascript">
    Sortable.create('sortList', {
        ghosting: true,
        constraint: 'vertical'
    });
</script>

With that, we have created a sortable list of items that requires simple drag-and-drop functionality to work, as shown in Figure 8-8.

A sortable list in action using script.aculo.us

Figure 8-8. A sortable list in action using script.aculo.us

Now we will see how Dojo would accomplish this task. The Dojo Toolkit functions by loading packages into the application page when they are needed. In fact, the method looks very similar to Java and comparable languages and the way they include libraries in a program. For example:

<script type="text/javascript">
    dojo.require('dojo.io.*'),
    dojo.require('dojo.event.*'),
    dojo.require('dojo.widget.*'),
</script>

This loads the io, event, and widget packages so that you can use them throughout the page. Like script.aculo.us, Dojo has built-in functionality for sortable lists using drag and drop.

Using the same list we used for the script.aculo.us example, we will again make it sortable. First, in the head of the XHTML page, we must load the necessary Dojo modules:

dojo.require('dojo.dnd.*'),
dojo.require('dojo.event.*'),

Once these modules have been loaded, we need a way to initiate the JavaScript on our list. We will take advantage of Dojo’s event handling for this so that we execute a function once the page has loaded. This is an initialization function with the event JavaScript underneath it:

/**
 * This function, onBodyLoad, initializes the drag-and-drop sorting capabilities of
 * the Dojo Toolkit by setting the list container (<ul> element) as the target for
 * dropping and the list items (<il> element) are set as the elements that are
 * draggable.
 */
function onBodyLoad( ) {
    /* Get the container element */
    var dndList = document.getElementById('sortList'),

    /*
     * Create the drop target out of the container element and set a grouping that
     * has the right to drop within it.
     */
    new dojo.dnd.HtmlDropTarget(dndList, ['sortListItems']);
    /* Get a node list of all of the container's <li> elements */
    var dndItems = dndList.getElementsByTagName('li'),

    /*
     * Loop through the list of <li> elements and make each of them draggable
     * and set them as members of the drop target's group.
     */
    for (var i = 0, il = dndItems.length; i < il; i++)
        new dojo.dnd.HtmlDragSource(dndItems[i], 'sortListItems'),
}

/* Set an event to call the onBodyLoad( ) function when the page is loaded */
dojo.event.connect(dojo, 'loaded', 'onBodyLoad'),

There aren’t many options for setting up the sortable list, as there are for script.aculo.us. Besides the id of the list to sort, you can only pass in an array of strings that represent the groups of pages to which you want the list to drag and drop. In our initialization function, onBodyLoad( ), the list will be used only for items in the sortListItems group. This allows some control over where items can be dragged and dropped in the application.

Creating drag-and-drop sort functionality is simple when you use one of the many JavaScript libraries, toolkits, or frameworks found on the Web. However, we need to integrate this functionality with Ajax to make it more useful.

Ajax and the Draggable List

The practical application of a sortable list is in the client sending its sort order information to the server. The server can then process the information and send a response back to the client. Returning to our contact application, we want to send the server the updated sort order so that it knows how to format, say, a table that it can then send back to the client. For now, we will concentrate on sending the information. We will return to the server part in a couple of chapters.

Using script.aculo.us, integrating Ajax is not difficult. As you probably noted in Table 8-1, the Sortable object has an onUpdate callback as part of its Sortable.create( ) options. We simply need to write a function that parses the new list, creates a string list with the new order, and sends that to the server.

Before we do that, we need to modify our list slightly:

<ul id="sortList">
    <li id="li_0"><span>Full Name</span></li>
    <li id="li_1"><span>Company</span></li>
    <li id="li_2"><span>Business Phone</span></li>
    <li id="li_3"><span>Business Fax</span></li>
    <li id="li_4"><span>Home Phone</span></li>
    <li id="li_5"><span>Mobile Phone</span></li>
    <li id="li_6"><span>Job Title</span></li>
    <li id="li_7"><span>Business Address</span></li>
    <li id="li_8"><span>Email address</span></li>
    <li id="li_9"><span>Icon</span></li>
    <li id="li_10"><span>Flag Status</span></li>
</ul>

This new list has an id attribute that we will use to identify which item is in which position for the order string. Now our function can parse this attribute to obtain the order of the list. For example:

Sortable.create('sortList', {
    ghosting: true,
    constraint: 'vertical',
    onUpdate: function(container) {
        /* Get the list of child nodes */
        var listItems = container.getElementsByTagName('li'),
        /* Start the parameter string */
        var params = 'order=';
        /*
         * Loop through the items and parse the id attribute, creating an array
         * with the <li> element portion in index 0, and the order number in
         * index 1.  Then add that to the string.
         */
        for (var i = 0, il = listItems.length; i < il; i++) {
            var temp = listItems[i].id.split('_'),
            if (i > 0)
                params += ',';
            params += temp[1];
        }
        /* Make the call to the server with the new order */
        new Ajax.Request('updateOrder.php', {
            method: 'post',
            parameters: params,
            onSuccess: function(xhrResponse) {
                /* you can do anything you want here...I say nothing */
            },
            onFailure: function(xhrResponse) {
                /*
                 * Maybe you would want to let the user know there was a
                 * problem and whom to contact about it.
                 */
            }
        });
    }
});

The only difficult part is deciding whether we need to do anything when the call to the server completes. We will talk about errors in Chapter 12, and then decide what we should do in this case. The server will get a comma-delimited string with the new sort order, such as 1,4,6,3,2,5,7,10,8,9. It will parse this string and do something with it. For now, that is all we want it to do—the server will parse the string and create a sorting string out of the data. This will be stored in a session variable for later use. Here is an example:

<?php
/* You always need this function if you are going to use sessions in PHP */
session_start( );

/* Was the order sent to the script? */
if (isset($_REQUEST['order'])) {
    /* Store the array in a session variable to retrieve later */
    $_SESSION['sort_array'] = split(',', $_REQUEST['order']);
    print(1);
} else
    print(0);
?>

Much like we split the string for the ids on the underscore (_) character, in this case we will be making an array based on the commas in the string. This array is actually what we need to store, as it is a good construct for future coding. In Chapter 10, we will see how to let the server know it needs to generate a new contact list that is to be sent to the client with the proper sort order. The script returns a 0 or a 1 depending on what happened in the script, which the client can use for a true or false.

The JavaScript libraries and toolkits are—and I know many of you will scoff at this—very useful and good for taking care of the code you shouldn’t have to think about. Instead, you can concentrate on the application’s actual functionality. I ask all of you naysayers, do desktop application developers code everything from scratch, or do they use third-party libraries when they are available?

An Ajax Slide Show

Building a slide show requires the client to load a lot of pictures. The pictures are hidden as they are loaded, only to be viewed when the show cycles them. If it is a large slide show, it could take a very long time to load the images—too long, in fact, for a lot of users to wait. Ajax will allow us to load images after the client has loaded, and the asynchronous part of Ajax allows the show to start while pictures are still being loaded.

The first thing to think about is how the data will be presented. Because this is a chapter on tables and lists, I think we should hold the images in an unordered list. We will want to keep track of three things for each picture: the image, a title for the image, and a description of the image. Before we begin to worry about the slide show, we need to lay out the structure of the XHTML. To get a little bit fancy, let’s lay out the page something like this:

<div id="bodyContainer">
    <div id="slideshowContainer">
        <div id="slideshowWrapper">
            <div id="imageTitle"></div>
            <ul id="slideshowList"></ul>
            <div id="navigationContainer">
                <a href="image.php">Previous</a> | <a href="image.php">Next</a>
            </div>
        </div>
    </div>
    <div id="imageDescription"></div>
</div>

As you can see, the list (slideshowList), along with an element to hold the title (imageTitle) and an element to hold the navigation (navigationContainer), sits inside a wrapper called, appropriately enough, slideshowWrapper. That wrapper is contained in another element, the slideshowContainer, which sits at the same level as the element holding the image’s description (imageDescription). Both are wrapped in the bodyContainer. All of these layers are here so that you can lay out the page in just about any manner you want.

Once we get to the image loading part, all of the images will be the child node of an <li> element that gets appended to the slideshowList. For now, this will remain an unordered list without any children.

Our next order of business is to define the CSS rules to make the slide show look more presentable. Example 8-12 takes care of styling our XHTML.

Example 8-12. slideshow.css: The CSS rules to lay out the slide show

a {
    background-color: transparent;
    color: #fff;
}

body {
    background-color: #fff;
    color: #000;
    font-family: serif;
    font-size: 16px;
}

/* Put the title in the center of the box */
#imageTitle {
    text-align: center;
}

/*
 * Since this is the main (outside) container, give it a big red border and
 * make the inside background black.  Shift it right with an extra margin on
 * the left to make room for the image description.
 */
#slideshowContainer {
    background-color: #000;
    border: 3px solid #f00;
    color: #fff;
    display: table;
    height: 540px;
    margin: 0 0 0 280px;
    overflow: hidden;
    width: 500px;
}

/* Make the wrapper act like it is a table cell so that it will obey the
 * vertical alignment to the middle.
 */
#slideshowWrapper {
    display: table-cell;
    vertical-align: middle;
}

/* Get rid of the list marker and center the list */
#slideshowList {
    list-style-type: none;
    margin: 0;
    padding: 0;
    text-align: center;
}

#slideshowList li {
    display: inline;
    margin: 0;
    padding: 0;
}

/* Put the navigation tools in the center of the box */
#navigationContainer {
    text-align: center;
}

/*
 * Move the image description into the space that the outside container made
 * by shifting over.  Let this container scroll if the contents are too large
 * (but that shouldn't happen).
 */
#imageDescription {
    border: 1px solid #000;
    height: 495px;
    margin: 23px 0 0 5px;
    overflow: auto;
    position: absolute;
    padding: 10px 0 10px 10px;
    top: 0;
    width: 250px;
}

Now that the application looks the way we want it to, we can concentrate on the JavaScript portion. The navigation links will need to have an onclick event associated with them that will call a function to move between the pictures in the list. Because the images are stored in a list as the firstChild element of the <li> element, the other image information that will be available must be stored elsewhere. Yes, all of the information could be stored in the list item, but then the slide show would require additional style rules and the JavaScript would have to look at a more complicated DOM tree.

Instead, a multidimensional array can store the other information, where index 0 will hold the image title and index 1 will hold the description. The following code provides an easy solution for changing back and forth between images:

/**
 * This function, changeSlide, moves the display of individual <li> elements one
 * element at a time while hiding all other elements to give the illusion of
 * moving back and forth through a slide show of <li> elements.
 *
 * @param {Integer} p_slideDirection The direction of the slide change (-1
 *     back and 1 forward).
 * @return Returns false so that the element that had the event click stops
 *     any default events.
 * @type Boolean
 * @see Element#hide
 * @see Effect#Appear
 */
function changeSlide(p_slideDirection) {
    /* Is the index going to be too small or too large? */
    if (!((index + p_slideDirection) < 0 || (index +
            p_slideDirection) > $('slideshowList').childNodes.length - 1)) {
        index += p_slideDirection;
        /* Loop through the unordered list and hide all images */
        var items = $('slideshowList').getElementsByTagName('li'),
        for (var i = 0, il = items.length; i < il; i++)
            Element.hide(items[i]);
        /*
         * Now make the image that is to be changed appear, and change the
         * title and description.
         */
        Effect.Appear($('slideshowList').childNodes[index]);
        $('imageTitle').innerHTML = imageData[index][0];
        $('imageDescription').innerHTML = imageData[index][1];
    }
    /* Return false so that the links do not try to actually go somewhere */
    return (false);
}

The parameter p that is passed to the function is simply -1 to go to the previous image and 1 to go to the next image. This function requires the Prototype Framework and the script.aculo.us library to function. We use script.aculo.us to make the images look more spectacular than if we had just used display: block and display: none.

For the images to be served up with Ajax, the developer must rely on the data URL format. The data URL format allows the src of an image to be encoded inline as Base64 content. It looks something like this:

<img src="data:image/jpg;base64,[...]" alt="A Base64-encoded image." title="A
Base64-encoded image." />

where the [...] is replaced with the Base64-encoded image data. Now for the bad news: Internet Explorer 7 and earlier do not recognize the data URL format for an src image. Have no fear, though: I will address this before I finish this section.

Assuming that we can get our image as a Base64-encoded string (we will address this when we discuss the server end of this application) our next task is the format that the Ajax request will receive. The following XML gives an example of a possible format:

<photoRequest>
    <image>
        <![CDATA[
            /9j/4AAQSkZJRgABAgEASABIAAD/7RbKUGhvdG9zaG9wIDMuMAA4QklNA+0
            AQBIAAAAAQABOEJJTQQNAAAAAAAEAAAAeDhCSU0D8wAAAAAACAAAA
            .
            .
            .
            jza1b6Wt4PMmgNPy+z/MOo1dtan3BWYtcxXifH/YPRZu09tGqxxuuckjPX//2Q==
        ]]>
    </image>
    <title>The Image's Title</title>
    <description>
        This is the description for the Image.
    </description>
</photoRequest>

The most important thing to note about this format (other than that it is really simple) is that the Base64-encoded string is inside a CDATA section. Without this, the browser does not recognize the string as true text, and no image will render. Our Ajax call will end up pseudorecursively calling itself until there are no more images to get, as Example 8-13 shows. Example 8-13 combines all of our JavaScript into a single file for the XHTML page to include.

Example 8-13. slideshow.js: Code used to load our images with Ajax and move through them

/**
 * @fileoverview This file, slideshow.js, builds up a list of images dynamically
 * through continuous /XMLHttpRequest/ calls to the server for data until all
 * pictures have been placed in the list.  This allows the application to load
 * faster, as it does not have to load all of the images before the page is
 * functional.  The application then allows users to view the list of images as
 * a slide show, viewing one image (and all of its associated data) at a time.
 *
 */

/**
 * This variable holds the current image number to be loaded.
 */
var imageNumber;
/**
 * This variable holds the extra image information (title, description).
 */
var imageData = [];
/**
 * This variable holds the index of which picture is being viewed.
 */
var index = 0;

/**
 * This function, setupApp, sets the initial image number to start pulling from
 * the server, then calls the /fetchNextImage/ function which starts the Ajax
 * calls.
 *
 * @see fetchNextImage
 */
function setupApp( ) {
    imageNumber = 0;
    fetchNextImage( );
}

/**
 * This function, fetchNextImage, checks to make sure it should make an Ajax call
 * based on the image number, and then calls the Prototype Framework's
 * /Ajax.Request/ method and creates a function to handle the results.
 *
 * @see Ajax#Request
 */
function fetchNextImage( ) {
    /* Is the image number bigger than 0? */
    if (++imageNumber > 0) {
        /* Call sendPhoto.php with the number of the photo */
        new Ajax.Request('sendPhoto.php', {
            method: 'post',
            parameters: 'number=' + imageNumber,
            /*
             * The onSuccess method checks to see if the number of elements sent
             * via XML is greater than one (one means there was an error).  If
             * it is, then it creates a new list item and image, placing the
             * latter inside the former before adding the Base64-encoded string
             * into the image's src.
             */
            onSuccess: function(xhrResponse) {
                /* Did we get an XML response we want? */
                if (xhrResponse .responseXML.documentElement.childNodes.length > 1) {
                    /* Create new elements within the DOM document */
                    var newItem = document.createElement('li'),
                    var newImage = document.createElement('img'),

                    /*
                     * Add id attributes to both and put the image in the list
                     * item
                     */
                    newImage.setAttribute('id', 'i' + imageNumber);
                    newItem.appendChild(newImage);
                    newItem.setAttribute('id', 'l' + imageNumber);
                    /* Add the new image to the list, then hide it */
                    $('slideshowList').appendChild(newItem);
                    Element.hide($('l' + imageNumber));
                    /* Add the Base64-encoded string */
                    $('i' + imageNumber).src = 'data:image/jpg;base64,' +
                        xhrResponse.responseXML.documentElement.getElementsByTagName(
                        'image')[0].firstChild.nodeValue;
                    /* Create the next index in the array to hold the image data */
                    imageData[(imageNumber - 1)] = [];
                    imageData[(imageNumber - 1)][0] =
                        xhrResponse.responseXML.documentElement.getElementsByTagName(
                        'title')[0].firstChild.nodeValue;
                    imageData[(imageNumber - 1)][1] =
                        xhrResponse.responseXML.documentElement.getElementsByTagName(
                        'description')[0].firstChild.nodeValue;
                    /* Is this the first image? */
                    if (imageNumber <= 1) {
                        /* Set the initial image and show it */
                        index = 0;
                        Effect.Appear($('l' + imageNumber));
                        $('imageTitle').innerHTML = imageData[0][0];
                        $('imageDescription').innerHTML = imageData[0][1];
                    }
                    /* Recursive call! */
                    fetchNextImage( );
                } else {
                    /* We are done */
                    imageNumber = -1;
                }
            }

        });
    }
}

/**
 * This function, changeSlide, moves the display of individual <li> elements one
 * element at a time while hiding all other elements to give the illusion of moving
 * back and forth through a slide show of <li> elements.
 *
 * @param {Integer} p_slideDirection The direction of the slide change (-1 back
 *     and 1 forward).
 * @return Returns false so that the element that had the event click stops any
 *     default events.
 * @type Boolean
 * @see Element#hide
 * @see Effect#Appear
 */
function changeSlide(p_slideDirection) {
    /* Is the index going to be too small or too large? */
    if (!((index + p_slideDirection) < 0 || (index +
            p_slideDirection) > $('slideshowList').childNodes.length - 1)) {
        index += p_slideDirection;
        /* Loop through the unordered list and hide all images */
        var items = $('slideshowList').getElementsByTagName('li'),
        for (var i = 0, il = items.length; i < il; i++)
            Element.hide(items[i]);
        /*
         * Now make the image to be changed appear, and change the title and
         * description.
         */
        Effect.Appear($('slideshowList').childNodes[index]);
        $('imageTitle').innerHTML = imageData[index][0];
        $('imageDescription').innerHTML = imageData[index][1];
    }
    /* Return false so that the links do not try to actually go somewhere */
    return (false);
}

try {
    /* Call /setupApp( )/ when the page is loaded */
    Event.observe(window, 'load', setupApp, false);
} catch (ex) {}

Now that we have some working script, we can finish the XHTML file to pull this together, as shown in Example 8-14.

Example 8-14. ajax_slideshow.html: A working slide show utilizing Ajax

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>An Ajax Slide Show</title>
        <meta http-equiv="imagetoolbar" content="no" />
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <link rel="stylesheet" type="text/css" media="screen" href="slideshow.css" />
        <script type="text/javascript" src="prototype.js"> </script>
        <script type="text/javascript" src="scriptaculous.js"> </script>
        <script type="text/javascript" src="slideshow.js"> </script>
    </head>
    <body>
        <div id="bodyContainer">
            <div id="slideshowContainer">
                <div id="slideshowWrapper">
                    <div id="imageTitle"></div>
                    <ul id="slideshowList"></ul>
                    <div id="navigationContainer">
                        <a href="image.php" onclick="return changeSlide(-1);">
                            Previous
                        </a> |
                        <a href="image.php" onclick="return changeSlide(1);">
                            Next
                        </a>
                    </div>
                </div>
            </div>
            <div id="imageDescription"></div>
        </div>
    </body>
</html>

What about the server, you ask. Don’t worry, I didn’t forget about that. If the scripting language being used on the server side of things is PHP, this is a simple task. PHP has a function, base64_encode( ), that does just what it says (and just what we need it to do). Other server-side scripting languages may have the same functionality, but I chose to be consistent in my use of PHP for server-side examples.

On the server, our script is going to require an image number be passed to it so that it knows what picture to send back. Assuming that all of our data, including the image as a BLOB, is sitting in a MySQL database, Example 8-15 shows how we can write the server script to send data back to the client.

Example 8-15. sendPhoto.php: PHP script that gets an image out of a database and sends the results to the client

<?php
/**
 * Example 8-15, sendPhoto.php: PHP script that gets image out of a database
 * and sends the results to the client.
 */

/**
 * The Zend Framework Db.php library is required for this example.
 */
require_once('Zend/Db.php'),
/**
 * The generic db.inc library, containing database connection information such as
 * username, password, server, etc., is required for this example.
 */
require('db.inc'),

/* Variable to hold the output XML string */
$xml = '';

/* Was a /number/ even passed to me? */
if (isset($_REQUEST['number'])) {
    /* Set up the parameters to connect to the database */
    $params = array ('host' => $host,
                     'username' => $username,
                     'password' => $password,
                     'dbname' => $db);

    try {
        /* Connect to the database */
        $conn = Zend_Db::factory('PDO_MYSQL', $params);
        /* Query the database with the passed number */
        $sql = 'SELECT encoded_string, title, description FROM pictures WHERE '
              .'pic_id = 'pic_'.$_REQUEST['number'].'';';
        /* Get the results of the query */
        $result = $conn->query($sql);
        /* Are there results? */
        if ($rows = $result->fetchAll( )) {
            /* Build the XML to be sent to the client */
            $xml .= '<phtoRequest>';
            foreach($rows in $row)
                $xml .=
                    '<image><![CDATA['
                    .chunk_split(base64_encode($row['encoded_string']))
                    .']]></image>';
                $xml .= "<title>{$row['title']}</title>";
                $xml .= "<description>{$row['description']}</description>";
            $xml .= '</phtoRequest>';
        } else
            /* No records...return error */
            $xml .= '<photoRequest><error>-1</error></photoRequest>';
    } catch (Exception $e) {
        /* Uh oh, something happened, so we need to return an error */
        $xml .= '<photoRequest><error>-1</error></photoRequest>';
    }
} else
    /* A number was not passed in, so return an error */
    $xml .= '<photoRequest><error>-1</error></photoRequest>';

/*
 * Change the header to text/xml so that the client can use the return string
 * as XML.
 */
header('Content-Type: text/xml'),
/* Give the client the XML */
print($xml);

Not only does this method allow the user to interact with the slide show application while images are being loaded, but also errors can be trapped when requesting images. This is something other image loading techniques failed to handle adequately. Figure 8-9 shows what this application would look like in action.

An Ajax-enabled slide show application

Figure 8-9. An Ajax-enabled slide show application

As for Internet Explorer, things are grim but not completely bleak yet. There is a workaround to the data URL problem that, to my knowledge, Dean Edwards created (see his blog, at http://dean.edwards.name/weblog/2005/06/base64-ie/). His solution is to send the Base64-encoded string back to the server, let PHP decode the string, and send back the result as an image.

For this to work dynamically, he relies on Internet Explorer’s support for nonstandard dynamic CSS expressions—specifically, the behavior property. First, you need a JavaScript function to ready the encoded string:

/* The regular expression to test for Base64 data */
var BASE64_DATA = /^data:.*;base64/i;
/* This is the path to the PHP that will decode the string */
var base64Path = 'base64.php';

/* The fixBase64 function will handle getting the new image by calling the PHP */
function fixBase64(img) {
    /* Stop the CSS expression from being endlessly evaluated */
    img.runtimeStyle.behavior = 'none';
    /* Should we apply the fix? */
    if (BASE64_DATA.test(img.src))
        /*
         * Setting the src to an external source makes it do the call (Ajax,
         * sort of).
         */
        img.src = base64Path + '?' + img.src.slice(5);
}

Then you need to have the dynamic CSS expression call this function:

img {behavior: expression(fixBase64(this));}

For a more elegant and completely CSS version, you can wrap all the JavaScript code into the CSS expression, like this:

img {
    behavior: expression((this.runtimeStyle.behavior = "none") &&
       (/^data:.*;base64/i.test(this.src)) &&
       (this.src="/my/base64.php?" + this.src.slice(5)))
}

That was easy to follow, wasn’t it? Now, all we have left is to handle this on the server. This is a simple solution in PHP:

<?php
/* Split the image so we know the type to send back and have the Base64 string */
$image = split(';', $_SERVER['REDIRECT_QUERY_STRING']);
$type = $image[0];
$ image  = split(',', $image[1]);
/* Let the client know what is coming back */
header('Content-Type: '.$type);
/* Send the decoded string */
print(base64_decode($image[1]));
?>

Just like that, we now have a cross-browser solution for our slide show application.

The Ajax slide show application shows just one more way in which lists can be useful for the structure of a dynamic widget. With proper styling and minor changes to the JavaScript, a developer could use a definition list instead of an unordered list. Then all of the image data could be stored together. The CSS would probably be more complex, and it may or may not simplify the JavaScript. However, this sort of solution would make the slide show more accessible, so it deserves some serious thought.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
34.231.180.210