Wrapping the contents of a Content Query Web Part in additional markup
Building content aggregations with the CQWP
You can use the Content Query Web Part to build all kinds of content aggregations: from simple list-like overviews to dynamic carousels. The CQWP supports all of that because it allows you to fully control the output HTML using XSLT. When building the markup of an overview you not only have to consider the markup of items but also the markup surrounding the overview and this has to do with how HTML and CSS work.
It’s a wrap
By wrapping some portion of a page into a container, such as a div, you can isolate that fragment of the page from other elements. Such isolation offers you great benefits not only from the layout perspective but also from the semantic, and therefore accessibility and search engine optimization, point of view.
Another great benefit of wrapping contents into a container element has to do with the cascading properties of CSS. Instead of specifically marking up every single item of a content aggregation as an item of specific type, you can instead mark the whole overview as a specific type of overview and leverage the cascading capabilities of CSS to format the items as desired. Consider the following markup:
<div class="overview-item"> <p><a href="page-1.html">Page 1</a></p> <p>Page 1 description</p> </div> <div class="overview-item"> <p><a href="page-2.html">Page 2</a></p> <p>Page 2 description</p> </div> <div class="overview-item"> <p><a href="page-3.html">Page 3</a></p> <p>Page 3 description</p> </div>
Did you notice the overview-item class that is applied to every div surrounding each item? How about if we could change the markup above as follows:
<div class="overview"> <div> <p><a href="page-1.html">Page 1</a></p> <p>Page 1 description</p> </div> <div> <p><a href="page-2.html">Page 2</a></p> <p>Page 2 description</p> </div> <div> <p><a href="page-3.html">Page 3</a></p> <p>Page 3 description</p> </div> </div>
Isn’t the markup cleaner? By adding the wrapping div and applying a CSS class there we can now remove the CSS class from every item. This not only lowers the page size but also simplifies the maintenance of the page: should we ever need to change anything about this overview, all we need to do is to change the CSS class on the wrapper div and that’s all!
Wrapping the contents of a Content Query Web Part in additional markup
Before we start modifying the CQWP’s XSL files, let’s take a moment to examine how the CQWP works and how the XSL files are used.
Anatomy of CQWP XSL files
The Content Query Web Part uses three XSL files: ContentQueryMain.xsl, Header.xsl and ItemStyle.xsl. The contents of the ContentQueryMain.xsl file are applied once to every aggregation. The Header.xsl file contains XSL templates that are applied once for every group and finally, the ItemStyle.xsl file contains XSL templates that are applied once for every item returned from a content aggregation. Below is the hierarchy in which XSL templates are applied when building the markup in CQWP:
- / (ContentQueryMain.xsl)
- OuterTemplate (ContentQueryMain.xsl)
- OuterTemplate.Body (ContentQueryMain.xsl)
- OuterTemplate.CallHeaderTemplate / OuterTemplate.CallFooterTemplate / OuterTemplate.CallItemTemplate (ContentQueryMain.xsl)
- Specific ItemStyle template (ItemStyle.xsl)
XSL templates 1-3 are executed once for every content aggregation. Templates mentioned under number 4 are wrapped in an for-each loop iterating through all content aggregation items. The OuterTemplate.CallHeaderTemplate and OuterTemplate.CallFooterTemplate XSL templates are applied once for every group and the OuterTemplate.CallItemTemplate template is applied once for every item. Finally the XSL processing moves from the ContentQueryMain.xsl file to ItemStyle.xsl and applies the right Item Style template as configured in the CQWP.
With that there are two ways in which we can wrap the contents of a content aggregation in some additional markup: we can do this from within the ContentQueryMain.xsl file, but we can as well add the surrounding markup to the contents of our Item Style.
Approach 1: Wrapping the contents of a content aggregation from within ContentQueryMain.xsl
The idea behind this approach is fairly easy: in order to wrap the results of a content aggregation in some additional markup we have to apply it before item-level styles are applied, which would be before the templates mentioned under number 4. Given the fact that the OuterTemplate.Body template contains only the for-reach loop iterating trough content aggregation items, it would be the best to have our markup added in the OuterTemplate template.
Because we want our additional markup to be ItemStyle-specific we first have to retrieve the name of the ItemStyle used by the CQWP. For this add the following code snippet in the ContentQueryMain.xsl file in line 43, just after the IsEmpty variable definition:
<xsl:variable name="ItemStyle" select="$Rows/@Style" />
Next we are ready to start adding the wrapping markup. Add the following code snippet in line 57 just before calling the OuterTemplate.Body template:
<xsl:choose> <xsl:when test="$ItemStyle = 'Default'"> <xsl:text disable-output-escaping="yes"><![CDATA[<div class="overview">]]></xsl:text> </xsl:when> </xsl:choose>
And then add the following code snippet in line 67 right after the closing tag of the call to the OuterTemplate.Body template:
<xsl:choose> <xsl:when test="$ItemStyle = 'Default'"> <xsl:text disable-output-escaping="yes"><![CDATA[</div>]]></xsl:text> </xsl:when> </xsl:choose>
Using the xsl:choose statement we start the logic of conditional application of the additional markup to our overviews. Although we could as well use the xsl:if comparison, as we have only one comparison at this moment, the odds are high that you might want to add surrounding markup to multiple ItemStyles.
Next we compare the name of the currently used Item Style and if it matches we start adding the surrounding markup. Because HTML in XSLT is considered XML it has to be well formatted to prevent errors. Since we are wrapping the contents of the OuterTemplate.Body template we have to split our HTML in the before and the after piece and in order to prevent XML formatting errors, we have to escape it using the xsl:text element with the disable-output-escaping attribute set to yes. Finally, because our HTML is invalid, it also has to be wrapped in a <![CDATA]> block. Once we have the before markup done, we have to do the same to add the closing markup.
Although this approach works and is quite easy to implement, it has one serious flaw. While the markup surrounding the content aggregation is Item Style-specific, we have added it to the ContentQueryMain.xsl file. With that we have created a tight coupling between the Item Styles defined in the ItemStyle.xsl file and the additional logic, which is required by the Item Style but which is present in the ContentQueryMain.xsl file. Should we ever want to reuse the specific Item Style, we would have to keep in mind to copy the surrounding markup from the ContentQueryMain.xsl file. The big advantage of choosing for this approach is the fact that it deals properly with the additional markup that the CQWP applies to every item which I described in more detail in my previous article.
Approach 2: Wrapping the contents of a content aggregation using ItemStyle.xsl and counters
Did you know that when applying XSL templates, the Content Query Web Part counts the position of which row it is processing at the moment? Using that counter we could add surrounding markup to our ItemStyles!
Important: By default the Content Query Web Part adds some HTML markup around every item from within the ContentQueryMain.xsl file. This approach will work only if you suppress this markup using the approach I presented in my previous article.
Although the Content Query Web Part stores the information about the position of the content aggregation item being processed at the time, it doesn’t make it available within Item Styles by default. We can change this behavior by applying the following modifications in the ContentQueryMain.xsl file.
Add the following code snippet in line 130 just after passing the CurPosition as parameter:
<xsl:with-param name="LastRow" select="$LastRow" />
Next add the following code snippet in line 149 right after defining the CurPosition parameter:
<xsl:param name="LastRow" />
Finally add the following code snippet in line 169 right after the call to apply Item Style templates:
<xsl:with-param name="CurPos" select="$CurPosition" /> <xsl:with-param name="LastRow" select="$LastRow" />
With that we are ready to leverage counters in our Item Styles to add surrounding markup. In this example we will extend the TitleOnly Item Style defined in the ItemStyle.xsl file.
First we need to extend the definition of our Item Style so that it receives counter information from ContentQueryMain.xsl. For this add the following code snippet in line 114 right under the definition of the TitleOnly XSL template:
<xsl:param name="CurPos" /> <xsl:param name="LastRow" />
With that we can start adding the surrounding markup. Add the following code snippet in line 127 right after defining the last variable:
<xsl:if test="$CurPos = 1"> <xsl:text disable-output-escaping="yes"><![CDATA[<div class="overview">]]></xsl:text> </xsl:if>
And then add the following code snippet in line 146 right before closing the XSL template:
<xsl:if test="$CurPos = $LastRow"> <xsl:text disable-output-escaping="yes"><![CDATA[</div>]]></xsl:text> </xsl:if>
As you can see adding the surrounding HTML markup is very similar to how we did it in the previous approach. The only difference is in the fact that in this approach we compare the number of the row to determine whether we should output the surrounding markup or not.
The great benefit of using this approach is the fact that all of the markup that belongs to that Item Style is defined within that Item Style itself. Should you ever want to reuse that Item Style, all you need to do, is to ensure, that the CQWP passes the item counter information to the Item Styles.
Building self-contained Item Styles
When building content aggregations using the Content Query Web Part there are often situations when you need to wrap the results of the aggregation in some additional markup. The CQWP offers you multiple approaches to do this, and depending on what you are trying to achieve, you should make an educated choice about which approach fits your scenario the best.