An alternative 'for' loop using XSLT and template callbacks
20th October 2009, one comment, tagged with Symphony, XSLTXSLT 1.0 has no native way of achieving a for loop, without iterating through XML. Until now.
XSLT is planted firmly in the declarative programming style — something that regularly frustrates newcomers from procedural languages such as PHP or JavaScript. You need to change your programming mindset to approach familiar concepts in new ways. One such example is the traditional for loop. In XSLT there exists a for-each element which iterates over an XML nodeset:
<xsl:for-each select="countries/country">
<xsl:value-of select="text()"/>
</xsl:for-each>
As dandy as this is, it relies on having XML to iterate over. How can you achieve a for loop to provide, say, ten iterations, from 1 to 10. In PHP this would be a simple as:
for($i=1; $i<=10; $i++) {
echo($i);
}
To achieve this using XSLT you need to take a different approach. Below I’ll outline two ideas for generating a for loop using XSLT 1.0 — one a simple hack, the other more advanced and elegant.
1. Tokens at the ready
Using the tokenize() function of the EXSLT strings library one can break a string of a given length into individual characters and loop through each:
<xsl:for-each select="str:tokenize('..........')/token">
<xsl:value-of select="position()"/>
</xsl:for-each>
There are ten full-stops in the string, meaning that ten token nodes will be created. Not particularly elegant or maintainable. This can be improved on slightly by generating the string dynamically using the EXSLT padding() function, making it easier to set its length.
<xsl:for-each select="str:tokenize(str:padding(10,'.'))/token">
<xsl:value-of select="position()"/>
</xsl:for-each>
2. Recursive to the max
The more ‘XSL-like’ approach is to use a recursive template for the iteration. The template is supplied with start and end conditions and repeatedly calls itself until the end condition is met. The finest explanation I’ve seen is Loop with recursion in XSLT on IBM developerWorks. Recommended reading.
Writing a recursive template to achieve the above requirement is relatively trivial:
<xsl:template name="for-loop">
<xsl:param name="count" select="1"/>
<xsl:if test="$count > 0">
<xsl:value-of select="$count"/>
<xsl:call-template name="for-loop">
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
The problem here is that the template output is muddled into the same template as the iteration/recursion logic. It would make more sense to have a single “iterator” template to perform the recursion, and have the output rendered in a separate template. Consider the following scenario:
You’re building a form and need to fill a select drop down with ten options (1–10), but you also need to render five checkboxes. Using the above example you would need to replicate the template twice, naming them select-loop and checkbox-loop. Inside each respective template you would change the output to return either an <option> element or a checkbox.
My colleague Paul Garrett and devised a cunning way for a call-template to execute a callback template which is where the custom rendering is achieved.
Consider this markup to render a select box with ten options, followed by five checkboxes:
<xsl:template match="data">
<select>
<xsl:call-template name="iterator">
<xsl:with-param name="iterations" select="'10'"/>
<xsl:with-param name="callback" select="'render-option'"/>
</xsl:call-template>
</select>
<xsl:call-template name="iterator">
<xsl:with-param name="iterations" select="'5'"/>
<xsl:with-param name="callback" select="'render-checkbox'"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="render-option" mode="iterator-callback">
<xsl:param name="position"/>
<option>
<xsl:value-of select="$position"/>
</option>
</xsl:template>
<xsl:template match="render-checkbox" mode="iterator-callback">
<xsl:param name="position"/>
<input type="checkbox" name="fields[checkbox-{$position}]"/>
</xsl:template>
We can call a generic iterator template passing the number of iterations required (as you would a traditional for loop) while providing the name of a callback template. The iterator template creates an XML node of the same name and selects it using an apply-templates. The callback template is passed the position in the loop providing context.
The full iterator template:
<xsl:template name="iterator">
<xsl:param name="start" select="'1'"/>
<xsl:param name="iterations" select="$iterations"/>
<xsl:param name="direction" select="'+'"/>
<xsl:param name="callback"/>
<xsl:param name="callback-params"/>
<xsl:param name="count" select="$iterations"/>
<xsl:if test="$callback and $count > 0">
<xsl:variable name="position">
<xsl:choose>
<xsl:when test="$direction='-'"><xsl:value-of select="$start - ($iterations - $count)"/></xsl:when>
<xsl:otherwise><xsl:value-of select="$start + ($iterations - $count)"/></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="callback-node">
<xsl:element name="{$callback}">
<xsl:if test="$callback-params">
<xsl:copy-of select="$callback-params"/>
</xsl:if>
</xsl:element>
</xsl:variable>
<xsl:apply-templates select="exsl:node-set($callback-node)" mode="iterator-callback">
<xsl:with-param name="position" select="$position"/>
</xsl:apply-templates>
<xsl:call-template name="iterator">
<xsl:with-param name="start" select="$start"/>
<xsl:with-param name="iterations" select="$iterations"/>
<xsl:with-param name="direction" select="$direction"/>
<xsl:with-param name="callback" select="$callback"/>
<xsl:with-param name="callback-params" select="$callback-params"/>
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Additional parameters can be passed for more advanced iterations and callbacks:
start allows you to start the iteration from a specific number, defaults to 1:
<xsl:with-param name="start" select="'3'"/>
iterations is the number of iterations to perform:
<xsl:with-param name="iterations" select="'100'"/>
direction allows you to count down (“-“) rather than the default up (“+”)
<xsl:with-param name="direction" select="'-'"/>
callback is the name of the XML node created to match on with your callback template
<xsl:with-param name="callback" select="'my-template'"/>
callback-params accepts XML which are then passed to the callback template
<xsl:with-param name="callback-params">
<name>Nick Dunn</name>
</xsl:with-param>
<xsl:template match="render-user" mode="iterator-callback">
<xsl:param name="position"/>
<p>Name: <xsl:value-of select="name"/></p>
</xsl:template>
(Note: you will need to include the EXSLT common namespace in your stylesheet to use the node-set function in the template above!)
Conclusion
We have seen the different ways of achieving a numeric for loop in XSLT, and in doing so created a re-usable recursive template with a dynamic callback. I’ve not seen this callback method documented anywhere else, so this may be a new technique that has novel applications beyond this example. Recurse to the max, don’t tokenize.
There has been one comment. Add yours »
Oh my wowz! it’s so good. im love it. its real for us :)