The XSLDataGrid: XSLT Rocks Ajax
August 23, 2006
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 DIV
s and float:left
s 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 XHTML Table |
+ XSLDataGrid.xsl | = | Decorated XHTML More tags, attributes, CSS, etc ... |
+ | Javascript Instantiation, event listeners, etc ... |
= |
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:
- by fetching the transformed, fully decorated XHTML each time from the server
- by fetching semantic XHTML from the server and transforming it on the client
- 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 DIV
s, SPAN
s, TH
s,
TD
s, COL
s 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 |
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
- Dynamic XSLDataGrid Test
- Client Transform, XHTML from server
- Client Transform, XHTML on page (Multiple Grids)
XSLDataGrid Usage
Requirements
Downloads
- prototype (Sam Stephenson)
- scriptaculous (Thomas Fuchs)
- sarissa (Manos Batsis)
- XSLDataGrid.css (Lindsey Simon)
- XSLDataGrid.js (Lindsey Simon)
- XSLDataGrid.xsl (Lindsey Simon)
- Utility.js (Lindsey Simon)
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