June 22, 2005
With XSLT support fast becoming a commonly available component in the browser, web developers can now leverage transformations to manipulate large amounts of data in the browser at speeds acceptable for more advanced user interfaces. Once Safari gets its act together, I see more and more UI-specific data processing being moved off of the server into the browser.
This article outlines the process involved in transforming the del.icio.us user API XML document into an HTML fragment. Because the XSLT processor is compiled, and therefore runs significantly faster than interpreted Javascript in the browser, I push as much of the HTML manipulation as I can to the processor. By assigning the HTML generation to the processor, I avoid using Javascript to iteratively create a final DOM table node. Instead, the application simply takes the finished output from the processor and inserts it into the document in one pass.
The XML is loaded using a standard XmlHTTPRequest object. A mature product would certainly use an asynchronous call with a callback — or something like Sarissa — to enable a more fluid UI and better contingency design. Here is a sample of the XML data:
<?xml version='1.0' standalone='yes'?>
<posts update="2005-05-08T00:31:11Z" user="johnvey">
<post href="http://developer-test.mozilla.org/docs/The_XSLT/JavaScript_Interface_in_Gecko:JavaScript/XSLT_Bindings" description="The XSLT/JavaScript Interface in Gecko:JavaScript/XSLT Bindings - Devmo" hash="f3b5eba5910f9ee34a444dc05c929ada" tag="javascript xml xslt" time="2005-05-08T00:31:04Z" />
<post href="http://www.textpattern.com/" description="Textpattern" hash="24032aec5555f2361ebf683f9214fb0f" tag="cms blog php" time="2005-05-07T06:37:16Z" />
<post href="http://www.magnolia.info/en/community.html" description="Magnolia Content Management (CMS) Community Page" hash="ebc95d99d6b19f14131e4f655f4e9de1" tag="cms java" time="2005-05-07T06:23:36Z" />
<post href="http://oscom.org/" description="OSCOM - Open Source Content Management" hash="8faafe200e575551ed06b6a3880ca677" tag="cms review" time="2005-05-07T06:17:31Z" />
</posts>
The first step is creating an XSL stylesheet that transforms the XML data into a sorted and filtered HTML fragment:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="posts">
<table>
<xsl:for-each select="post[contains(@tag,'my_search_term') or contains(@description,'my_search_term')]">
<xsl:sort select="@time" order="descending" />
<xsl:if test="position() < 100">
<tr>
<td>
<a href="{@href}" title="{@hash}" target="_blank"><xsl:value-of select="@description" /></a>
<br />
<xsl:value-of select="@extended"/>
</td>
<td><xsl:value-of select="@tag" /></td>
<td><xsl:value-of select="@time"/></td>
</tr>
</xsl:if>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
The key lines in the stylesheet are the:
This seemingly trivial piece of XSL is actually responsible for the majority of the work, and because of the speed of the XSLT processor, can perform the filtering in a fraction of the time of a comparable Javascript function over large data sets. Note that the <xsl:sort> tag comes after the <for:each> — this ensures that the processor sorts the data after the filtering.
In order to implement the live filter-as-you-type feature, I cheated and used Javascript to generate the <xsl:for-each> select clause. The stylesheet is actually declared as a string in the Javascript code, and passed directly to the XSLT processor. Because the stylesheet is dynamically generated, I am able to inject arbitrary XPath queries into the select clause. The actual XPath select statement then becomes a dynamic Javascript string concatenation:
xslAsString += '<xsl:for-each select="' + xslSelect + '">';
Doing so allows me to avoid using passed parameters and insert as many different conditions as I want into the statement. I'm sure that seasoned XSL hackers could come up with a single stylesheet that supported multiple search terms and operators to replace this kludge.
Now that the XML data and the XSL stylesheet is loaded, instantiate an XSLT processor to do the actual transformation from XML into HTML:
// TODO: get the XSL stylesheet as a string var myXslStylesheet; // TODO: get the XML DOM data document var myXmlData; // init a processor var myXslProc; // init the final HTML var finishedHTML = ""; // create a XSLT processor if(document.implementation.createDocument) { // Mozilla has a very nice processor object myXslProc = new XSLTProcessor(); // convert the XSL to a DOM object first var parser = new DOMParser(); myXslStylesheet = parser.parseFromString(myXslStylesheet, "text/xml"); // attach the stylesheet; the required format is a DOM object, and not a string myXslProc.importStylesheet(myXslStylesheet); // do the transform (domDocument is the current HTML page you're on) var fragment = myXslProc.transformToFragment(myXmlData, domDocument); // create a DOM container and insert offline var tmpBox = document.createElement("div"); tmpBox.appendChild(fragment); // grab the innerHTML and write to output, and insert into HTML document finishedHTML = tmpBox.innerHTML; } else { // IE requires a couple more hoops to jump through // first create a DOM document var xslDoc = new ActiveXObject("Msxml2.FreeThreadedDOMDocument"); // then create an XSLT template document var xslTemplate = new ActiveXObject("Msxml2.XSLTemplate"); xslDoc.async = false; // load the stylesheet into the DOM document xslDoc.loadXML(myXslStylesheet); // attach that DOM document to the XSLT template xslTemplate.stylesheet = xslDoc; // get the rental-model XSLT processor object myXslProc = xslTemplate.createProcessor(); // feed it the XML data myXslProc.input = myXmlData; // do the transform myXslProc.transform(); // grab the output, and insert into HTML document finishedHTML = myXslProc.output; }
The finishedHTML generated by the above code is the complete HTML table that contains the entire listing of bookmarks that match my_search_term, sorted by reverse timestamp. The efficiency of offloading the filtering and sorting to the XSLT processor becomes apparent when dealing with very large records sets.
The final version of the XSL stylesheet used by direc.tor adds the following features:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="sortOrder" />
<xsl:param name="sortColumn" />
<xsl:param name="lc" select="\'abcdefghijklmnopqrstuvwxyz\'" />
<xsl:param name="uc" select="\'ABCDEFGHIJKLMNOPQRSTUVWXYZ\'" />
<xsl:template match="posts">
<table>
<colgroup>
<col style="width: 1%" />
<col style="width: 69%" />
<col style="width: 15%" />
<col style="width: 15%" />
</colgroup>
<thead>
<tr>
<th></th>
<th id="c_description" onclick="rd.sort(\'description\')">Description</th>
<th id="c_tag" onclick="rd.sort(\'tag\')">Tags</th>
<th id="c_time" onclick="rd.sort(\'time\')">Time</th>
</tr>
</thead>
<tbody>
<xsl:for-each select="' + xslSelect + '">
<xsl:sort select="@*[name() = $sortColumn]" order="{$sortOrder}" />
<xsl:if test="position() < 100">
<tr onmouseover="hl(this)" onmouseout="nl(this)">
<td>
<a href="#" onclick="return(rd.editLink(\'{@href}\'))" title="Edit this link" class="x">Edit</a>
</td>
<td>
<a href="{@href}" title="{@hash}" target="_blank"><xsl:value-of select="@description" /></a>
<br />
<xsl:value-of select="@extended"/>
</td>
<td><xsl:value-of select="@tag" /></td>
<td><xsl:value-of select="@time"/></td>
</tr>
</xsl:if>
</xsl:for-each>
</tbody>
</table>
</xsl:template>
</xsl:stylesheet>
Again, there are probably 32 ways to do this in XSL, so I leave it up to the reader to figure this one out.
Once finished, the finishedHTML string is post-processed by the search term highlighter, and then inserted into the online DOM tree. When the user updates the search text box, the HTML table is destroyed, and this entire XSL process is executed again.