Introduction
XSL is a declarative programming language. Variables that are once declared cannot be reassigned. It is often difficult for programmers coming from procedural languages background to do advanced tasks. Using XSL, we can solve complex problems, which at first glance often seem to be difficult or impossible. In this brief article, I will demonstrate the recursive nature of templates. In a typical case of a product catalog display, our requirement is to display the product details in each cell, and number of columns should be selectable by the user. The sample XML file is listed below:
<data>
<product name="Heine HKL with trolleystand"
price="230.45" weight="34.4kg" />
<product name="Universal clamp and Projector"
price="670.45" weight="10.64kg" />
<product name="Examination Lamp, Universal Mount"
price="25.45" weight="1.08kg" />
<product name="Provita Examination Lamp, Mobile Base"
price="215.45" weight="1.4kg" />
<product name="35Watt Flexible Arm Light to fit Rail system"
price="130.45" weight="11.67kg" />
.
.
.
.
.
.
</data>
Assuming we are getting the above from a business component, each element row corresponds to a product, whose attributes comprises of product specific data. Each product along with its details will be rendered in a single cell. And the number of columns should be definable at runtime. Following is the brief XSL which does the rendering:
="1.0"="utf-8"
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:param name="numCols" select="3"/>
<table cellpadding="0" cellspacing="5" border="0">
<xsl:call-template name="renderColumns">
<xsl:with-param name="listrows" select="//product"/>
<xsl:with-param name="startindex" select="1"/>
<xsl:with-param name="numofCols" select="$numCols"/>
</xsl:call-template>
</table>
</xsl:template>
<xsl:template name="renderColumns">
<xsl:param name="listrows" />
<xsl:param name="startindex"/>
<xsl:param name="numofCols"/>
<xsl:if test="count($listrows) > 0">
<tr>
<xsl:apply-templates
select="$listrows[position() >=
$startindex and position() <
($startindex+$numofCols)]" mode="rows">
</xsl:apply-templates>
</tr>
<xsl:call-template name="renderColumns">
<xsl:with-param name="listrows"
select="$listrows[position() >= $startindex+$numofCols]"/>
<xsl:with-param name="startindex" select="$startindex"/>
<xsl:with-param name="numofCols" select="$numofCols"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="node()" mode="rows">
<td nowrap="true">
<table style="width:100%;border-right: thin solid;
border-top: thin solid; border-left:
thin solid; border-bottom: thin solid;">
<xsl:apply-templates select="@*"/>
</table>
</td>
</xsl:template>
<xsl:template match="@*">
<tr>
<td style="font-size: larger; text-transform:
uppercase; background-color: gainsboro">
<xsl:value-of select="name()"/>
</td>
<td>
<xsl:value-of select="."/>
</td>
</tr>
</xsl:template>
</xsl:stylesheet>
Explanation
<xsl:template match="/">
<xsl:param name="numCols" select="3"/>
As would a C or C++ programmer organize and reuse the code using functions or object methods, in XSL, we can organize code using templates. The above code is the root template which will be invoked by the XSLT processor. We are declaring a parameter using xsl:param
, whose name is numCols
. The user can pass this parameter; if the user is not supplying any value in this parameter, then, by default, it will have a value of 3. This variable will specify the number of columns to be rendered.
<xsl:call-template name="renderColumns">
<xsl:with-param name="listrows" select="//product"/>
<xsl:with-param name="startindex" select="1"/>
<xsl:with-param name="numofCols" select="$numCols"/>
</xsl:call-template>
We are calling the renderColumns
template, to which we are passing three parameters. In listrows
, we are selecting all product elements, startindex
signifies the starting index, and numofCols
will control the number of rows to be rendered.
<xsl:template name="renderColumns">
<xsl:param name="listrows" />
<xsl:param name="startindex"/>
<xsl:param name="numofCols"/>
<xsl:if test="count($listrows) > 0">
<tr>
<xsl:apply-templates
select="$listrows[position() >=
$startindex and position() <
($startindex+$numofCols)]"
mode="rows">
</xsl:apply-templates>
</tr>
<xsl:call-template name="renderColumns">
<xsl:with-param name="listrows"
select="$listrows[position() >= $startindex+$numofCols]"/>
<xsl:with-param name="startindex" select="$startindex"/>
<xsl:with-param name="numofCols" select="$numofCols"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
In the XSL template renderColumns
, we are selecting the elements whose position will be greater than or equal to the starting index and whose position is less than or equal to the sum of startindex
and numofcols
. After rendering these subset of elements, we are recursively calling renderColumns
by selecting a subset of elements whose position is greater than the sum of startindex
and numofCols
, which are already rendered. For exiting this recursive loop, we have a test condition at the start which checks for the count of elements in the listrows
variable. As we are selecting only those elements which are yet to be rendered while calling recursively, the set of nodes by each call will be reduced by the number of elements rendered. For rendering rows, in this call template, we are using the following template:
<xsl:template match="node()" mode="rows">
<td nowrap="true">
<table style="width:100%;border-right: thin solid;
border-top: thin solid; border-left:
thin solid; border-bottom: thin solid;">
<xsl:apply-templates select="@*"/>
</table>
</td>
</xsl:template>
in which we are converting the attribute nodes into elements and calling another template:
<xsl:template match="@*">
<tr>
<td style="font-size: larger; text-transform:
uppercase; background-color: gainsboro">
<xsl:value-of select="name()"/>
</td>
<td>
<xsl:value-of select="."/>
</td>
</tr>
</xsl:template>
which does the job of rendering the product details in a cell.
Conclusion
Even though variables are constants through out the life time of a variable in XSL, we can achieve things which, at first glance, look impossible due to the declarative nature of XSL. Upon close look and thinking in a declarative manner, we can solve the problem.