set selected attribute of html option element within select based on outer for-each index

Wolfgang picture Wolfgang · Dec 10, 2012 · Viewed 12.3k times · Source

The title sounds much more complex than the problem really is: I have been trying to put things together by bits and pieces found on stackoverflow and elsewhere, but I can't quite figure it out.

Task: I have a list of items, which I want to iterate through and for each iteration, I would like to create a drop down list and then by default select the item based on the current overall index.

The example will make it very clear. Here's the XML:

<?xml version="1.0" encoding="UTF-8"?>
<Plants>
   <Plant PlantId="13" PlantType="Tree"/>
   <Plant PlantId="25" PlantType="Flower"/>
   <Plant PlantId="70" PlantType="Shrub"/>
</Plants>

Then I have some XSL:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="html" indent="yes" encoding="UTF-8" omit-xml-declaration="yes"/>
    <xsl:template match="/">
        <xsl:param name="listIdx" select="0">
        </xsl:param>
        <table>
            <thead>
                    <tr>
                        <td>PlantType</td>
                    </tr>
            </thead>
            <tbody>
                <xsl:for-each select="Plants/Plant">
                    <tr>
                        <td>
                            <select>
                                <xsl:for-each select="/Plants/Plant">
                                    <xsl:element name="option">
                                        <xsl:attribute name="value">
                                            <xsl:value-of select="@PlantId"/>
                                        </xsl:attribute>
                                        <xsl:if test="count(.) = 2">
                                            <xsl:attribute name="selected">selected</xsl:attribute>    
                                        </xsl:if>
                                        <xsl:value-of select="@PlantType"/>
                                    </xsl:element>
                                </xsl:for-each>
                            </select>
                        </td>
                    </tr>
                </xsl:for-each>
            </tbody>
        </table>        
    </xsl:template>
</xsl:stylesheet>

What I get is this:

PlantType
Tree [=dropdown with Tree, Flower, Shrub]
Tree [=dropdown with Tree, Flower, Shrub]
Tree [=dropdown with Tree, Flower, Shrub]

What I'd love to have is:

PlantType
Tree [=dropdown with Tree, Flower, Shrub (idx 1 preselected)]
Flower [=dropdown with Tree, Flower, Shrub (idx 2 preselected)]
Shrub [=dropdown with Tree, Flower, Shrub (idx 3 preselected)]

I guess there will be two approaches: 1) use a listIdx in the outer loop (match) and then compare the current index within the inner loop with listIdx. 2) compare innner list index with outer list index on the fly. If it's easy enough to illustrate the key components, I'd very much appreciate it! Thank you!

Answer

Tim C picture Tim C · Dec 10, 2012

What you could do is define a variable in your outer loop to hold the current position of the plant element

 <xsl:variable name="position" select="position()"/>

Then, in your inner loop, you can check the second position against this variable, which will still be in scope

<xsl:if test="position() = $position">
    <xsl:attribute name="selected">selected</xsl:attribute>
</xsl:if>

Here is the full XSLT in this case

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

    <xsl:template match="/">
        <xsl:param name="listIdx" select="0"/>
        <table>
            <thead>
                <tr>
                    <td>PlantType</td>
                </tr>
            </thead>
            <tbody>
                <xsl:for-each select="Plants/Plant">
                    <xsl:variable name="position" select="position()"/>
                    <tr>
                        <td>
                            <select>
                                <xsl:for-each select="/Plants/Plant">
                                    <xsl:element name="option">
                                        <xsl:attribute name="value">
                                            <xsl:value-of select="@PlantId"/>
                                        </xsl:attribute>
                                        <xsl:if test="position() = $position">
                                            <xsl:attribute name="selected">selected</xsl:attribute>
                                        </xsl:if>
                                        <xsl:value-of select="@PlantType"/>
                                    </xsl:element>
                                </xsl:for-each>
                            </select>
                        </td>
                    </tr>
                </xsl:for-each>
            </tbody>
        </table>
    </xsl:template>
</xsl:stylesheet>

This produces the following output

<table>
    <thead>
        <tr>
            <td>PlantType</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                <select>
                    <option value="13" selected="selected">Tree</option>
                    <option value="25">Flower</option>
                    <option value="70">Shrub</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>
                <select>
                    <option value="13">Tree</option>
                    <option value="25" selected="selected">Flower</option>
                    <option value="70">Shrub</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>
                <select>
                    <option value="13">Tree</option>
                    <option value="25">Flower</option>
                    <option value="70" selected="selected">Shrub</option>
                </select>
            </td>
        </tr>
    </tbody>
</table>

However, it is often better to use xsl:apply-templates over xsl:for-each, if only to avoid excessive indentation. You could also pass the position as a parameter in this case. The following XSLT also produces the same output

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

    <xsl:template match="/">
        <xsl:param name="listIdx" select="0"/>
        <table>
            <thead>
                <tr>
                    <td>PlantType</td>
                </tr>
            </thead>
            <tbody>
                <xsl:apply-templates select="Plants/Plant"/>
            </tbody>
        </table>
    </xsl:template>

    <xsl:template match="Plant">
        <tr>
            <td>
                <select>
                    <xsl:apply-templates select="/Plants/Plant" mode="options">
                        <xsl:with-param name="position" select="position()"/>
                    </xsl:apply-templates>
                </select>
            </td>
        </tr>
    </xsl:template>

    <xsl:template match="Plant" mode="options">
        <xsl:param name="position"/>
        <option value="{@PlantId}">
            <xsl:if test="position() = $position">
                <xsl:attribute name="selected">selected</xsl:attribute>
            </xsl:if>
            <xsl:value-of select="@PlantType"/>
        </option>
    </xsl:template>
</xsl:stylesheet>

Also note the use of Attribute Value Templates to create the value attribute on the option element (and note there is no real need to use xsl:element to create a statically name element)