Reputation: 2726
I'm looking to get some XSLT (for Umbraco CMS) coded properly and I'm getting kind of stumped. What I'm trying to do is:
Start from a certain node, put each child node into a div; for every 3 children, wrap in a parent div.
Instead of my mess of for-each
,choose
and when
statements, I have tried implementing a apply-template
structure but I just can't seem to get the hang of it; so here's my mess of XSLT right now (I'm sure this is bad practice & terrible for performance, but I really don't know what to do at the moment):
<div class="row four">
<h2>Smart Phones <a href="#" class="see-all">see all smart phones →</a></h2>
<div class="row three"> <!-- This div should be created again for every 3 divs -->
<xsl:for-each select="umbraco.library:GetXmlNodeById('1063')/descendant::*[@isDoc and string(showInMainNavigation) = '1']">
<xsl:choose>
<xsl:when test="position() < 3">
<div class="col">
<a href="{umbraco.library:NiceUrl(./@id)}">
<img class="phonePreviewImg" src="{./previewImage}" style="max-width:117px; max-height:179px;" />
<h4 class="phoneTitle"><xsl:value-of select="./@nodeName" />/h4>
<p class="phonePrice">$<xsl:value-of select="./price" /></p
</a>
</div>
</xsl:when>
<xsl:when test="position() = 3"> <!-- set this div to include class of `omega` -->
<div class="col omega">
<a href="{umbraco.library:NiceUrl(./@id)}">
<img class="phonePreviewImg" src="{./previewImage}" style="max-width:117px; max-height:179px;" />
<h4 class="phoneTitle"><xsl:value-of select="./@nodeName" />/h4>
<p class="phonePrice">$<xsl:value-of select="./price" /></p
</a>
</div>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</div> <!-- End Row Three -->
</div> <!-- End Row Four -->
Obviously this code does not produce the "wrap every three". Can anyone shed some light on what I need to do to accomplish this?
Upvotes: 2
Views: 1541
Reputation: 11953
UPDATE - improved the answer
I cannot think of an elegant solution using templates, but this clunky one with a loop works:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="render">
<xsl:param name="node"/>
<xsl:param name="last"/>
<div>
<xsl:if test="$last">
<xsl:attribute name="class">
<xsl:text>omega</xsl:text>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="$node"/>
</div>
</xsl:template>
<xsl:template match="/*">
<root>
<xsl:variable name="nodes" select="*[not(@skip)]"/>
<xsl:for-each select="$nodes">
<xsl:if test="(position() mod 3)=1">
<xsl:variable name="position" select="position()"/>
<div>
<xsl:call-template name="render">
<xsl:with-param name="node" select="."/>
<xsl:with-param name="last" select="false()"/>
</xsl:call-template>
<xsl:if test="$nodes[$position+1]">
<xsl:call-template name="render">
<xsl:with-param name="node" select="$nodes[$position+1]"/>
<xsl:with-param name="last" select="false()"/>
</xsl:call-template>
</xsl:if>
<xsl:if test="$nodes[$position+2]">
<xsl:call-template name="render">
<xsl:with-param name="node" select="$nodes[$position+2]"/>
<xsl:with-param name="last" select="true()"/>
</xsl:call-template>
</xsl:if>
</div>
</xsl:if>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
applied to:
<root>
<node>1</node>
<node skip="1">to be skipped</node>
<node>2</node>
<node>3</node>
<node skip="1">to be skipped</node>
<node skip="1">to be skipped</node>
<node>4</node>
<node skip="1">to be skipped</node>
<node>5</node>
<node>6</node>
<node>7</node>
<node skip="1">to be skipped</node>
</root>
produces:
<root>
<div>
<div>1</div>
<div>2</div>
<div class="omega">3</div>
</div>
<div>
<div>4</div>
<div>5</div>
<div class="omega">6</div>
</div>
<div>
<div>7</div>
</div>
</root>
You need to replace the select
used to set the $nodes
variable the XPath selecting the nodes you want, and the render
template with the code needed to generate the result you need for each node.
Upvotes: 1
Reputation: 243479
As simple and short as this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="num[position() mod 3 = 1]">
<div>
<xsl:apply-templates mode="inGroup"
select=".|following-sibling::*[not(position() >2)]"/>
</div>
</xsl:template>
<xsl:template match="num" mode="inGroup">
<p><xsl:apply-templates mode="inGroup"/></p>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<nums>
<num>01</num>
<num>02</num>
<num>03</num>
<num>04</num>
<num>05</num>
<num>06</num>
<num>07</num>
<num>08</num>
<num>09</num>
<num>10</num>
</nums>
the wanted, correct result is produced:
<div>
<p>01</p>
<p>02</p>
<p>03</p>
</div>
<div>
<p>04</p>
<p>05</p>
<p>06</p>
</div>
<div>
<p>07</p>
<p>08</p>
<p>09</p>
</div>
<div>
<p>10</p>
</div>
Upvotes: 1
Reputation: 3738
Here's an elegant solution using templates.
When this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output omit-xml-declaration="yes" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:param name="pNumInGroup" select="3" />
<xsl:template match="/*">
<html>
<xsl:apply-templates select="*[position() mod $pNumInGroup = 1]" />
</html>
</xsl:template>
<xsl:template match="node">
<div>
<xsl:for-each
select=".|following-sibling::*[not(position() > $pNumInGroup - 1)]">
<div>
<xsl:apply-templates />
</div>
</xsl:for-each>
</div>
</xsl:template>
</xsl:stylesheet>
...is applied to the sample XML provided by @MiMo:
<root>
<node>1</node>
<node>2</node>
<node>3</node>
<node>4</node>
<node>5</node>
<node>6</node>
<node>7</node>
</root>
...the correct result is produced:
<html>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>
<div>
<div>4</div>
<div>5</div>
<div>6</div>
</div>
<div>
<div>7</div>
</div>
</html>
If the parameter value is changed to 5
:
<xsl:param name="pNumInGroup" select="5" />
...the correct result is still produced:
<html>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>
<div>
<div>6</div>
<div>7</div>
</div>
</html>
Explanation:
pNumInGroup
parameter at the top of the document (with a default value of 3
). This is useful, as it allows the XSLT to be used more flexibly (i.e., if you need a different number of <div>
elements per group, simply pass them as a parameter).<node>
elements that are selected by the previous template. For each, a new <div>
element is created and populated with the remaining items appropriate to that wrapped group.NOTE: I generally stay away from <xsl:for-each>
unless I really need it; in the case of the last template, I don't really need it (I could just as easily have specified the wrapping/secondary <div>
logic with another template). However, for the sake of "crispness" and not over-templating the XSLT, I chose this route.
Upvotes: 0