Earlier today, Jacob Munson brought up the task of outputting an XML menu (as HTML) without knowing ahead of time how deeply nested the menus and sub menus might be. As I have been doing a lot with both recursion and XSLT (XML transformations) in ColdFusion lately, it was the first thing that popped into my mind. XSLT is not the friendliest language to learn, so I thought I would put up a little example of recursion in XSLT and ColdFusion.
Instead of working with a menu, I am going to work with a task list. This is basically the same thing, but I simply couldn't think of any good menu examples. Here, we are going to define a set of tasks by breaking them down into smaller and smaller sub tasks. This XML document will be stored inside of a ColdFusion XML document object created using the CFXML tag:
Launch code in new window » Download code as text file »
As you can see, our only root Task is eating breakfast. This task is then sub-divided into smaller, more manageable tasks. Each of those tasks may or may not be further sub-divided, so on and so forth.
Now, we want to take that ColdFusion XML document object and transform it into some XHTML. Of course, since we don't know how deeply nested the XML DOM tree is going to be, we have to provide a way to continually traverse it in a depth-first manner until we hit a leaf and then continue on to the next available branch until all task nodes have been explored. And, as we have seen from previous posts, this is an ideal situation for recursion.
Our recursion will define the current Task node by outputting the name and then outputting each of the child task nodes. Each of the child task nodes will be output by the same method (or template in our case). So, in typical recursion style, our XSLT template for Task is used to define itself.
Recursion is hard to visualize, so let's just take a look at the XSLT. This XSLT is transformed using my XSLT ColdFusion custom tag, which basically creates the XML transformation and executes it all in one go:
Launch code in new window » Download code as text file »
Our XSLT first matches the Tasks XML node. This template outputs the first ordered list element (OL) and then applies the Task template to each of the top-level task nodes. Then, for each Task node, we output the name and recursively re-apply the Task template to each of the sub-tasks. Remember, the whole point of recursion is that some piece of functionality is defined in part by self-executing.
Running the above code, we get the following output:
All in all, the XSLT for a problem like this is extremely small; possibly smaller than the functionally equivalent ColdFusion recursion you might see. Now, in our task list, we just output a task name, but certainly, it would be a small jump to create a menu system that had Text and Href nodes that created links as part of the menu definitions.
Download Code Snippet ZIP File
Comments (5) | Post Comment | Ask Ben | Permalink | Other Searches | Print Page
Hi Ben,
Great post. I had taken a look at XSLT about a year ago, but dropped it due to lack of time and having the impression that it was more complicated than it was worth.
This post, however, sends me back to my books. Thanks for taking the time to post this.
Posted by Francois Levesque on Dec 12, 2007 at 7:29 AM
@Francois,
If you are interested, I wrote up a little tutorial a while:
http://www.bennadel.com/index.cfm?dax=blog:952.view
I am only recently into XSLT, but it seems like a really powerful tool if given the right scenario.
Posted by Ben Nadel on Dec 12, 2007 at 7:51 AM
Perfect timing! i was just starting to work on this exact problem. You just saved me a ton of time. :) I'm pretty comfortable with XPath (thanks to jQuery) but XSLT has always been a bit of a hurdle. Great post.
Posted by Ken Dunnington on Dec 12, 2007 at 4:44 PM
@Ken,
Good timing indeed. If you run into any problems, let me know.
Posted by Ben Nadel on Dec 12, 2007 at 4:50 PM
I have been using xslt for menu's lately. Previously to get the hierarchical tree of <ul> and <li> I had to use recursive functions containing queries which meant hundreds of queries being triggered. To get round this I created a flat node structure e.g.
<nodes>
<cfquery name="getNodes">
<node nodeID="#nodeID#" nodeParentID="#nodeParentID#" nodeName="#nodeName#" />
</cfquery>
</nodes>
Then using the XSLT I transformed this structure into a hierarchical tree of nodes.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="nodes">
<!-- Starts the tree -->
<ul>
<xsl:apply-templates/>
</ul>
</xsl:template>
<xsl:template match="//node[@nodeParentID=0]">
<xsl:call-template name="process-branch">
<xsl:with-param name="nodeID"><xsl:value-of select="@nodeID"/></xsl:with-param>
<xsl:with-param name="nodeParentID"><xsl:value-of select="@nodeParentID"/></xsl:with-param>
<xsl:with-param name="nodeName"><xsl:value-of select="@nodeName"/></xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template name="process-branch">
<xsl:param name="nodeID"/>
<xsl:param name="nodeParentID"/>
<xsl:param name="nodeName"/>
<xsl:choose>
<xsl:when test="count(//node[@nodeParentID=current()/@nodeID])=0">
<li><xsl:value-of select="@nodeName" /></li>
</xsl:when>
<xsl:otherwise>
<li><xsl:value-of select="@nodeName" />
<ul>
<xsl:for-each select="//node[@nodeParentID=current()/@nodeID]">
<xsl:call-template name="process-branch">
<xsl:with-param name="nodeID"><xsl:value-of select="@nodeID"/></xsl:with-param>
<xsl:with-param name="nodeParentID"><xsl:value-of select="@nodeParentID"/></xsl:with-param>
<xsl:with-param name="nodeName"><xsl:value-of select="@nodeName"/></xsl:with-param>
</xsl:call-template>
</xsl:for-each>
</ul>
</li>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Posted by Matt on May 2, 2008 at 5:33 AM