build a node set based off latest item from multiple sections
Looking for advice on an approach to storing a set of single nodes, per matching value on a property.
I have a drop-down-list datatype defined on a document type called category ('Jazz', 'Rock', 'News', etc.) I want to get the single latest item, from 'Jazz' and 'Rock' marked pages (but not 'News').
Further, all of the 'Jazz' category nodes are in a single sub-tree. Likewise, all 'Rock' category nodes are in a separate sub-tree at the same level as the 'Jazz' sub-tree:
-Jazz Node
--Jazz category sub node 1
--Jazz category sub node 2
...
--Jazz category sub node n
-Rock Node
--Jazz category sub node 1
--Jazz category sub node 2
...
Jazz category sub node n
I have done a lot with lists, but I do not think I have ever created a macro, where I pick off ONE latest node from each of a list of areas (e.g. Jazz, Rock), and then combine those nodes into a node-set, and finally, loop through the node set.
I think my macro should just have a section for each category, and store the latest node from each category in a variable? Then combine all the variables into a node set? Then loop through it?
I think my macro should just have a section for each category, and store the latest node from each category in a variable? Then combine all the variables into a node set? Then loop through it?
You're totally right - that's how you need to do it:
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:umb="urn:umbraco.library"
xmlns:make="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="umb make"
>
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:param name="currentPage" />
<xsl:variable name="siteRoot" select="$currentPage/ancestor-or-self::*[@level = 1]" />
<!-- Grab a reference to all the "roots" -->
<xsl:variable name="jazzRoot" select="$siteRoot/Category[@nodeName = 'Jazz']" />
<xsl:variable name="rockRoot" select="$siteRoot/Category[@nodeName = 'Rock']" />
<xsl:variable name="latestProxy">
<!-- For every root node, -->
<xsl:for-each select="$jazzRoot | $rockRoot">
<!-- go through its siblings, -->
<xsl:for-each select="*[@isDoc]">
<!-- sorted by date updated (latest first) -->
<xsl:sort select="@updateDate" data-type="text" order="descending" />
<!-- grab the first one's `@id` -->
<xsl:if test="position() = 1">
<nodeId><xsl:value-of select="@id" /></nodeId>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<!-- Create an XPath-navigable version of that set -->
<xsl:variable name="latest" select="make:node-set($latestProxy)/nodeId" />
<xsl:template match="/">
<!-- Process the nodes in the tree that have an `@id` matching the ones we found to be the latest -->
<xsl:apply-templates select="($jazzRoot | $rockRoot)/*[@id = $latest]" />
</xsl:template>
<!-- Template for rendering the nodes -->
<xsl:template match="*[@isDoc]">
<p><xsl:value-of select="@nodeName" /></p>
</xsl:template>
</xsl:stylesheet>
Note: You will most likely need to change the jazzRoot and rockRoot variables at the top, depending on your aliases...
Ha, you beat me to it. Here's what I came up with in the meantime, I will compare to yours, because I'm still really trying to get down the apply-templates thing....
1) When building the nodeset in the variable latestProxy, what is the difference between using
"value-of" vs. "copy-of", specifically to create node-sets?
<nodeId><xsl:value-of select="@id" /></nodeId>
I've seen some other examples where copy-of is used, and in this case, it seemed to break it.
Also,
In this section:
<!-- go through its siblings, -->
<xsl:for-each select="*[@isDoc]">
It is looping through the siblings under the root (like in the example content, thanks).
Actually, I have my nodes that I want to get in date folders, so it's like this:
-Jazz Node
--2014
---05
----11
-----Jazz category sub node 1
--2014
---05
----12
-----Jazz category sub node 2
...
--[some year folder]
---[some month folder]
----[some day folder]
-----Jazz category sub node n
So I changed my selector syntax from *[@isDoc] to *//story (that's the document type) at the leaf level of the date folders. This also required me to adjust the apply templates select:
2) Are there anything obviously wrong with what I've done (just wondering if I'm losing something by not include the @isDoc, e.g. (story[@isDoc][@id = $latest] or something like that)?
3) In the final template match for rendering nodes here:
<!-- Template for rendering the nodes -->
<xsl:template match="*[@isDoc]">
What is the thinking about he match argument *[@isDoc]?
That is, when would it ever be different?
It is so nice, organizationally speaking. Thank you for this elegance.
The reason I use "shallow" <nodeId>1233</nodeId> elements in the variable (and thus <xsl:value-of /> to get the id instead of <xsl:copy-of /> to get a full copy) is because I already have the nodes in the various variables - I just need the id to get to it again. Using a copy of the node also copies the full subtree, which could be a lot of extra nodes - and since <xsl:copy-of /> actually copies the nodes there could be a lot of processing hidden there. Another problem with copied nodes is that they lose their context (so in the template or for-each that processes a copy, you can only "see" its descendants - not its ancestors or siblings, which you may often need for lookups, etc.). When you only store the id and subsequently process the "real" node, you have the usual Umbraco-All-Access to the surrounding tree.
What you did, using //story in the for-each and apply-templates is perfectly good - using the alias is much better - but it's not always known (obviously I couldn't know it beforehand :-) the [@isDoc] is just something you need to do when creating an XSLT you want to be able to reuse, or a template for Umbraco which is going to be used on any alias imaginable.
Again, the [@isDoc] in the match is just a generic "match any Umbraco document type" selector. The great thing about it is that you could use that one, and if you had different document types you could add templates for each of those separately, to have them be rendered correctly - you can even do stuff like this:
<!--
Generic template for rendering (the [@isDoc] increases the default priority,
so we'll take it down a notch by explicitly specifying a custom priority)
-->
<xsl:template match="*[@isDoc]" priority="-1">
<p><xsl:value-of select="@nodeName" /></p>
</xsl:template>
<!-- Template for stories -->
<xsl:template match="story">
<section class="story">
<!-- ... -->
</section>
</xsl:template>
<!-- One specific story needs to be different -->
<xsl:template match="story[@nodeName = 'Very specific story']">
<section class="story special">
<!-- Some special output etc. -->
</section>
</xsl:template>
I hope you can see that there's a great deal of power/beauty in using templates.
Yes, I'm starting too, and I know it holds a lot of the answers to things I want to do, but have not been able to yet.
I think that I am still hazy on some of the bigger picture....
Maybe you can further my understanding by answering these three questions...
After studying your example for Generic docTypes above, I'm hung up on a concept.
1.) Does every XSLT need to have this:
<xsl:template match="/">
as an entry point?
I've always thought of that as the "place execution starts" when I call an XSLT.
As another example, on this last XSLT, I look at this as the starting point:
and I think to myself: "when you use apply-templates in the match="/", it is then going to look in the rest of the file, for ANY template definition, and if the match criteria is met, it is going to execute that, i.e.:
<xsl:template match="*[@isDoc]">
will execute because it is the generic match.
2.) Is that a correct understanding?
So if I am right, an XSLT (in this umbraco context) could have multiple template matches. Like, in your example above:
<xsl:template match="*[@isDoc]" priority="-1">
<p><xsl:value-of select="@nodeName" /></p>
</xsl:template>
<!-- Template for stories -->
<xsl:template match="story">
<section class="story">
<!-- ... -->
</section>
</xsl:template>
<!-- One specific story needs to be different -->
<xsl:template match="story[@nodeName = 'Very specific story']">
<section class="story special">
<!-- Some special output etc. -->
</section>
</xsl:template>
I'm thinking, if ANY of the match criteria are met, that template match could execute.
3.) Or does only the first one execute whose criteria is met?
It’s OK to think of the root template as the starting point for an XSLT stylesheet; it’s correct in the sense that it’s the first thing the processor looks for, but it’s not required because there’s a built-in template that kicks in, if it isn’t found.
The thing is, in Umbraco you need to handle this scenario in 98% of all cases because of the way the Umbraco XML document is handed to you, thus setting the context of selections etc.
Not quite - when the processor starts, it grabs the root node (i.e. “/“) of the <macro> XML chunk, and asks the templates in the stylesheet if any of ‘em knows how to handle it. If none of them answers, the built-in template will be used - it just says <xsl:apply-templates /> which means that the processor will then select all the children of the <macro> element and start finding templates for them, one at a time. But that document is rather empty, so not many other templates would be executed…
The way it works with the template matching is that only the template with the best match will be executed - or a little more correct: Of all the templates that matches, the one with the highest priority will be executed. And if two templates has the same priority, the last one defined will “win”.
There are some rules as to how the priority is calculated, but as a rule of thumb you should just know that *[@isDoc] is a more specific pattern than Textpage and thus the former will have a higher priority. You can add a priority attribute to a template, to boost its calculated value (which I did in the code above).
And glad you clarified with the priority. I will leave it as an exercise to figure out if "-1" is demoting the priority or elevating it. Intuitively, it
looks like demotion (although perhaps "boost" is a generic term for changing the priority either up or down.)
Umbraco then provides access to the entire website structure through the currentPage XSLT param, which acts as a pointer to the page that's currently being rendered.
How can $currentPage both provide access to the entire website structure AND act as a pointer to the page that's currently being rendered?
I've read the 2nd part earlier and so always thought of the $currentPage param as giving me the XML sub-tree starting on the node the macro is run from on down. But I have worked through examples where I printed out $currentPage using copy-of and noticed it gives me almost the whole cache. I have been confused by this.
So how is $currentPage param "a pointer to the page that's currently being rendered"?
You create a pointer to the first <Textpage> element in the first <Website> below the <root> element. This lets you access attributes, textnodes and childnodes of that node, but you can also ask for its parent, ancestors and siblings, just by using XPath.
The $currentPage parameter actually works just as if you'd actually written it like this:
Ok, but how come if you print out the contents of currentPage, it gives everything?
I'm guessing $currentPage is much more than just a variable with some xml inside of it. It
most be more like some kind of object.
Because, let's say I have a node with id=1234 and document type 'ThisDocType' a few levels down in the content tree.
Then, underneath that node, I have document type 'ThatDocumentType' with node id = 1235.
Then, I have a macro which is included in a template for node w/id=1234. Then as you have pointed out, when the page renders, currentPage param in the macro is really like this:
build a node set based off latest item from multiple sections
Looking for advice on an approach to storing a set of single nodes, per matching value on a property.
I have a drop-down-list datatype defined on a document type called category ('Jazz', 'Rock', 'News', etc.) I want to get the single latest item, from 'Jazz' and 'Rock' marked pages (but not 'News').
Further, all of the 'Jazz' category nodes are in a single sub-tree. Likewise, all 'Rock' category nodes are in a separate sub-tree at the same level as the 'Jazz' sub-tree:
-Jazz Node --Jazz category sub node 1 --Jazz category sub node 2 ... --Jazz category sub node n
-Rock Node --Jazz category sub node 1 --Jazz category sub node 2 ... Jazz category sub node n
I have done a lot with lists, but I do not think I have ever created a macro, where I pick off ONE latest node from each of a list of areas (e.g. Jazz, Rock), and then combine those nodes into a node-set, and finally, loop through the node set.
I think my macro should just have a section for each category, and store the latest node from each category in a variable? Then combine all the variables into a node set? Then loop through it?
Or would you take a different approach?
Hi Jacob,
You're totally right - that's how you need to do it:
Note: You will most likely need to change the
jazzRoot
androckRoot
variables at the top, depending on your aliases.../Chriztian
Ha, you beat me to it. Here's what I came up with in the meantime, I will compare to yours, because I'm still really trying to get down the apply-templates thing....
Chriztian, I had a few questions...
1) When building the nodeset in the variable latestProxy, what is the difference between using "value-of" vs. "copy-of", specifically to create node-sets?
I've seen some other examples where copy-of is used, and in this case, it seemed to break it.
Also,
In this section:
It is looping through the siblings under the root (like in the example content, thanks). Actually, I have my nodes that I want to get in date folders, so it's like this:
So I changed my selector syntax from *[@isDoc] to *//story (that's the document type) at the leaf level of the date folders. This also required me to adjust the apply templates select:
2) Are there anything obviously wrong with what I've done (just wondering if I'm losing something by not include the @isDoc, e.g. (story[@isDoc][@id = $latest] or something like that)?
3) In the final template match for rendering nodes here:
What is the thinking about he match argument *[@isDoc]? That is, when would it ever be different?
It is so nice, organizationally speaking. Thank you for this elegance.
Hi Jacob,
Great questions! I'll do my very best...
The reason I use "shallow"
<nodeId>1233</nodeId>
elements in the variable (and thus<xsl:value-of />
to get the id instead of<xsl:copy-of />
to get a full copy) is because I already have the nodes in the various variables - I just need the id to get to it again. Using a copy of the node also copies the full subtree, which could be a lot of extra nodes - and since<xsl:copy-of />
actually copies the nodes there could be a lot of processing hidden there. Another problem with copied nodes is that they lose their context (so in the template or for-each that processes a copy, you can only "see" its descendants - not its ancestors or siblings, which you may often need for lookups, etc.). When you only store the id and subsequently process the "real" node, you have the usual Umbraco-All-Access to the surrounding tree.What you did, using
//story
in the for-each and apply-templates is perfectly good - using the alias is much better - but it's not always known (obviously I couldn't know it beforehand :-) the[@isDoc]
is just something you need to do when creating an XSLT you want to be able to reuse, or a template for Umbraco which is going to be used on any alias imaginable.Again, the [@isDoc] in the match is just a generic "match any Umbraco document type" selector. The great thing about it is that you could use that one, and if you had different document types you could add templates for each of those separately, to have them be rendered correctly - you can even do stuff like this:
I hope you can see that there's a great deal of power/beauty in using templates.
/Chriztian
Yes, I'm starting too, and I know it holds a lot of the answers to things I want to do, but have not been able to yet. I think that I am still hazy on some of the bigger picture....
Maybe you can further my understanding by answering these three questions...
After studying your example for Generic docTypes above, I'm hung up on a concept.
1.) Does every XSLT need to have this:
as an entry point?
I've always thought of that as the "place execution starts" when I call an XSLT.
As another example, on this last XSLT, I look at this as the starting point:
and I think to myself: "when you use apply-templates in the match="/", it is then going to look in the rest of the file, for ANY template definition, and if the match criteria is met, it is going to execute that, i.e.:
will execute because it is the generic match.
2.) Is that a correct understanding?
So if I am right, an XSLT (in this umbraco context) could have multiple template matches. Like, in your example above:
I'm thinking, if ANY of the match criteria are met, that template match could execute.
3.) Or does only the first one execute whose criteria is met?
Hi Jacob - I have answers for you :-)
It’s OK to think of the root template as the starting point for an XSLT stylesheet; it’s correct in the sense that it’s the first thing the processor looks for, but it’s not required because there’s a built-in template that kicks in, if it isn’t found. The thing is, in Umbraco you need to handle this scenario in 98% of all cases because of the way the Umbraco XML document is handed to you, thus setting the context of selections etc.
Not quite - when the processor starts, it grabs the root node (i.e. “/“) of the
<macro>
XML chunk, and asks the templates in the stylesheet if any of ‘em knows how to handle it. If none of them answers, the built-in template will be used - it just says<xsl:apply-templates />
which means that the processor will then select all the children of the<macro>
element and start finding templates for them, one at a time. But that document is rather empty, so not many other templates would be executed…The way it works with the template matching is that only the template with the best match will be executed - or a little more correct: Of all the templates that matches, the one with the highest priority will be executed. And if two templates has the same priority, the last one defined will “win”. There are some rules as to how the priority is calculated, but as a rule of thumb you should just know that
*[@isDoc]
is a more specific pattern thanTextpage
and thus the former will have a higher priority. You can add a priority attribute to a template, to boost its calculated value (which I did in the code above)./Chriztian
Awesome.
Thanks, sinking in.
And glad you clarified with the priority. I will leave it as an exercise to figure out if "-1" is demoting the priority or elevating it. Intuitively, it
looks like demotion (although perhaps "boost" is a generic term for changing the priority either up or down.)
You're right - using -1 takes it down, so the other simpler rules will just work without having to boost everyone of them :)
/Chriztian
Thank you for clarifying that.
So, wait. I looked at this,
and I read this statement:
How can $currentPage both provide access to the entire website structure AND act as a pointer to the page that's currently being rendered?
I've read the 2nd part earlier and so always thought of the $currentPage param as giving me the XML sub-tree starting on the node the macro is run from on down. But I have worked through examples where I printed out $currentPage using copy-of and noticed it gives me almost the whole cache. I have been confused by this.
So how is $currentPage param "a pointer to the page that's currently being rendered"?
Hi Jacob - you have all the great questions! :)
Well, think about this:
Whenever you set a variable like this:
You create a pointer to the first
<Textpage>
element in the first<Website>
below the<root>
element. This lets you access attributes, textnodes and childnodes of that node, but you can also ask for its parent, ancestors and siblings, just by using XPath.The
$currentPage
parameter actually works just as if you'd actually written it like this:-but only smarter, right? 'Coz your XSLT doesn't know the ID of the current page being rendered.
A global param can be initialized from .NET, which is what Umbraco does in this case.
/Chriztian
Ok, but how come if you print out the contents of currentPage, it gives everything?
I'm guessing $currentPage is much more than just a variable with some xml inside of it. It most be more like some kind of object.
Because, let's say I have a node with id=1234 and document type 'ThisDocType' a few levels down in the content tree. Then, underneath that node, I have document type 'ThatDocumentType' with node id = 1235.
Then, I have a macro which is included in a template for node w/id=1234. Then as you have pointed out, when the page renders, currentPage param in the macro is really like this:
And with that I can just write select="$currentPage/@id" and get the nodeid 1234.
My expectation though, is that when I write out:
I should get something like:
But I get the whole content tree instead. I don't get that.
is working on a reply...