Often its needed to group a set of nodes using node properties
etc. A similar problem is getting all distinct values of a set of
values, e.g. all months represented in a set of dates.
Generally the best overall approach is using xsl keys
and id's, not least due to performance issues - this is
known as the Muenchian Method. Keys are mapping nodes to a
specific key value that can be used in selecting and grouping
nodes. Id's are unique strings identifying a node.
A basic example of using keys and id's is adding a key for all
nodes, using the month-part (substring(@updateDate, 9,2)) as
group-separator/id. This example displays the unique
updatedate-months for nodes in the system:
<xsl:key name="date" match="node" use="substring(@updateDate, 9,2)"/>
...
<xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('date', substring(@updateDate, 9,2))[1])]">
<xsl:value-of select="substring(@updateDate, 9,2)"/><br/>
</xsl:for-each>
Deciphering keys and id's
One thing to note is, that the use-clause dictates how
the key-string should be generated for all nodes matching the
match-clause. In the upper example a "date"-key is
generated for all "node"-nodes in the current document using the
"substring(@updateDate, 9,2)" key-string, thus all "node"-nodes
updated in March will have key "03". Since this is global to the
document, a key('date', '08') function call is also global to the
document, and will search the full document for nodes matching
the '08'-key.
But lets go back a bit, and start with a simpler example.
Say we have a set of strings:
January
February
March
January
December
January
February
April
April
and we want to find and list the distinct strings only:
January
February
March
December
April
We need a key definition to match that, something like
this:
For all content nodes of type DateNode
their key should be the node-name (e.g. January, February
etc.)
So our key definition would look like this:
<xsl:key name="MyKey" match="DateNode" use="@nodeName"/>
Now the key is in place. Next step is using it, and to iterate the
nodes to find and output the distinct string we can do like
this:
<xsl:for-each select="$currentPage/DateNode[generate-id() = generate-id(key('MyKey', @nodeName)[1])]">
<xsl:value-of select="@nodeName"/>
</xsl:for-each>
This needs a bit of explanation. The xslt function
key(keyname, usestring) returns the set
of nodes that are included in the key keyname and that
matches usestring as their key. E.g. key("MyKey",
"January") would return the set of all DateNodes in the document
with @nodeName = "January".

One important lesson to learn from this is, that if you want to
limit the nodes searched you need to elaborate a bit on the key.
Obviously you can change the match-clause to be more specific using
normal xslt syntax. In Umbraco you may want to search
e.g. subnodes of some specific node only, and unfortunately you
cannot use variable- or parameter references in key match- and
use-clauses. Thus, instead you can add context specific info to the
generated key, e.g. add the node-id, parent node-id or whatever you
like.
In this example the distinct updateDate-months of the
currentPage subnodes are written - the key definition adds parent
node-id's to the key, which in the for-each loop matches the
$currentPage:
<xsl:key name="date" match="node" use="concat(../@id,substring(@updateDate, 9,2))"/>
...
<xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('date', concat($currentPage/@id,substring(@updateDate, 9,2)))[1])]">
<xsl:value-of select="substring(@updateDate, 9,2)"/><br/>
</xsl:for-each>
One final example
This snippets displays the distinct months of a set of 4
subnodes with a document type date-field (here
SomeDate)
The idea is:
- Define some special key/function used for grouping the nodes -
in your case it's simply using the month part of a date property
called "SomeDate", but it could be less/more advanced. It's a bit
similar to a hash-function. All nodes with the same key value
defines a group.
- Find the first node of every group. This is done by comparing
unique node-id's (fetched by generate-id() with the first node-id
of the group). This is the outer for-each
- Iterate over all nodes in a group using the key() function for
fetching all nodes with a specific value. The Key() function
returns a node-set.
<xsl:param name="currentPage"/>
<!-- This is the key definition using the month of the SomeDate documenttype field
- all nodes with the same month get same key id-->
<xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM')"/>
<xsl:template match="/">
<!-- Iterate over all nodes with an id similar to the first in every group -->
<xsl:for-each select="$currentPage/node[generate-id() = generate-id(key('months', umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM'))[1])]">
<h5><xsl:value-of select="umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM')"/></h5>
<!-- Iterate over all nodes in a group, i.e. with the same key -->
<xsl:for-each select="key('months',umbraco.library:FormatDateTime(data[@alias='SomeDate'], 'MMMM'))">
<xsl:sort select="data[@alias='SomeDate']" />
<xsl:value-of select="umbraco.library:LongDate(data[@alias='SomeDate'])"/> :
<xsl:value-of select="@nodeName" /><br />
</xsl:for-each>
</xsl:for-each>
</xsl:template>
The result looks like this:
marts
1. marts 2009 :
DateTest1
23. marts 2009 :
DateTest3
april
9. april 2009 :
DateTest2
juni
9. juni 2009 :
DateTest4
Additional samples
The previous example only shows the concept of using keys and
id's, leaving out e.g. functionality for handling nodes from
multiple years. Here is the previous example enhanced to handle
years also by Scott Hugh Alexander Petersen:
<xsl:param name="currentPage"/>
<!-- This is the key definitions using the year and month of the the node createDate property
- all nodes with the same month get same month-key id, all nodes with the same year get the same year-key id -->
<xsl:key name="years" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy')"/>
<xsl:key name="months" match="node" use="umbraco.library:FormatDateTime(@createDate, 'yyyy-MM')"/>
<xsl:template match="/">
<ul> <!-- Iterate over all nodes with a year-id similar to the first in every group -->
<xsl:for-each select="$currentPage/ancestor-or-self::node [ (generate-id() = generate-id(key('years', Exslt.ExsltDatesAndTimes:year(@createDate))[1]))]">
<xsl:sort select="@createDate" order="descending" />
<li> <xsl:value-of select="Exslt.ExsltDatesAndTimes:year(@createDate)" />
<ul>
<xsl:variable name="months" select="key('years', Exslt.ExsltDatesAndTimes:year(@createDate))" />
<!-- Iterate over all nodes with a month-id similar to the first in every group -->
<xsl:for-each select="$months[generate-id() = generate-id(key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))[1])]">
<xsl:sort select="@createDate" order="ascending" />
<li> <xsl:value-of select="umbraco.library:FormatDateTime(@createDate, 'MM')"/>
<ul> <!-- Iterate over all nodes in a group, i.e. with the same months-key -->
<xsl:for-each select="key('months',umbraco.library:FormatDateTime(@createDate, 'yyyy-MM'))">
<xsl:sort select="@createDate" order="ascending" />
<li> <xsl:value-of select="umbraco.library:LongDate(@createDate)"/> : <xsl:value-of select="@nodeName" /> </li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</ul>
</xsl:template>