Implementing CSR display templates as a custom list view is one of my favorite things about Client-side Rendering in SharePoint. The reasons are twofold:
- First, we take full control of the view. We’re not just rendering some little piece (i.e. one field) of a form about which we’re not supposed to know anything.
- Even better, the deployment for this is very simple, in fact, it can be done entirely through the OOB SharePoint user interface…sort of!
The devil’s in the details and every SharePoint developer-related topic seems to include a “sort of”, but still custom list view display templates in SharePoint are pretty cool.
For starters, let’s talk about what we’re going to build. I want to build an FAQ (i.e. Frequently Asked Questions). The requirements are:
- Show a list of questions.
- If the user clicks a question, toggle the visibility of the answer. Don’t muck with any other answers (i.e. two or more answers can be visible at the same time).
- The user should be able to expand/collapse all answers with a single click.
So here is what my implementation will look like:
Fig 1: What We’re Building
I’m going to use an announcements list to store my data. The title will be the question and the body will be the answer. The FAQ, of course, will be a view on the list. From the picture, you can probably guess that I’m going to be using jQuery and jQuery-UI to get the accordion functionality. And that’s where the “sort of” comes into play, so let’s talk about that now.
Custom List View Dependencies
I’m putting the cart before the horse a bit here because I haven’t talked about how to deploy list display templates yet, but trust me when I tell you that you can easily use the OOB user interface to deploy these templates, but sadly there is no such way to specify dependencies. My template will depend on jQuery, jQuery-UI, and the CSS for jQuery-UI, so if I can’t specify those dependencies how am I going to ensure that they are met.
Scriptlink User Custom Actions
The answer is that I’m going to load those dependencies at the site level as ScriptLink user custom actions. I’ll include a utility page in the download called SetScriptlinks.aspx, and below is a screenshot of how I’ll use this to configure the user custom actions I’ll need. This will cause these scripts to be loaded on every page in the site collection. If my list display template were the only place I was using these dependencies, loading them this way would be overkill. But I’ve already shown a number of CSR templates using jQuery. Once I’ve configured these custom actions, I should go back and remove jQuery and jQuery-UI from all of the JSLink properties I set it on before, and just assume it will be loaded. At this point, I’m going to argue that I’m using them in enough pages to justify loading them on every page, and trust to browser-based caching to achieve reasonable performance.
Fig 3: Set Script Link Utility Page
If you can’t justify loading your dependencies on every page, you’re going to have to do something more creative and dynamic. Like loading the scripts in JavaScript, and ensuring that they’ve been processed before you proceed to use them. This is what SharePoint’s Scripts on Demand (SOD) does, as well as libraries like RequireJS and HeadJS, so you may not need to roll your own solution. But these things all add a layer of complexity that I’d prefer to avoid for such a small solution if possible.
Scriptlink Limitations
And keep in mind that this solution only works for scripts that are located somewhere in your site collection. If you try to load an external script as a user custom action, SharePoint will let you set it but then it breaks your site (as in loads every page as a blank page). And you will only be able to fix this programmatically from outside the site (like Powershell or CSOM code). For this reason, the utility page ignores any entry that does not begin with ~sitecollection or ~site. It also ignores any lines that do not end with .js or .css.
Also, note that SharePoint doesn’t really let you load user custom actions for CSS. Scriptlinks may only be of type script. The utility page converts CSS references to ScriptBlock custom actions that look basically like the code below. ScriptBlock custom actions are basically just chunks of JavaScript that you want SharePoint to load.
1 2 3 | document.write("<link rel='stylesheet' " + "type='text/css' " + "href='https://intellipointsol.sharepoint.com/sites/csrdemos/style library/jquery-ui.css'>"); |
Custom List View Templates 101
The image below shows all of the things that you can override in a custom list view display template, and the corresponding section of the custom list view that is rendered by each callback. Of course, OnPreRender and OnPostRender don’t specifically point to anything, because they’re not responsible for rendering any part of the form.
Keep in mind that in this visualization, there are two kinds of stages. There are sequential stages, like OnPreRender/View/OnPostRender or Header/Body/Footer. But there are also nested stages, like Body/Group/Item/Field. These differences in the visualization are significant.
You can override a sequential stage like OnPreRender, and after you finish doing what you want to do, the CSR engine will call View to keep the process moving along. You don’t have to do anything or even be aware that there is a next stage in your code.
But if you override a stage with nested stages, you’re really overriding that stage and any internal nested stages of that stage. For example, if you override Body and just return “&nbps;”, no renderer is called for Group, Item, or Field, and the entire body will include only a non-breaking space. You either have to render Group, Item, and Field yourself, or you need to call the renderer for Group and let it render Group, Item and Field, however, it chooses to do so. I haven’t had a need to call one of these nested renders, so I haven’t figured out how to do it, but it’s just a matter of stepping through clienttemplates.js and seeing how it implements Body, or Group, or whatever and then mimicking that behavior.
It’s also important to understand when the actual HTML gets shoved into the DOM. All of the callbacks prior to OnPostRender are just building up a big HTML string. Then it gets pushed into the DOM, and then OnPostRender gets called. So if you want to attach event handlers or say, I don’t know, maybe apply something like a jQuery-UI accordion to the DOM, OnPostRender is the place to do it.
I’m not going to go any deeper into it; I’ll include some references below that will go as deep as you like. But if you understand the picture above, you know enough to understand the code below.
The Accordion Custom List View Implementation
Below is the shell for the display template implementation. Other than the fact that it overrides a bunch of stuff that wasn’t available to be overridden in my previous templates if you’ve read my previous CSR blog posts it shouldn’t look that foreign.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | (function() { /** * Implementation class for an Accordion View Display Template. */ $.accordionViewer = { /** * Get the html for the view header. * @param {object} the rendering context. * @return {string} the HTML text. */ getHeader: function(ctx) { // TODO }, /** * Get the html for a single item in the view. * @param {object} the rendering context. * @return {string} the HTML text. */ getItem: function(ctx) { // TODO }, /** * Invoke jQuery-UI's accorion on each item, and wire up the expand/collapse all link events. * @param {object} the rendering context. */ postRender: function(ctx) { // TODO } }; // our overrides instance var overrides = { Templates: { Header: $.accordionViewer.getHeader, Item: $.accordionViewer.getItem, Footer: " " }, OnPostRender: $.accordionViewer.postRender }; if (typeof _spPageContextInfo != 'undefined' && _spPageContextInfo != null) { // MDS is enabled var url = (_spPageContextInfo.siteServerRelativeUrl === '/' ? "" : _spPageContextInfo.siteServerRelativeUrl) + '/_catalogs/masterpage/Display Templates/List Views/AccordionViewCSR.js'; // register a callback to register the templates on partial page loads RegisterModuleInit(url, function() { SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides); }); } // register for full page loads (F5/refresh/Or Non-MDS) SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides); })(jQuery); |
Nothing very earth shattering here. To implement my accordion, I’m overriding Header, Item, Footer, and OnPostRender. But hang on, I’m overriding Footer with a string. And not just any string, it contains a single space. Hmm…that doesn’t seem right. Why would I do that?
The Journey that Brought Me Here
As I was trying to get this to work on SharePoint 2016 the first time, the process went like this:
- First cut, I didn’t override Footer at all. This had some strange consequences. It basically outputs all of the column headers from the OOB list view (i.e. all of the pieces that Andrei Markeev’s diagram said were produced by Header, not Footer?). So my accordion view rendered fine, but at the bottom was an empty table that looks just like an OOB list view.
- Anyway, fine, I went ahead and overrode Footer and just returned an empty string. And the result…no change. I still had an empty table at the bottom of my accordion. WTF?
- Finally, I tried just returning some valid HTML like “<span>Hello World</span>”. No more empty table, and welcome “Hello World”, my old friend.
Changing it back to an empty string, I brought up the debugger, broke on my Footer (now converted into a function that returned an empty string so I could break on it), and stepped out into clienttemplates.js. What I found was something that looked basically like this pseudo-code:
1 2 3 | if( !render(ctx) ) { // revert to the OOB renderer } |
So it’s taking the result of my render method (a string) and treating it like a Boolean. And if it evaluates to false, it calls the OOB rendering method instead. But in JavaScript, null, undefined, or an empty string all evaluate to false. So in order to avoid the OOB rendering for footer (which somehow renders what I expect Header to render), I have to override Footer and return something that evaluates to true (a non-empty string). Now as I’m writing this post I’m working on SharePoint online, and it works as I expect with either of Footer returning an empty string or no Footer override at all. But since I’d like my code to work on all versions of SharePoint (at least all versions that support CSR), I override footer and return a string with a single space.
The Header
Not much to say here. This method just produces the HTML for two anchor tags for expand and collapse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Get the html for the view header. * @param {object} the rendering context. * @return {string} the HTML text. */ getHeader: function(ctx) { var result = $("<p/>"); // placeholder, will return inner html // add the expand link result.append($("<a/>", { "class": "expand", "href": "javascript:void(0)", "style": "margin-right: 10px; text-decoration: underline" }).text("Expand")); // add the collapse link result.append($("<a/>", { "class": "collapse", "href": "javascript:void(0)", "style": "text-decoration: underline" }).text("Collapse")); // return the HTML to be rendered return result.html(); }, |
The Items
The getItem method returns the following HTML for each item in the view.
1 2 3 4 5 6 | <div class="accordion" style="width:800px"> <h3 class="accordion-part" style="font-weight:bold">The question?</h3> <div class="accordion-part"> The answer </p> </p> |
Note that the outer div for each item will be an accordion with exactly one content area. The reason for this is that jQuery-UI accordion doesn’t support opening multiple content areas at the same time, which doesn’t satisfy two of my requirements. But by producing an accordion for each item, all stacked on top of each other, it still looks like an accordion but acts the way I want it to. Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * Get the html for a single item in the view. * @param {object} the rendering context. * @return {string} the HTML text. */ getItem: function(ctx) { var result = $("<p/>"); // placeholder, will return inner html // add an outer div for the accordion var div = $("<div/>", { "class": "accordion", "style": "width: 800px" }); // add a header for the accordion div.append($("<h3/>", { "class": "accordion-part", "style": "font-weight: bold;" }).text(ctx.CurrentItem.Title)); // add the body of the accorion div.append($("<div/>", { "class": "accordion-part" }).html(ctx.CurrentItem.Body)); result.append(div); // return the HTML to be rendered (i.e. the accordion) return result.html(); }, |
Post Render
At this point all of my HTML has been inserted into the DOM. This code calls jQuery-UI’s accordion method on each accordion div. It also hooks up event handlers for the expand and collapse links.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * Invoke jQuery-UI's accorion on each item, and wire up the expand/collapse all link events. * @param {object} the rendering context. */ postRender: function(ctx) { // invoke jQuery-UI's accorion on each item $(".accordion").accordion({ heightStyle: "content", collapsible: true, active: false // change to true to start all accordions expanded }); //$(".accordion").first().accordion({ active: 0 }); // uncomment to start first accordion expanded // add a click handler to the expand link $(".expand").click(function() { // expand each item $(".accordion").accordion({ active: 0 }); }); // add a click handler to the collapse link $(".collapse").click(function() { // collapse each item $(".accordion").accordion({ active: false }); }); } |
Deploying the Custom List View Template
And now for the fun part. We’ve already done the hard part by getting our dependencies loaded. To get our display template available as a view type, all we have to do is drop the JavaScript file some place in the Master Page Gallery. Anyplace will do, but as suggested by Martin Hatch, I usually create a folder called List Templates in the Display Templates folder already present, and put my custom list views there. When you upload your code, you will get prompted to set properties on it with the following dialog:
Fig 4: Specify the file properties for the display template
The important properties are:
- Content Type: must be set to “JavaScript Display Template”
- Title: this is how your template will appear on the select view type page when creating a new view
- Target Control Type: must be set to View
- Standalone: must be set to Standalone
- Target Scope: honestly, I always set this to *, this can be a web site URL to limit where your view type is available
- Target List Template ID: 104 is the list template ID for Announcements
Not bad; just drop in the file and you have a new view type.
Configuring a List to Use the Accordion Custom List View
You use the view type just like any other view type. Go to any Announcements list and click on create view. The first page asks you to select a view type, and under custom view types you’ll see “Announcements Accordion View” (the title we entered in the properties), like so:
On the next page, you create the view as normal, except you must be sure to select the Title and Body columns to be displayed. My view template specifically displays these columns, but if you don’t select them here, they won’t be available to the display template to use. Keep in mind that any other columns selected here will not be displayed, as the template is currently coded. Also, make sure you scroll down to item limit and set the value high enough to display all of the questions. As currently coded, this template does not support pagination.
Ideally, you would like to be able to configure which columns get displayed. But at a bare minimum, I need to know which field to use for the question, and there isn’t any way to set any kind of custom configuration properties on a view.
With a little more work, I could just hard code Title as the question, and then display whatever other columns you specify in the view as the body, in the order you selected them. But I didn’t need that extra flexibility for this project, so I didn’t code it. But something is going to be hard coded if you want to treat one or more fields differently, because there’s no way to configure it.
Also remember that if you create a CSR template for the Body field, that overrides View, it will be used to render the body on every other view except this one. Because I’m not passing through rendering of fields, I’m displaying body myself how I choose to display it. Again, with a little more work, I could figure out how to pass through rendering of fields if I need that.
And finally, pagination doesn’t work because I’m overriding Footer, but didn’t render the HTML to support pagination. It’s not shown in Andrei Markeev’s visualization, because his list happens to contain a small enough number of items that no pagination is necessary, but Footer does render the pagination if needed. Once more, a little extra work and I could add this. But FAQs are usually pretty short, so I don’t figure I need it.
Wrap
View display templates are a pretty nice and easy way to do some simple SharePoint customization. The limitations of this implementation are that it doesn’t support pagination, and it doesn’t support group by. I haven’t seen a good implementation of group by in custom list view display templates, but the jQuery-UI accordion could be useful for that too. I’d also like to show pagination. Eventually, maybe I’ll take a crack at those too 😉
References
- JSLink and Display Templates Part 5 – Creating custom List Views – Martin Hatch
- SharePoint 2013 Client Side Rendering: Custom List View – Andrei Markeev
- AccordionViewCSR.zip – the complete source code.
Thanks for this great article. I have a couple of questions .. If I display the new list view on a page with just the list, this works terrific. However, what I needed to be able to accomplish is to have a landing page with different list and the same list with different views. My hope was that when I added the Web part to the landing page and specified the view created using the template, that only that web part would use the accordion .. however, all my lists converted .. do you know a way around this issue?
I haven’t tried it, but I would have expected the same behavior you were hoping for. Maybe keep a list of lists/views to which you want it applied and call the OOB rendering methods for anything else. A pretty lousy solution since you’d lose much of the dynamic nature of the list view template overrides.
I’ll have to play around with it and see what I can come with. I’m pretty tired up this coming week, but I’ll let you know if I figure anything out.
Sadly, I can’t find a way to get around this. Once the JavaScript is loaded, it gets applied to all views. Things that limit it like target scope only prevent the JavaScript from getting loaded. Once it’s loaded, it will get applied to all views of any lists that match the target list template id, even if that list doesn’t have any views that use the accordion template.