del.icio.us direc.tor: Using XSLT to Filter and Sort Records in the Browser

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.

Loading the XML

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>
	

Creating the Basic XSL Stylesheet

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:

  • <xsl:for-each> tag that returns only the posts that have the the phrase my_search_term in either the tags or description field
  • <xsl:sort> tag that determines the sort order of the returned posts
  • <xsl:if> tag that caps the returned results to 100

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.

Loading the XSLT Processor

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.

Advanced XSL Stylesheet

The final version of the XSL stylesheet used by direc.tor adds the following features:

  • Case-insensitive search
  • Selectable sort column and order
  • Bookmark edit link
<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.

Updating the UI

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.

« Return to del.icio.us direc.tor