Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • jacob phillips 130 posts 372 karma points
    May 13, 2014 @ 06:26
    jacob phillips
    0

    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?

  • Chriztian Steinmeier 2798 posts 8788 karma points MVP 7x admin c-trib
    May 13, 2014 @ 08:17
    Chriztian Steinmeier
    100

    Hi Jacob,

    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...

    /Chriztian

  • jacob phillips 130 posts 372 karma points
    May 13, 2014 @ 08:27
    jacob phillips
    0

    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....

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#x00A0;"> ]>
    <xsl:stylesheet 
        version="1.0" 
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
        xmlns:msxml="urn:schemas-microsoft-com:xslt"
        xmlns:msxsl="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:SqlHelper="urn:SqlHelper" xmlns:umbraco.contour="urn:umbraco.contour" xmlns:tags="urn:tags" 
        exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets SqlHelper umbraco.contour tags msxsl ">
    
    
    <xsl:output method="xml" omit-xml-declaration="yes"/>
    
    <xsl:param name="currentPage"/>
    
    <xsl:template match="/">
    
    <xsl:variable name="DateNow" select="umbraco.library:CurrentDate()" />
    
    <xsl:variable name="eclectics" select="umbraco.library:GetXmlNodeById(14257)//story[contains(concat(',',primaryCategory,','),concat(',','Eclectic',',')) and archiveDate != '' and umbraco.library:DateGreaterThan($DateNow, displayDate)]" />
    <xsl:variable name="eclectic">
    <xsl:for-each select="$eclectics">
    <xsl:sort select="displayDate"/>
    <xsl:if test="position() &lt; 2" >
    <xsl:copy-of select="." />
    </xsl:if>
    </xsl:for-each>
    </xsl:variable>
    
    <xsl:variable name="classicals" select="umbraco.library:GetXmlNodeById(14256)//story[contains(concat(',',primaryCategory,','),concat(',','Classical',',')) and archiveDate != '' and umbraco.library:DateGreaterThan($DateNow, displayDate)]" />
    <xsl:variable name="classical">
    <xsl:for-each select="$classicals">
    <xsl:sort select="displayDate"/>
    <xsl:if test="position() &lt; 2" >
    <xsl:copy-of select="." />
    </xsl:if>
    </xsl:for-each>
    </xsl:variable>
    
    <xsl:variable name="latestposts">
    <container>
    <xsl:copy-of select="$eclectic" />
    <xsl:copy-of select="$classical" />
    </container>
    </xsl:variable>
    
    <xsl:for-each select="msxsl:node-set($latestposts)/container/story">
    <xsl:value-of select="./@id" />
    </xsl:for-each>
    
    </xsl:template>
    
    </xsl:stylesheet>
    
  • jacob phillips 130 posts 372 karma points
    May 13, 2014 @ 20:49
    jacob phillips
    0

    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?

                <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:

    <xsl:apply-templates select="($classicalRoot | $eclecticRoot | $jazzRoot | $rootsRoot)//story[@id = $latest]" />
    

    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.

  • Chriztian Steinmeier 2798 posts 8788 karma points MVP 7x admin c-trib
    May 13, 2014 @ 23:20
    Chriztian Steinmeier
    0

    Hi Jacob,

    Great questions! I'll do my very best...

    1. 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.

    2. 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.

    3. 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.

    /Chriztian

  • jacob phillips 130 posts 372 karma points
    May 17, 2014 @ 01:26
    jacob phillips
    0

    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:

    <xsl:template match="/">
        <xsl:apply-templates select="($classicalRoot | $eclecticRoot | $jazzRoot | $rootsRoot)//story[@id = $latest]" />
    </xsl:template>
    

    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?

  • Chriztian Steinmeier 2798 posts 8788 karma points MVP 7x admin c-trib
    May 20, 2014 @ 00:36
    Chriztian Steinmeier
    0

    Hi Jacob - I have answers for you :-)

    1. 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.

    2. 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…

    3. 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).

    /Chriztian

  • jacob phillips 130 posts 372 karma points
    May 20, 2014 @ 02:18
    jacob phillips
    0

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

     

     

  • Chriztian Steinmeier 2798 posts 8788 karma points MVP 7x admin c-trib
    May 20, 2014 @ 09:47
    Chriztian Steinmeier
    0

    You're right - using -1 takes it down, so the other simpler rules will just work without having to boost everyone of them :)

    /Chriztian

  • jacob phillips 130 posts 372 karma points
    May 20, 2014 @ 20:56
    jacob phillips
    1

    Thank you for clarifying that.

    So, wait. I looked at this,

    and I read this statement:

    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"?

  • Chriztian Steinmeier 2798 posts 8788 karma points MVP 7x admin c-trib
    May 20, 2014 @ 21:36
    Chriztian Steinmeier
    1

    Hi Jacob - you have all the great questions! :)

    Well, think about this:

    Whenever you set a variable like this:

    <xsl:variable name="specialNode" select="/root/Website[1]/Textpage[1]" />
    

    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:

    <xsl:param name="currentPage" select="document('../App_Data/Umbraco.config')//*[@id = 1234]" />
    

    -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

  • jacob phillips 130 posts 372 karma points
    May 21, 2014 @ 01:12
    jacob phillips
    0

    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:

    <xsl:param name="currentPage" select="document('../App_Data/Umbraco.config')//*[@id = 1234]" />
    

    And with that I can just write select="$currentPage/@id" and get the nodeid 1234.

    My expectation though, is that when I write out:

    <xsl:copy-of select="$currentPage" />
    

    I should get something like:

    -<ThisDocumentType id="1234" nodeName="some name" transcript="lorem ipsum whatever">
    --<somePropertyOfThisDocumentType>blah</somePropertyOfThisDocumentType>
    --<ThatDocumentType id="1235" nodeName="hi, I'm a node under 1234">
    ---<somePropertyOfThatDocumentType>blah</somePropertyOfThatDocumentType>
    --</ThatDocumentType>
    -</ThisDocumentType>
    

    But I get the whole content tree instead. I don't get that.

Please Sign in or register to post replies

Write your reply to:

Draft