I'm using the tags datatype, URL parameters /?filter=a,b,c and XSLT to display the results. I'm using the example here but it returns nodes that match any of the tags instead of all of the tags.
If a user selects a and c, I still want to show nodes that contain those tags, even if they also contain b. Or if they select a,b,c just show nodes that match all three.
I just don't know enough XSLT to customize the logic enough. I'm on Umbraco 6.1.6. Any other info I need to provide?
Here is the XSLT I'm working with: Thanks for any advice or assitance!
<xsl:param name="currentPage"/>
<xsl:template match="/">
<!-- Query-string in the browser -->
<xsl:variable name="filter" select="umbraco.library:RequestQueryString('filter')"/>
<!-- run a for-each to split the tags -->
<xsl:for-each select="umbraco.library:Split($filter, ',')/value">
<!-- Save the values in a variable -->
<xsl:variable name="ct" select="."/>
<!-- Run a for-each to over the nodes we want to "search" -->
<!-- We will look for pages underneath node id 1279 - change this to fit your hierachy -->
<xsl:for-each select="umbraco.library:GetXmlNodeById(1279)/descendant-or-self::*">
<!-- Check if our variable is not empty -->
<xsl:if test="$ct != ''">
<!-- Check to if any of these pages contains the tags from our ct-variable -->
<xsl:if test="contains(tags, $ct)">
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The fact that the tags are stored in a CSV list makes this a bit harder, but here's something to play with — hopefully you can understand what's going on:
<!-- Grab the tags from QueryString -->
<xsl:variable name="query" select="umbraco.library:RequestQueryString('tags')" />
<!-- Create a nodeset from them -->
<xsl:variable name="queryTags" select="umbraco.library:Split($query, ',')" />
<!-- Get the number of tags -->
<xsl:variable name="tagCount" select="count($queryTags/value)" />
<!-- Grab and cache all the relevant nodes to test against, e.g., only include ones that are actually
tagged with something. Be as specific as possible here to improve performance. -->
<xsl:variable name="nodesToMatch" select="$currentPage//*[@isDoc][normalize-space(tags)]" />
<xsl:template match="/">
<p>
<xsl:value-of select="concat('Tags: ', $query)" />
</p>
<h3>Nodes that match, using ANY</h3>
<!-- This is pretty straightforward - a single XPath will do -->
<p><xsl:apply-templates select="$nodesToMatch[umbraco.library:Split(tags, ',')/value = $queryTags/value]" /></p>
<h3>Nodes that match, using ALL</h3>
<!--
For this, we need to first collect the nodes that match, by building a
temp variable which will look like this:
<nodes>
<tagged tag="xslt">
<nodeId>1234</nodeId>
<nodeId>2132</nodeId>
</tagged>
<tagged tag="basic">
<nodeId>4536</nodeId>
<nodeId>1655</nodeId>
<nodeId>2132</nodeId>
</tagged>
</nodes>
If a node has ALL tags, it's present in all <tagged> groups,
like the node "2132" in the above sample.
-->
<xsl:variable name="matchALLproxy">
<nodes>
<xsl:for-each select="$queryTags/value">
<xsl:variable name="currentTag" select="." />
<tagged tag="{$currentTag}">
<xsl:apply-templates select="$nodesToMatch[umbraco.library:Split(tags, ',')/value = $currentTag]" mode="collect" />
</tagged>
</xsl:for-each>
</nodes>
</xsl:variable>
<xsl:variable name="matchALL" select="msxml:node-set($matchALLproxy)/nodes" />
<p>
<!-- Go through all nodes that match at least one tag -->
<xsl:for-each select="$nodesToMatch[@id = $matchALL/tagged/nodeId]">
<xsl:variable name="id" select="@id" />
<xsl:if test="count($matchALL/tagged[nodeId = $id]) = $tagCount">
<!-- This was tagged with all the tags so render it -->
<xsl:apply-templates select="." />
</xsl:if>
</xsl:for-each>
</p>
</xsl:template>
<!-- Helper template to collect a node's id -->
<xsl:template match="*" mode="collect">
<nodeId><xsl:value-of select="@id" /></nodeId>
</xsl:template>
<!-- The generic template to render a matching document -->
<xsl:template match="*[@isDoc]">
<p>
<xsl:value-of select="@nodeName" />
<xsl:text /> [<xsl:value-of select="tags" />]<xsl:text />
</p>
</xsl:template>
That works perfectly for what we are doing! You are awesome Chriztian, thank you very much. I've got some work to do for our display of the returns but the core functionality is dead on. Thank you so much!
By the way, we are only looping through less than 200 nodes, so this is pretty performant too. I'm removing the ANY match and just leaving the ALL. I'll post my final code including diplay in a few days in case anyone else can use it!
How would I compare the value of a nodes tags to the tagged tag="value" so that I could sort the results into groups based on each tag? So all nodes tagged with brochures would be grouped in one loop, all nodes tagged with videos would be grouped into another section and display like:
Brochures:
-node 1
-node 2
Video:
-node3
-node 4
Would you use an if statement in the render portion of this part? Or a choose?
<xsl:for-each select="$nodesToMatch[@id = $matchALL/tagged/nodeId]">
<xsl:variable name="id" select="@id" />
<xsl:if test="count($matchALL/tagged[nodeId = $id]) = $tagCount">
<!-- This was tagged with all the tags so render it -->
<xsl:apply-templates select="." />
</xsl:if>
</xsl:for-each>
We are giving the user 3 dropdowns (product, topic, type) to filter by and we want to group the results by one of the choices in the type dropdown. So the results that are returned will always have a type tag that is unique, ie brochure, or webinar, or tool.
We want to display the results of the filtering based on those types. Because I user might not use the type filter and the results for the first 2 filters may include several types.
Brochures:
-nodes returned containing "brochure" tag -nodes returned containing "brochure" tag
I think I have it, I stuck the heading outside the $nodesToMatch for-each loop and then inside each one, added a test for a string value in the tags and then print those nodes. Probably not the best way to do it, but I'm on a tight deadline unfortunately.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxml="urn:schemas-microsoft-com:xslt"
xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:Examine="urn:Examine" xmlns:applied="urn:applied" xmlns:umbraco.contour="urn:umbraco.contour" xmlns:ucomponents.cms="urn:ucomponents.cms" xmlns:ucomponents.dates="urn:ucomponents.dates" xmlns:ucomponents.email="urn:ucomponents.email" xmlns:ucomponents.io="urn:ucomponents.io" xmlns:ucomponents.media="urn:ucomponents.media" xmlns:ucomponents.members="urn:ucomponents.members" xmlns:ucomponents.nodes="urn:ucomponents.nodes" xmlns:ucomponents.random="urn:ucomponents.random" xmlns:ucomponents.request="urn:ucomponents.request" xmlns:ucomponents.search="urn:ucomponents.search" xmlns:ucomponents.strings="urn:ucomponents.strings" xmlns:ucomponents.urls="urn:ucomponents.urls" xmlns:ucomponents.xml="urn:ucomponents.xml"
exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets Examine applied umbraco.contour ucomponents.cms ucomponents.dates ucomponents.email ucomponents.io ucomponents.media ucomponents.members ucomponents.nodes ucomponents.random ucomponents.request ucomponents.search ucomponents.strings ucomponents.urls ucomponents.xml ">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<!-- Grab the tags from QueryString -->
<xsl:variable name="query" select="umbraco.library:RequestQueryString('filter')" />
<!-- Create a nodeset from them -->
<xsl:variable name="queryTags" select="umbraco.library:Split($query, ',')" />
<!-- Get the number of tags -->
<xsl:variable name="tagCount" select="count($queryTags/value)" />
<!-- Grab and cache all the relevant nodes to test against, e.g., only include ones that are actually
tagged with something. Be as specific as possible here to improve performance. -->
<xsl:variable name="nodesToMatch" select="$currentPage//*[@isDoc][normalize-space(tags)]" />
<xsl:template match="/">
<p>
<xsl:value-of select="concat('Tags: ', $query)" />
</p>
<!-- <h3>Nodes that match, using ANY</h3>
This is pretty straightforward - a single XPath will do
<p><xsl:apply-templates select="$nodesToMatch[umbraco.library:Split(tags, ',')/value = $queryTags/value]" /></p> -->
<!-- <h3>Nodes that match, using ALL</h3> -->
<!--
For this, we need to first collect the nodes that match, by building a
temp variable which will look like this:
<nodes>
<tagged tag="xslt">
<nodeId>1234</nodeId>
<nodeId>2132</nodeId>
</tagged>
<tagged tag="basic">
<nodeId>4536</nodeId>
<nodeId>1655</nodeId>
<nodeId>2132</nodeId>
</tagged>
</nodes>
If a node has ALL tags, it's present in all <tagged> groups,
like the node "2132" in the above sample.
-->
<xsl:variable name="matchALLproxy">
<nodes>
<xsl:for-each select="$queryTags/value">
<xsl:variable name="currentTag" select="." />
<tagged tag="{$currentTag}">
<xsl:apply-templates select="$nodesToMatch[umbraco.library:Split(tags, ',')/value = $currentTag]" mode="collect" />
</tagged>
</xsl:for-each>
</nodes>
</xsl:variable>
<xsl:variable name="matchALL" select="msxml:node-set($matchALLproxy)/nodes" />
<h3><xsl:text>Brochure</xsl:text></h3>
<!-- Go through all nodes that match at least one tag -->
<xsl:for-each select="$nodesToMatch[@id = $matchALL/tagged/nodeId]">
<xsl:variable name="id" select="@id" />
<xsl:if test="count($matchALL/tagged[nodeId = $id]) = $tagCount">
<xsl:variable name="list" select="umbraco.library:Split(tags, ',')/value" />
<xsl:if test="contains($list, 'Brochure')">
<!-- This was tagged with all the tags so render it -->
<xsl:apply-templates select="." />
</xsl:if>
</xsl:if>
</xsl:for-each>
<h3><xsl:text>Webinar</xsl:text></h3>
<!-- Go through all nodes that match at least one tag -->
<xsl:for-each select="$nodesToMatch[@id = $matchALL/tagged/nodeId]">
<xsl:variable name="id" select="@id" />
<xsl:if test="count($matchALL/tagged[nodeId = $id]) = $tagCount">
<xsl:variable name="list" select="umbraco.library:Split(tags, ',')/value" />
<xsl:if test="contains($list, 'webinarsondemand')">
<!-- This was tagged with all the tags so render it -->
<xsl:apply-templates select="." />
</xsl:if>
</xsl:if>
</xsl:for-each>
<h3><xsl:text>Demos</xsl:text></h3>
<!-- Go through all nodes that match at least one tag -->
<xsl:for-each select="$nodesToMatch[@id = $matchALL/tagged/nodeId]">
<xsl:variable name="id" select="@id" />
<xsl:if test="count($matchALL/tagged[nodeId = $id]) = $tagCount">
<xsl:variable name="list" select="umbraco.library:Split(tags, ',')/value" />
<xsl:if test="contains($list, 'Demos')">
<!-- This was tagged with all the tags so render it -->
<xsl:apply-templates select="." />
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:template>
<!-- Helper template to collect a node's id -->
<xsl:template match="*" mode="collect">
<nodeId><xsl:value-of select="@id" /></nodeId>
</xsl:template>
<!-- The generic template to render a matching document -->
<xsl:template match="*[@isDoc]">
<p>
<xsl:value-of select="@nodeName" />
<xsl:text /> [<xsl:value-of select="tags" />]<xsl:text />
</p>
<!-- <xsl:value-of select="@nodeName" />
<xsl:text /> [<xsl:value-of select="tags" />]<xsl:text /> -->
</xsl:template>
</xsl:stylesheet>
Filtering nodes based on multiple tags XSLT
I'm using the tags datatype, URL parameters /?filter=a,b,c and XSLT to display the results. I'm using the example here but it returns nodes that match any of the tags instead of all of the tags.
If a user selects a and c, I still want to show nodes that contain those tags, even if they also contain b. Or if they select a,b,c just show nodes that match all three.
Node structure is:
Resource
-Webinar
-Node
-Node
-Brochure
-Node
-Node
-Video
-Node
-Node
I just don't know enough XSLT to customize the logic enough. I'm on Umbraco 6.1.6. Any other info I need to provide?
Here is the XSLT I'm working with: Thanks for any advice or assitance!
Hi BenH,
The fact that the tags are stored in a CSV list makes this a bit harder, but here's something to play with — hopefully you can understand what's going on:
Let me know if you need more help,
/Chriztian
I was hoping you would reply, I'll get this in and reply back! Thank you!
That works perfectly for what we are doing! You are awesome Chriztian, thank you very much. I've got some work to do for our display of the returns but the core functionality is dead on. Thank you so much!
By the way, we are only looping through less than 200 nodes, so this is pretty performant too. I'm removing the ANY match and just leaving the ALL. I'll post my final code including diplay in a few days in case anyone else can use it!
How would I compare the value of a nodes tags to the tagged tag="value" so that I could sort the results into groups based on each tag? So all nodes tagged with brochures would be grouped in one loop, all nodes tagged with videos would be grouped into another section and display like:
Brochures:
-node 1
-node 2
Video:
-node3
-node 4
Would you use an if statement in the render portion of this part? Or a choose?
I'm not sure that makes sense to me - you've performed a search for nodes matching ALL tags - you should get the same output regardless how you sort?
/Chriztian
Sorry, I should give more context!
We are giving the user 3 dropdowns (product, topic, type) to filter by and we want to group the results by one of the choices in the type dropdown. So the results that are returned will always have a type tag that is unique, ie brochure, or webinar, or tool.
We want to display the results of the filtering based on those types. Because I user might not use the type filter and the results for the first 2 filters may include several types.
Brochures:
-nodes returned containing "brochure" tag
-nodes returned containing "brochure" tag
Webinars
-nodes returned containing "Webinar" tag
-nodes returned containing "Webinar" tag
Tools:
-nodes returned containing "Tools" tag
-nodes returned containing "Tools" tag
Apologies if I'm not explaining this clearly!!
I think I have it, I stuck the heading outside the $nodesToMatch for-each loop and then inside each one, added a test for a string value in the tags and then print those nodes. Probably not the best way to do it, but I'm on a tight deadline unfortunately.
Chriztian, how could I go through the for-each loop only three times? Say I have 10 nodes that match all but only want to display the first three?
is working on a reply...