I'm quite new to XSLT and therefore I want to know what is the best practice for checking the existance of an attribute. My XML looks something like this:
<root>
<languages>
<lang id="EN">English<lang>
<lang id="FR">French<lang>
<lang id="DE">German</lang>
</languages>
<items>
<item lang="EN">test 1</item>
<item>test 2</item>
<item lang="FR">item 3</item>
</items>
</root>
Note that the 'lang'-attribute for the 'item'-element is optional.
Now I want to loop through the items using a -loop, while checking if it has a "lang"-attribute. If it does, I want to fetch the entire string using the ID (eg. EN -> 'English'). If the attribute isn't set I want it to write "No language set" or something alike.
Now I use the following code but I'm questioning myself if it can't be done in a more efficient way.
<xsl:for-each select="//root/items/item">
<xsl:variable name="cur_lang" select="@lang" /> <!-- first I store the attr lang in a variable -->
<xsl:choose>
<xsl:when test="@lang"> <!-- then i test if the attr exists -->
<xsl:value-of select="//root/languages/lang[@id=$cur_lang]" /> <!-- if so, parse the element value -->
</xsl:when>
<xsl:otherwise>
No language set <!-- else -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
Any suggestions / tips?
It might be more efficient to use a key. You define a key using a top-level element outside your templates
<xsl:key name="langByCode" match="lang" use="@id" />
Then in the loop you can simply say
<xsl:when test="@lang"> <!-- then i test if the attr exists -->
<xsl:value-of select="key('langByCode', @lang)" />
</xsl:when>
But generally speaking a more natural XSLT approach to the whole thing would be to use template matching instead of for-each
and if
:
<xsl:template match="item[@lang]">
<xsl:value-of select="key('langByCode', @lang)" />
</xsl:template>
<xsl:template match="item">
<xsl:text>No language set</xsl:text>
</xsl:template>
With these templates in place you can then do <xsl:apply-templates select="/root/items/item" />
and it will pick the appropriate template for each item automatically. The rule is that it will use the most specific template, so the item[@lang]
one for those items that have a lang
attribute and the plain item
one for those that don't.
A third possibility is a little trick I learned on SO to put the whole if/else check into a single XPath expression
<xsl:value-of select="
substring(
concat('No language set', key('langByCode', @lang)),
1 + (15 * boolean(@lang))
)" />
The trick here is that boolean(@lang)
when treated as a number is 1
if the lang attribute exists and 0
if it doesn't. If there is a lang="EN"
, say, then we construct a string "No language setEnglish"
and then take the substring starting at the 16th character, which is "English"
. If there is no lang attribute we construct the string "No language set"
(because the string value of an empty node set is the empty string) and take the substring starting at the first character (i.e. the whole string).
You could use the same trick with other attributes, e.g. suppose we had an optional color attribute and wanted to say "No color specified"
if it is absent, you can do that with
<xsl:value-of select="substring(
concat('No color specified', @color),
1 + (18 * boolean(@color))
)" />
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With