Menu

The XSLDataGrid: XSLT Rocks Ajax

August 23, 2006

Lindsey Simon

Most web applications have a requirement somewhere in their interface for a tabular view of data -- often, a view of the rows in a database table. In some cases, the use of a static HTML <TABLE> is appropriate, but users have become increasingly accustomed to richer, more malleable interfaces that let them change column widths, order, etc. Among the application widgets in the web developer's toolbox, the dynamic datagrid is an often cumbersome one to set up. This article will outline a datagrid component powered by XSLT and JavaScript that aims to achieve easy setup, high performance, and minimum dependence.

The Problem

There are roughly three types of approaches to using a JavaScript widget in a web application. Approach #1 involves creating a server-side object with properties (in PHP or Ruby, for instance), and then eventually calling a render method on the object that creates a XHTML string to be sent to the browser. For instance, in PHP it might look something like this:

$dg = new DataGrid();

$dg->columns = array( "field1", "field2", "field3" );

$dg->data = $data;

etc...

$dg->render();

Approach #2 is much like approach #1, except that an object is instantiated in the browser (with JavaScript), and the render method is essentially a series of Document Object Model (DOM) object creations and attachments to the browser's render tree. For example, using the ActiveWidgets Grid, the code might look like this:

var myCells = [

["MSFT","Microsoft Corporation", "314,571.156"],

["ORCL", "Oracle Corporation", "62,615.266"]

];



var myHeaders = ["Ticker", "Company Name", "Market Cap."];



// create grid object

var obj = new AW.UI.Grid;



// assign cells and headers text

obj.setCellText(myCells);

obj.setHeaderText(myHeaders);



// set number of columns/rows

obj.setColumnCount(3);

obj.setRowCount(2);



// write grid to the page

document.write(obj);

Approach #3 is a combination of approaches #1 and #2, whereby some relatively simple, declarative XHTML is used as a starting point, and some method is used to populate the render tree with extra bells and whistles. An analogy here is building a house where the declarative XHTML is the frame and the DOM additions are the siding, air conditioning, and champagne-filled hot tub!



var myGrid = new XSLDataGrid( 'renderDiv', { width: 480, height: 200, transformer:'client', debugging: true } );

Approach #1 to widget creation is the least portable, as it relies on whatever server-side language a developer is using at the time. Approach #2 will most likely degrade poorly if an end-user has disabled JavaScript in his browser; this approach also tends to perform less reliably with larger datasets. Thus, compromise -- the mother of endless tinkering -- will be our guide to Approach #3.

In Defense of the Table Tag

The advent of added and better Cascading Style Sheet (CSS) support in browsers has caused developers to reconsider their use of the <TABLE> tag. For the most part this is a great thing, as tables can be constraining and inefficient for design and layout. However, the <TABLE> tag and its children offer a declarative, semantic language for describing the presentation of tabular data that succeeds where an obscure combination of nested DIVs and float:lefts becomes cumbersome. And if a user agent has CSS disabled, the pure DIV approach to table presentation will be a mess.

XSLT On XHTML [1]

XSLT offers developers a mechanism for decorating the DOM that ensures well-formed markup and is both powerful and flexible. XSL transforms for UI widgets must be followed by a call to put the resultant XHTML back into the DOM being rendered. The XSLDataGrid uses its container's innerHTML property to do so (this is a technique known as AHAH [2]). It is worth noting here that many argue against using the proprietary, but well-supported, innerHTML property to populate the DOM. In an article entitled "Benchmark - W3C DOM vs. innerHTML", the ever-invalueable quirksmode.org has this to say about tables specifically:

The most obvious conclusion of these tests is that innerHTML is faster than "real" W3C DOM methods in all browsers. The W3C DOM table methods are slow to very slow, especially in Explorer.

More than once people said that creating elements only once and then cloning them when necessary leads to a dramatic performance improvement. These tests don't show anything of that kind. Although in most browsers cloning is minimally faster than creating, the difference between the two methods is small.

So what does the picture look like with XSLT for widget instantiation? The following diagram shows how the XSLDataGrid works.

Semantic Table
Semantic XHTML Table
+ XSLDataGrid.xsl = Decorated XHTML
More tags, attributes,
CSS, etc ...
+ Javascript
Instantiation,
event listeners,
etc ...
= Semantic Table
Rich DataGrid UI in Render Tree
and XML DOM in Dual-DOM in Memory

Dual-DOM, or, How I Dealt with innerHTML

There are essentially three ways to use the XSLDataGrid component:

  1. by fetching the transformed, fully decorated XHTML each time from the server
  2. by fetching semantic XHTML from the server and transforming it on the client
  3. by transforming XHTML already on the page in the client

In case 1, we don't really have to worry about keeping up with the DOM in the client, since we can farm off all the change operations (column resize, sort, and reorder) to the server, re-filling the container's innerHTML each time. Cases 2 and 3 are different, because we want to perform XSLT multiple times -- in other words, we will continue to need some valid XHTML on which we can perform XSLT. It's important to note that with innerHTML, what you put in is not necessarily what you get out when you read it. Internet Explorer, for instance, does some major "optimization" to the HTML, removing quotation marks, end tags, etc. Because we cannot quickly and reliably get well-formed XHTML from the container's innerHTML, when the XSLDataGrid initializes, it saves an XML DOM Document made from the original semantic XHTML in memory (aka Dual-DOM). It's also usually easier and more efficient to update this simpler XML DOM when we want to perform change operations than it would be to update the more complex, decorated DOM in the render tree. Let's take the example of resizing a column. Updating the DOM in the render tree would mean updating a great number of the DIVs, SPANs, THs, TDs, COLs etc., all the while taxing our client to render these changes as they're made. In the XML DOM, we change one width attribute's value and then re-run the XSLT. This approach allows us to do all of the "heavy lifting" in our XSLT engine -- get the presentation rules right once in XSL and then leverage it.

XSLT in the Browser

Internet Explorer and Firefox both offer exposed APIs for XSLT, and Manos Batsis has released a free software JavaScript library named Sarissa that wraps up the whole process of loading the XHTML and the XSL, and then calling the browser's native transform method. At the time of this writing, it appears that XSLT is not exposed to JavaScript in Safari or Konqueror. Support for Opera's XSLT API is not yet implemented in the XSLDataGrid.

Client-side sorting is done in the XSLDataGrid by first using DOM to strip out a subset of template nodes from the original XSL file. Then, the current TBODY content is transformed using this subset of the nodes along with a few param sets, and voila! Unfortunately, this sorting technique is limited to the datatypes recognized by XSLT 1.0, which are only "text" and "number." "date" would be pretty useful, and I suspect that I'll work on additional qname stylesheet templates to handle other sort cases in the coming weeks.

Benchmarks

Depending on the iterative nature of your transform or the development cycle in your product, it might make sense to offload the XSL transform to the client. To get a sense of how this performance scales with the XSLDataGrid, take a look at the following metrics table, which was created with some help from the Venkman profiler on my laptop. The server-side metrics are from a GNU/Linux machine with PHP 5.1.4. The client test machine is a 2GHz Pentium M running Mozilla Firefox 1.5.0.6.

Rows Pre-XSLT (kilobytes) Post-XSLT (kilobytes) Client-side XSLT (millisec.) Server-side XSLT (seconds)
200 17.5 29.6 156.25 .0306
500 43.6 68.8 369.79 .0356
1000 87.1 134 781.25 .0860
2000 179.1 270.5 1684.38 .2068
4000 363.1 543.5 3070.31 .3979
8000 731.1 1089.5 6265.63 .7861
20000 1885.1 2787.5 16695.31 4.088

Graph - XSLT in client

Conclusions

The greatest advantage to using XSLT for a JavaScript widget is the flexibility it provides for instantiation. Most Ajax-using web developers will be working with a server-side component/language, and having the option to reduce a client-side JavaScript decoration step to improve performance is nice, though it comes with a bandwidth price. In many projects, developers may be faced with a mixed bag: they may have a need for some large dynamic datagrids, which can only be originated on the server, as well as some smaller hand-coded tables, where a less-rich datagrid would be fine. For instance, developers might not always want to capture the fact that a user changed a column's size and store it as a preference, but even for these less-rich datagrids, developers do want them to look and feel the same. The XSLT approach gives the developer an opportunity to choose either a client- or server-based technique to achieve a similar result.

XSLDataGrid Demos

XSLDataGrid Usage

Requirements

Downloads

Somewhere in your HTML (probably in the <head>) you'll need to include the following, if you haven't already. Also, you'll need to put the XSLDataGrid.xsl file in the same directory as the XSLDataGrid.js file.

<script type="text/javascript" src="PATH_TO/prototype.js"></script>

<script type="text/javascript" src="PATH_TO/scriptaculous/scriptaculous.js"></script>

<script type="text/javascript" src="PATH_TO/sarissa.js"></script>

<script type="text/javascript" src="PATH_TO/XSLDataGrid/Utility.js"></script>

<style type="text/css">@import "PATH_TO/XSLDataGrid/XSLDataGrid.css";</style>

<script type="text/javascript" src="PATH_TO/XSLDataGrid/XSLDataGrid.js"></script>

JavaScript Syntax

Instantiation in JavaScript of the XSLDataGrid is done in much the same way as functions are in Prototype and Scriptaculous.



var myGrid = new XSLDataGrid( 'containerDiv', { option1: value1, option2: value2, etc ... } );
Option Type Default Description
transformer string {client|server} client Where will the XSL transform(s) take place? If "server," then all get requests to "url" are expected to return the transformed XHTML -- i.e., transformed on the server. If "client," then the grid will either transform inline XHTML or get semantic XHTML from "url" and subsequently transform it in the browser. (Note: Client sort is currently limited to XSLT 1.0 datatype limits: text & number - qname are not yet implemented for dates.)
url string (none) An optional URL for fetching either the semantic XHTML or the transformed XHTML from a server (i.e., XSLDataGridTestTransform.php).
extra_parameters string (none) A string of any extra parameters to append to "url" with each get request (i.e., "session_id=lindsey123&haxor=true").
width number 300 Width in pixels.
height number 150 Height in pixels.
prefetch bool true Should the grid perform a get request to "url" on initialization?
gridPopupDivId string gridPopupDivId If you're embedding the grid into an application, you may want to use another absolutely positioned empty div for the right-click popup context menus.
rowReloadLimitOnRearrange number 200 If there is more than this number of rows in the grid, do not try to perform the header rearranging in the browser. In IE, table DOM manipulation with more than 200 rows seems pretty slow to me.
hideColContextMenuDelay number 1000 How long should the right-click context menu stay up after it loses focus (in milliseconds)?
scrollerWidth number 19 Scrollbar width in pixels.
debugging bool false If debugging is set to true, lots of feedback information will be sent to Firefox's awesome Firebug extension console using console.debug().

XHTML Markup

Skeleton of required base markup:



<div id="container_id">

   <table

      class="XSLTable"

      width=""

      height=""

      >

      <thead>

         <tr>

            <th

               id=""

               width=""

               class="SEE TABLE BELOW"

               data-type="optional{text|number}"

               >Column Label

            </th>

         </tr>

      </thead>

      <tbody>

         <tr>

            <td>Column Data</td>

         </tr>

      </tbody>

   </table>

</div>

In the above skeleton, you'll need to fill in width, height, id, and class. Having those width and height values will also help in case JavaScript is disabled in the client.

data-type is optional for client-side XSL sorting. Right now the client-side sort technique is purely XSLT (only number and text sorting). I've not written any special sort routines for other (qname) datatypes, like dates, but I welcome suggestions.

Currently, width and height must be numbers (in pixels), as opposed to percentages.

THEAD/TR/TH class: You can use all or any of the following per column:

Option Description
sortable Include this class if the column can be sorted and grouped on. If you're implementing server-side XSLT, you'll need to look at the sort and group parameters being sent in the URL.
rearrageable Include this class if the column can be reordered via drag-and-drop.
resizable Include this class if the column can be resized.
filterable Include this class if the column can be filtered on.
grouped This will automatically group the results by this column. It's a little odd to do this in the initial load, as opposed to using the right-click column menu, but it's here.

Footnotes

[1] Examples/resources for using XSLT on XHTML

[2] AHAH Microformat