0

I'm trying to build a navigation that in theory could recurse infinitely with XSL. Unfortunately my skills in this arena are still being built. Can anyone tell me where I'm going wrong with this code?

The kicker here is that the first 2 levels of the nav are somewhat unique (class names and such) but after level 3 the nav is pretty much the same elements being nested over and over.

To Update: There's no recursion after nav level 2, the output is non existent after level 2, and I can't figure out how to apply the recursion. I feel like this should be easier but my skills in XSL are not that great.

HTML (What we need it to produce):

  <ul class="nav-l1">
    <li class="nav-active"><a href="nav2.html" class="nav-item trigger-chan nav-next"><span><span class="trigger-cntr">First Level Parent</span></span></a>
      <ul class="nav-l2 nav-hidden">

        <li class="nav-active"><a href="nav2.html" class="nav-item"><span>Overview</span></a></li>
        <li><a href="#" class="nav-item trigger-chan nav-next"><span><span class="trigger-cntr">Second Level Parent</span></span></a>

          <ul class="nav-ls nav-hidden">
            <li><a href="#" class="nav-item"><span>Third Level</span></a></li>
            <li><a href="#" class="nav-item"><span>Third Level</span></a></li>
          </ul>
        </li>
        <li><a href="#" class="nav-item"><span>Second Level</span></a></li>
      </ul>
    </li>

    <li><a href="#" class="nav-item trigger-chan nav-next"><span><span class="trigger-cntr">First Level Parent</span></span></a>

      <!-- 2nd level of navigation.  -->
      <ul class="nav-l2 nav-hidden">
        <li><a href="#" class="nav-item"><span>Overview</span></a></li>
        <li><a href="#" class="nav-item trigger-chan nav-next"><span><span class="trigger-cntr">Second Level Parent</span></span></a>

          <!-- 3rd level of navigation.  -->
          <ul class="nav-ls nav-hidden">
            <li><a href="#" class="nav-item"><span>Third Level</span></a></li>
            <li><a href="#" class="nav-item"><span>Third Level</span></a></li>
            <li><a href="#" class="nav-item"><span>Third Level</span></a></li>
          </ul>
        </li>
        <li><a href="#" class="nav-item"><span>Second Level</span></a></li>
        <li><a href="#" class="nav-item"><span>Second Level</span></a></li>
      </ul>
    </li>

  </ul>

Sample XML (The format of the initial data feed):

<data>
    <folders level="1">
        <folder clickable="Y" url="/lorem/ipsum.html" name="Lorem Ipsum"/>
        <folder clickable="Y" url="/level/one.html" name="Level One"/>
        <folder clickable="Y" url="/foo/bar.html" name="Foo Bar">
            <folders level="2">
                <folder clickable="Y" url="/level/two.html" name="Level two"/>
                <folder clickable="Y" url="/child/item.html" name="Child Item">
                    <folders level="3">
                        <folder clickable="Y" url="/child/child/item.html" name="Child's Child Item">
                            <folders level="4">
                                <folder clickable="Y" url="/destiny/child/item.html" name="Destiny's Child"/>
                            </folders>
                        </folder>
                    </folders>
                </folder>
            </folders>
        </folder>
    </folders>
</data>

XSL (What we need to transform the XML):

    <?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="/">
    <xsl:choose>
      <xsl:when test="/data/folders">

          <!-- NAVIGATION BEGINS HERE -->
          <ul class="nav-l1">

            <!-- LEVEL 1 -->
            <xsl:for-each select="/data/folders[@level=1]/folder[not (@clickable ) or @clickable ='Y' ]">
              <li>
                <xsl:if test="@selected='Y'">
                  <xsl:attribute name="class">nav-active</xsl:attribute>
                </xsl:if>

                <a href="{@url}" class="nav-item">
                  <xsl:choose>
                    <xsl:when test="child::*">
                      <xsl:attribute name="class">trigger-chan nav-next</xsl:attribute>
                      <span>
                            <span class="trigger-cntr">
                              <xsl:value-of select="@name"/>
                            </span>
                      </span>
                    </xsl:when>
                    <xsl:otherwise>
                      <span><xsl:value-of select="@name" /></span>
                    </xsl:otherwise>
                  </xsl:choose>
                </a>

                <!-- LEVEL 2 -->
                <xsl:choose>
                  <xsl:when test="child::*">
                    <ul class="nav-l2 nav-hidden">
                      <xsl:for-each select="folders[@level=2]/folder[not (@clickable ) or @clickable ='Y' ]">
                        <li>
                          <xsl:if test="@selected='Y'">
                            <xsl:attribute name="class">nav-active</xsl:attribute>
                          </xsl:if>

                          <a href="{@url}" class="nav-item">
                            <xsl:choose>
                              <xsl:when test="child::*">
                                <xsl:attribute name="class">trigger-chan nav-next</xsl:attribute>
                                <span>
                                <span class="trigger-cntr">
                                  <xsl:value-of select="@name"/>
                                </span>
                              </span>
                              </xsl:when>
                              <xsl:otherwise>
                                <span><xsl:value-of select="@name" /></span>
                              </xsl:otherwise>
                            </xsl:choose>
                          </a>

                          <!-- LEVEL 3 and beyond ... -->
                          <xsl:apply-templates />

                        </li>
                      </xsl:for-each>
                    </ul>
                  </xsl:when>
                  <xsl:otherwise>
                  </xsl:otherwise>
                </xsl:choose>
              </li>

            </xsl:for-each>
          </ul>
      </xsl:when>
    </xsl:choose>
  </xsl:template>

<!-- Infinity and beyond (=> Level 3) -->
  <xsl:template name="folder">
    <ul class="nav-ls nav-hidden">
      <li>
          <xsl:if test="@selected='Y'">
            <xsl:attribute name="class">nav-active</xsl:attribute>
          </xsl:if>

          <a href="{@url}" class="nav-item">
            <xsl:choose>
              <xsl:when test="child::*">
                <xsl:attribute name="class">trigger-chan nav-next</xsl:attribute>
            <span>
                      <span class="trigger-cntr">
                        <xsl:value-of select="@name"/>
                      </span>
                </span>
              </xsl:when>
              <xsl:otherwise>
                <span><xsl:value-of select="@name" /></span>
              </xsl:otherwise>
            </xsl:choose>
          </a>
        <xsl:apply-templates select="folders[@level>3]/folder" />
      </li>
    </ul>
  </xsl:template>

</xsl:stylesheet>

Any help is greatly appreciated.

Thanks!

dnyce
  • 415
  • 1
  • 6
  • 20
  • 1
    Why do you think there anything wrong with the code? Are there errors? Is the output wrong? –  Oct 31 '13 at 23:08
  • There's no recursion after nav level 2, the output is non existent after level 2, and I can't figure out how to apply the recursion. I feel like this should be easier but my skills in XSL are not that great. Why the downvote? – dnyce Oct 31 '13 at 23:15
  • I didn't downvote, but I presume its because the problem wasn't clear in the initial question. Edit your comment in and it should probably go away. –  Oct 31 '13 at 23:20

1 Answers1

3

Your XSLT is very "pull" focused rather than "push" focused and could use some re-engineering to be more functional (in the functional programming sense).

But the main error is here:

<!-- Infinity and beyond (=> Level 3) -->
<xsl:template name="folder">

This is declaring a "named template" but it's never called. Not a bad thing, as name/call-templates should be avoided where possible.

Change it to the below and it should work as expected when using the apply-templates you already have in the XSLT.

<xsl:template match="folder">

edit: A push style approach

This might not be perfect, but a few things: * You skip over <folders> entirely, when they are the list, where as the <folder>s are the list items. * You appear to duplicate the item code for levels 1, 2 and 3. * You only ever make one item in the level 3 and above, all others would be ignored?

Notice below, that I've defined how I want <folders> to be rendered, based on level, and how <folder>s are rendered independently.

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
    <xsl:apply-templates/>
</xsl:template>
<xsl:template match="folders">
    <ul>
        <xsl:attribute name="class">
            <xsl:text>nav-l</xsl:text>
            <xsl:choose>
                <xsl:when test="@level&gt;2">
                    <xsl:text>s nav-hidden</xsl:text>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="@level"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:attribute>
        <xsl:apply-templates/>
    </ul>
</xsl:template>
<xsl:template match="folder">
    <li>
        <xsl:if test="@selected='Y'">
            <xsl:attribute name="class">nav-active</xsl:attribute>
        </xsl:if>
        <a href="{@url}" class="nav-item">
            <xsl:choose>
                <xsl:when test="child::*">
                    <xsl:attribute name="class">trigger-chan nav-next</xsl:attribute>
                    <span>
                        <span class="trigger-cntr">
                            <xsl:value-of select="@name"/>
                        </span>
                    </span>
                </xsl:when>
                <xsl:otherwise>
                    <span>
                        <xsl:value-of select="@name"/>
                    </span>
                </xsl:otherwise>
            </xsl:choose>
        </a>
        <xsl:apply-templates select="folders"/>
    </li>
</xsl:template>
</xsl:stylesheet>

Notice how the above template is shorter than your initial one, as repeated elements are removed. All <folder>s render the same, so only one template is needed. In fact, the <folder>s don't need to know their level, only the parent <folders> do, so that logic is moved to the correct template.

People who are new to XSLT often overlook how apply-templates can be used effectively to define recursion and allow very clear separations between elements. When creating templates, try and focus on how this particular element should be represented, and knowing how its child elements may be required. Then, focus on a new template for the child elements and how those should be rendered and so on.

Adding on to the above, always be on the look out for ../ in @selects or <value-of>s as it implies that your logic is dependant on elements that are outside the current "scope". Not always bad, but something to be aware of.

Try and minimise for-each where possible and see if they can be rewritten to use apply-templates, and unless absolutely required for some odd reason try and never use call-templates as they are less functional.

Community
  • 1
  • 1
  • Ok I'll try that but what you say makes sense. I feel like this code is VERY pull focused. I've seen some examples on the web of the great recursion functionality found in XSLT but I'm struggling with how to apply that to my nav. If this works, I'll accept the answer but it'd be really great to see your "push" style. :) – dnyce Oct 31 '13 at 23:24
  • Well, that sure did work Lego. Thanks! Do you have any tips for how I could re-jigger this code to make it more functional? – dnyce Oct 31 '13 at 23:27
  • 1
    @dnyce I've added an example "push" template for you to work from. –  Oct 31 '13 at 23:35
  • You are my new hero! This alone warrants more than an upvote... :) The only question I'm left with is how to handle the case of the first 2 parents? (Level 1 & 2 of the Nav) The class names that we have to apply for those 2 is slightly different than the class names for the folders from level 3 and on. – dnyce Oct 31 '13 at 23:41