tags:

views:

399

answers:

5

I have a list of elements that I want to split into individual lists of 3. The end result would be something like this:

<ul>
    <li>element</li>
    <li>element</li>
</ul>
<ul>
    <li>element</li>
    <li>element</li>
</ul>
<ul>
    <li>element</li>
    <li>element</li>
</ul>

My XSLT is like this, but it doesn't work, because I can't insert </ul>, and I can't insert a less than sign (<).

<ul>
    <xsl:for-each select="$myroot/item">
        <li></li>

        <xsl:if test="position() mod $maxItemsPerColumn = 0">
            <!-- I want to close my ul, and start a new one here, but it doesn't work! -->
        </xsl:if>
    </xsl:for-each>
</ul>

Any ideas? Thanks in advance!

A: 

you can do

<xsl:text disable-output-escaping="yes"><![CDATA[</ul>]]></xsl:text>

<xsl:text disable-output-escaping="yes"><![CDATA[<ul>]]></xsl:text>

so in your case:

<ul>
    <xsl:for-each select="$myroot/item">
        <li></li>

        <xsl:if test="position() mod $maxItemsPerColumn = 0">
            <xsl:text disable-output-escaping="yes"><![CDATA[</ul>]]></xsl:text>
            <xsl:text disable-output-escaping="yes"><![CDATA[<ul>]]></xsl:text>                
        </xsl:if>
    </xsl:for-each>
</ul>

Update: I had deleted this after reading the answers by Pierre and Greg but I decided to keep it up as it does answer your question and it could prove useful to someone, somewhere.

Update 2: Yeah I understand why this may be horrid and inspire Nazgûl-like fear in my peers and yes I have tried to down-vote this myself however I think this answer may be helpful to someone in the future.

Jared
That's truly evil.
Greg Hewgill
That's...horrifying. Seriously, using *either* CDATA or `disable-output-escaping` in XSLT is generally a warning that you're making a conceptual mistake of some kind. Using *both*?
Robert Rossney
I think you meant to say "this answer may be **harmful** to someone in the future" ;-)
NickFitz
At least we know it can be done this way, even though it may be the best approach.
Binoj Antony
A: 

you could try something like this (not tested, but you've got the idea )

(...)
<xsl:call-template name="recursive">
 <xsl:with-param name="root" select="$myroot"/>
 <xsl:with-param name="index" select="number(1)"/>
</xsl:call-template>
(...)


<xsl:template name="recursive">
<xsl:param name="root"/>
<xsl:param name="index"/>
<ul>
<li><xsl:value-of select="$root/item[$index]"/></li>
<li><xsl:value-of select="$root/item[$index +1 ]"/></li>
</ul>
<xsl:if test="$root/item[$index+2]">
<xsl:call-template name="recursive">
 <xsl:with-param name="root" select="$root"/>
 <xsl:with-param name="index" select="$index+2"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Pierre
+1  A: 

You can do it using a recursive solution:

<xsl:call-template name="group">
  <xsl:with-param name="items" select="$myroot/item" />
</xsl:call-template>

<xsl:template name="group">
  <xsl:param name="items" />
  <xsl:if test="count($items) > 0">
    <ul>
      <xsl:for-each select="$items[position() &lt;= 3]">
        <li>...</li>
      </xsl:for-each>
    </ul>
    <xsl:call-template name="group">
      <xsl:with-param name="items" select="$items[position() > 3]" />
    </xsl:call-template>
  </xsl:if>
</xsl:template>

What this does is call the group template for the whole list of items. The group template writes out the first three elements of the list (or fewer if there aren't three) inside <ul> ... </ul> tags. Then it calls itself again with the rest of the list of items omitting the first three. When the list is empty, the group template does nothing.

XSLT is a very functional language, and following the rules (ie. not using disable-output-escaping for this) will save you from much pain and suffering in the future when you need to modify your templates again.

Greg Hewgill
A: 

I hope you never need to use this:

<xsl:text disable-output-escaping="yes">&lt;/ul&gt;&lt;ul&gt;</xsl:text>
Rubens Farias
+9  A: 

You don't need to do anything fancy like recursion. And good lord, don't even contemplate using CDATA.

You just have to think like XSLT and ask: "What input element do I want to transform into my output element?"

Assuming that each ul is supposed to contain N items, you want to transform every Nth input item, starting with the first, into a ul:

<xsl:variable name="n" select="number(4)"/>

<xsl:template match="/">
  <output>
    <xsl:apply-templates select="/root/item[position() mod $n = 1]"/>
  </output>
</xsl:template>

Each of these item elements becomes a ul that contains the item and each of its N-1 following siblings:

<xsl:template match="item">
  <ul>
    <xsl:for-each select=". | following-sibling::item[position() &lt; $n]">
      <li>
        <xsl:value-of select="."/>
      </li>
    </xsl:for-each>
  </ul>
</xsl:template>

Assuming an input document like this:

<root>
  <item>1</item>
  <item>2</item>
  <item>3</item>
  <item>4</item>
  <item>5</item>
  <item>6</item>
  <item>7</item>
  <item>8</item>
  <item>9</item>
</root>

...you get this output, if $n is set to 4:

<output>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
  <ul>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
  </ul>
  <ul>
    <li>9</li>
  </ul>
</output>
Robert Rossney
@Robert Rossney: Nice one, clean and simple. +1
Tomalak
Awesome! Works perfectly!
SkippyFire