A frequently requested transform is placing a list of items into a grid or table. For instance, given a list of product names, you might want to present them two across in a table, like this:
product 1 | product 2 |
product 3 | product 4 |
Items need to be grouped in pairs into table rows. XSL currently does not have a built-in mechanism to handle arbitrary grouping. A combination of the <xsl:if> element with the expr script escape and the context method can be used to approximate this behavior. The following template turns a list of "product" elements into a two-column table.
<TABLE BORDER="1">
<xsl:for-each select="products/product">
<xsl:if expr="(childNumber(this) % 2) == 1">
<TR>
<TD><xsl:value-of /></TD>
<TD><xsl:value-of select="../product[index() $gt$ context()!index()][0]"/></TD>
</TR>
</xsl:if>
</xsl:for-each>
</TABLE>
How does this work? First, you iterate through each product element but use an <xsl:if> with the script expression "childNumber(this) % 2" to isolate only those products that should start a new row (every other product). The name of this product also is placed in the first cell in the row. Then you make another cell and place within it the next child—the one filtered out using the <xsl:if>. The query to select the next element navigates to the parent and selects all products with an index greater than the index of the current node (the "context()") to obtain a set of following siblings. This set is trimmed by the subscript "[0]" to select just the immediately following or "next" product element.
The context() method is the key to this query, since it provides access to the node selected by the previous query. An optimal parameter provides access to the entire path of nodes that led to the execution of the current query. Positive numbers walk down the path from the root of the transformation, which is indicated by context(0); negative numbers walk up the path from the current query. The default value of the parameter is -1, which indicates the node selected by the previous query. This node of course provides the context for the current query.
Three-row tables can be obtained by adding another cell to the row to obtain another sibling element, and adjusting the expression to allow only every third element to create a new row. You can extrapolate this mechanism to create tables with even more columns.
<TABLE BORDER="1">
<xsl:for-each select="products/product">
<xsl:if expr="(childNumber(this) % 3) == 1">
<TR>
<TD><xsl:value-of /></TD>
<TD><xsl:value-of select="../product[index() $gt$ context()!index()][0]"/></TD>
<TD><xsl:value-of select="../product[index() $gt$ context()!index()][1]"/></TD>
</TR>
</xsl:if>
</xsl:for-each>
</TABLE>
Note that since the expression uses the childNumber method, which gives the number relative to other elements in the XML source, you cannot simultaneously sort the list of items and use the above mechanisms to insert them into a table.