Search code examples
selectxsltforeachvalue-of

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


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 inner list index with outer list index on the fly.


Solution

  • 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)