Recursive XSLT For Nested XML Nodes In ColdFusion

Posted December 11, 2007 at 7:01 PM by Ben Nadel

Tags: ColdFusion

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:

  • <!--- Define the nested tasks. --->
  • <cfxml variable="xmlTasks">
  •  
  • <tasks>
  •  
  • <task>
  • <name>Breakfast</name>
  • <task>
  • <name>Cook Eggs</name>
  •  
  • <task>
  • <name>Crack Eggs</name>
  •  
  • <task>
  • <name>Crack Egg 1</name>
  • </task>
  • <task>
  • <name>Crack Egg 2</name>
  • </task>
  • <task>
  • <name>Crack Egg 3</name>
  • </task>
  • <task>
  • <name>Crack Egg 4</name>
  • </task>
  • </task>
  • <task>
  • <name>Beat Eggs</name>
  •  
  • <task>
  • <name>Beat Clockwise</name>
  •  
  • <task>
  • <name>Beat</name>
  • </task>
  • <task>
  • <name>Beat</name>
  • </task>
  • <task>
  • <name>Beat</name>
  • </task>
  • </task>
  • <task>
  • <name>Beat Counter Clockwise</name>
  •  
  • <task>
  • <name>Beat</name>
  • </task>
  • <task>
  • <name>Beat</name>
  • </task>
  • <task>
  • <name>Beat</name>
  • </task>
  • </task>
  • </task>
  • <task>
  • <name>Put Eggs In Pan</name>
  • </task>
  • <task>
  • <name>Put Eggs on Plate</name>
  • </task>
  • </task>
  • <task>
  • <name>Eat Eggs</name>
  • <task>
  • <name>Put In Mouth</name>
  •  
  • <task>
  • <name>Put On Fork</name>
  • </task>
  • <task>
  • <name>Move For To Mouth</name>
  • </task>
  • </task>
  • <task>
  • <name>Chew Eggs</name>
  • </task>
  • <task>
  • <name>Swallow</name>
  • </task>
  • </task>
  • </task>
  •  
  • </tasks>
  •  
  • </cfxml>

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:

  • <!--- Transform the XML and output the result. --->
  • <cf_xslt xml="#xmlTasks#">
  •  
  • <!--- Document type declaration. --->
  • <?xml version="1.0" encoding="ISO-8859-1"?>
  •  
  • <xsl:transform
  • version="1.0"
  • xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  •  
  •  
  • <!---
  • Start out by matching the root element. This will
  • prevent the task node template from matching on
  • its own (we only want task nodes to match when we
  • explicitly apply templates).
  • --->
  • <xsl:template match="tasks">
  •  
  • <!--- Begin our initial task list. --->
  • <ol>
  • <!---
  • Now, apply the task template to all
  • top-level tasks in the task list.
  • --->
  • <xsl:apply-templates select="task" />
  • </ol>
  •  
  • </xsl:template>
  •  
  •  
  • <!--- Matches a task node. --->
  • <xsl:template match="task">
  •  
  • <!--- Output the task. --->
  • <li>
  • <span>
  • <xsl:value-of select="name" />
  • </span>
  •  
  • <!---
  • Check to see if there are any sub tasks
  • that need their own list.
  • --->
  • <xsl:if test="task">
  •  
  • <!---
  • Start a new sub-task list and then
  • RECURSIVELY apply the task template
  • to each of the sub-task nodes.
  • --->
  • <ol>
  • <xsl:apply-templates select="task" />
  • </ol>
  •  
  • </xsl:if>
  • </li>
  •  
  • </xsl:template>
  •  
  •  
  • </xsl:transform>
  •  
  • </cf_xslt>

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:

  1. Breakfast
    1. Cook Eggs
      1. Crack Eggs
        1. Crack Egg 1
        2. Crack Egg 2
        3. Crack Egg 3
        4. Crack Egg 4
      2. Beat Eggs
        1. Beat Clockwise
          1. Beat
          2. Beat
          3. Beat
        2. Beat Counter Clockwise
          1. Beat
          2. Beat
          3. Beat
      3. Put Eggs In Pan
      4. Put Eggs on Plate
    2. Eat Eggs
      1. Put In Mouth
        1. Put On Fork
        2. Move For To Mouth
      2. Chew Eggs
      3. Swallow

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.




Reader Comments

Dec 12, 2007 at 7:29 AM // reply »
25 Comments

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.


Dec 12, 2007 at 7:51 AM // reply »
10,638 Comments

@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.


Dec 12, 2007 at 4:44 PM // reply »
1 Comments

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.


Dec 12, 2007 at 4:50 PM // reply »
10,638 Comments

@Ken,

Good timing indeed. If you run into any problems, let me know.


May 2, 2008 at 5:33 AM // reply »
1 Comments

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>


Mar 17, 2009 at 3:27 PM // reply »
2 Comments

Hi Ben

Your post is very smart, i have a recursive logic using java objects and i was looking for to adept it to a web infrastructure using xml, i just had to make some adjusts on your post to adequate to my need.
...Here go an unlimited tree menu structure using that xml:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>

<xsl:template match="tasks">
<xsl:apply-templates select="task" />
</xsl:template>

<xsl:template match="task">

<xsl:if test="task=false()">
<li>
<a>
<xsl:attribute name="href">
<xsl:value-of select="href" />
</xsl:attribute>

<xsl:value-of select="name" />
</a>
</li>
<xsl:apply-templates select="task" />
</xsl:if>

<xsl:if test="task=true()">

<li>
<a>
<xsl:attribute name="href">
<xsl:value-of select="href" />
</xsl:attribute>

<xsl:value-of select="name" />
</a>
<ul>
<xsl:apply-templates select="task" />
</ul>
</li>
</xsl:if>
</xsl:template>
</xsl:stylesheet>


Mar 25, 2009 at 10:14 PM // reply »
10,638 Comments

@Marco,

Looks good.


Viv
Apr 8, 2009 at 8:46 AM // reply »
1 Comments

How do we process the nested tags for same value. For e.g.

<Style value="Italics">
<Style value="Bold">
<Style value="Underline>
Text
</Style>
</Style>
</Style>

I want the output to be

<span style="font-style:italic; font-weight:bold; text-decoration:underline>Text</span>


Apr 8, 2009 at 9:32 AM // reply »
2 Comments

@Viv,

Just make a research on how to use style or css (are the same) plus javascript plus html code, if i try to explain it here...i will make some mess.
there is some way to use css inside xsl, but i didn' went so deep on it yet.

for example:
...html code above
<div id="id" class="style_class">
<< your html code >>
</div>

<style>
.style_class ul{ setups}
.style_class ul li {setups}
.style_class ul li a{setups}
.style_class ul li ul li a{setups}
...and so on
</style>
..html code below

your div tag you can populate it using javascript reading xml plus xsl and the style class you configure it acording to the html tags that was declared.
if you have a simple html page it is easy to work straight in the html page, but if you have a more complex html page or xsl structure maybe it will be easier to setup it using xsl.


Nov 19, 2009 at 4:05 AM // reply »
4 Comments

swwweeet. i figured it was possible. tried a for-each with no luck


Nov 19, 2009 at 10:11 AM // reply »
10,638 Comments

@AB,

Awesome.


Don
Dec 21, 2009 at 5:53 PM // reply »
57 Comments

HAH! Your nightmare has returned. I mentioned this once but now I'm really digging in and hoping somebody knows how to do this already to save me the hassle.
This deals with Amazon's api. When you get a product you can get their "browsenodes" output. But what they do is start from the leaves and work to the root. So it goes like this for a Tarzan figurine:
Collectible Figurines
Home Decor Accents
Home Decor
Furniture & Decor

And so on to the root Home & Garden node. And then it starts on the next one. What needs to be done is invert this.

Obviously I can just keep digging into each node until I find everything but what a pain.

Any suggestions?


Dec 22, 2009 at 8:36 AM // reply »
10,638 Comments

@Don,

I'm sorry, I am not sure what you're question actually is. What information are you trying to get at?


Oct 4, 2011 at 4:01 AM // reply »
1 Comments

Thanks a lot, I was looking for recursion of nodes and it helped me a lot. thanks again



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 3, 2012 at 10:49 PM
How I Got Node.js Running On A Linux Micro Instance Using Amazon EC2
Wow this was really helpful! Only thing I would add is you need to update your .bash_profile after you edit the secure_path. This is what I did: $ . ~/.bash_profile Otherwise, NPM won't be found. ... read »
Feb 3, 2012 at 10:14 PM
Pushing Base64-Encoded Images Over HTML5 WebSockets With Pusher And ColdFusion
@Ben, Just wanted to let you know that pusher are soon to start limiting sizes on messages. This was the detail that came through in the Feb dispatch: "However, we will soon be limiting the s ... read »
Feb 3, 2012 at 5:05 PM
Regular Expressions Make CSV Parsing In ColdFusion So Much Easier (And Faster)
I tried using your RegEx in my C# program, but it was matching an extra empty-string at the end and so I would end up with an extra field that doesn't exist, so I changed it to this: (^|,)("(?: ... read »
Feb 3, 2012 at 3:47 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
Josh Cyr posted this on Twitter just a little bit ago. Thought it was appropriate. http://stackoverflow.com/questions/1619152/how-to-create-rest-urls-without-verbs/1619677#1619677 ... read »
Feb 3, 2012 at 2:28 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
@Michael, You definitely make a good point (and extra points for quoting movies - I love movies). When you use a return() statement to define the object's public API, it does provide a consistent a ... read »
Feb 3, 2012 at 2:04 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
To quote Jurassic Park: "Just because you can doesn't mean you should". I completely, utterly disagree with the thought that this is more readable. Consider the current module pattern: if ... read »
Feb 3, 2012 at 1:10 PM
REST API Design Rulebook By Mark Masse
@Jordan, Yeah, WRML was created by Mark Masse (author of the book). I also found it to be a bit convoluted. I suppose it is intended to allow the Client to be able to programmaticaly respond to cha ... read »
Feb 3, 2012 at 1:08 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
@Jason, To be honest, I don't have good answers for that kinds of stuff. And, to the point, that is specifically why I *really* liked the REST API Design Rulebook by Mark Masse - he just cuts throu ... read »