Reputation: 199
I'm trying to wrap my mind around xslt. A number of questions here on stackoverflow help ( XSLT templates and recursion and XSLT for-each loop, filter based on variable ) but I'm still kinda puzzled. I guess I'm "thinking of template as functions" ( https://stackoverflow.com/questions/506348/how-do-i-know-my-xsl-is-efficient-and-beautiful )
Anyway...my data is
<Entities>
<Entity ID="8" SortValue="0" Name="test" ParentID="0" />
<Entity ID="14" SortValue="2" Name="test2" ParentID="8" />
<Entity ID="16" SortValue="1" Name="test3" ParentID="8" />
<Entity ID="17" SortValue="3" Name="test4" ParentID="14" />
<Entity ID="18" SortValue="3" Name="test5" ParentID="0" />
</Entities>
What I'd like as output is basically a "treeview"
<ul>
<li id="entity8">
test
<ul>
<li id="entity16">
test3
</li>
<li id="entity14">
test2
<ul>
<li id="entity17">
test4
</li>
</ul>
</li>
</ul>
</li>
<li id="entity18">
test5
</li>
</ul>
The XSLT I have so far is wrong in that it definitely "thinks of templates as functions" and also throws a StackOverflowException (:-)) on execution
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="html" indent="yes"/>
<xsl:template match="Entities">
<ul>
<li>
<xsl:value-of select="local-name()"/>
<xsl:apply-templates/>
</li>
</ul>
</xsl:template>
<xsl:template match="//Entities/Entity[@ParentID=0]">
<xsl:call-template name="recursive">
<xsl:with-param name="parentEntityID" select="0"></xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template name="recursive">
<xsl:param name="parentEntityID"></xsl:param>
<xsl:variable name="counter" select="//Entities/Entity[@ParentID=$parentEntityID]"></xsl:variable>
<xsl:if test="count($counter) > 0">
<xsl:if test="$parentEntityID > 0">
</xsl:if>
<li>
<xsl:variable name="entityID" select="@ID"></xsl:variable>
<xsl:variable name="sortValue" select="@SortValue"></xsl:variable>
<xsl:variable name="name" select="@Name"></xsl:variable>
<xsl:variable name="parentID" select="@ParentID"></xsl:variable>
<a href=?ID={$entityID}&ParentEntityID={$parentID}">
<xsl:value-of select="$name"/>
</a>
<xsl:call-template name="recursive">
<xsl:with-param name="parentEntityID" select="$entityID"></xsl:with-param>
</xsl:call-template>
</li>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
I know how to do this by code, no problem. This time, though, I'm looking for a solution in xslt and for that any help would be greatly appreciated.
Upvotes: 5
Views: 4150
Reputation: 243539
A solution that is correct and efficient (the currently accepted answer doesn't produce the wanted nested ul
elements:
<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:key name="kChildren" match="Entity" use="@ParentID"/>
<xsl:template match="/*[Entity]">
<ul>
<xsl:apply-templates select="key('kChildren', '0')">
<xsl:sort select="@SortValue" data-type="number"/>
</xsl:apply-templates>
</ul>
</xsl:template>
<xsl:template match="Entity">
<li id="entity{@ID}">
<xsl:value-of select="concat('
 ', @Name, '
')"/>
<xsl:if test="key('kChildren', @ID)">
<ul>
<xsl:apply-templates select="key('kChildren', @ID)">
<xsl:sort select="@SortValue" data-type="number"/>
</xsl:apply-templates>
</ul>
</xsl:if>
</li>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<Entities>
<Entity ID="8" SortValue="0" Name="test" ParentID="0" />
<Entity ID="14" SortValue="2" Name="test2" ParentID="8" />
<Entity ID="16" SortValue="1" Name="test3" ParentID="8" />
<Entity ID="17" SortValue="3" Name="test4" ParentID="14" />
<Entity ID="18" SortValue="3" Name="test5" ParentID="0" />
</Entities>
the wanted, correct result is produced:
<ul>
<li id="entity8">
test
<ul>
<li id="entity16">
test3
</li>
<li id="entity14">
test2
<ul>
<li id="entity17">
test4
</li>
</ul>
</li>
</ul>
</li>
<li id="entity18">
test5
</li>
</ul>
Upvotes: 1
Reputation: 6956
Although call-template
and named templates are a very useful feature of the language, if you find yourself preferring them to apply-templates
it may be a sign that you are still thinking in functions rather than templates. This is particularly true if the first thing you do in a named template is select a nodeset on which to operate.
Here is a simple version of what you are trying to do.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<ul>
<xsl:apply-templates select="Entities/Entity[@ParentID=0]" />
</ul>
</xsl:template>
<xsl:template match="Entity">
<li>
<xsl:value-of select="@Name" />
<xsl:apply-templates select="../Entity[@ParentID=current()/@ID]" />
</li>
</xsl:template>
</xsl:stylesheet>
Note that there is no need for the counter, as the value of the "parent" provides the necessary context.
Also note that all Entities
behave in the same way, regardless of where they are in the tree, they contain their @Name
value, and apply the template to any Entity
objects whose @ParentID matches the @ID of the current level.
Upvotes: 7