I have two goals in this post. First I want to show using CSR in SharePoint to do something cool (or at least useful), cascading lookups. Second I’d like to show a utility page that allows you to configure JSLink in a much better way than setting the JSLink property on a web part using the browser.
For the first goal, I’m going to create cascading lookup lists in a SharePoint form. I chose cascading lookups for a number of reasons:
- It’s a form customization that people frequently ask how to implement on forums like stack exchange (probably the single most common request).
- There is a nice implementation built into the jquery.SPServices library by Marc Anderson, which I’m going to use.
- It doesn’t require any custom rendering. Everything it does occurs after rendering. But it does have to override the render method, so it will need to pass through the rendering to the out of box client templates using the same technique as CSRSpy from my last post.
Of course, using SPServices also means that I have a dependency on jQuery, since it has a dependency on jQuery. And SPServices requires lookup lists to be setup in a certain way, so I’ll get that setup first.
Setting Up for SPServices Cascading Lookups
In order to use SPServices for cascading lookups, you need to have at least two lookup fields in your list to two different lists. The first list is just an ordinary lookup list, which only needs a single field to use for the lookup. The second list has to be what Marc Anderson calls a relationship list, meaning it has a field to use for the lookup and a lookup field to the parent list.
So in my case, I have the following two list, SalesRegion which is the parent list and SalesDivision which is the relationship list, and they look like this:
Now I actually want two levels of cascading lookups in my list, so I also have a third list setup, which is SalesState and is a relationship list with a lookup to SalesDivision. And of course I need three columns added to my list that are lookup columns to these three lists. Once that’s done, I’m ready to use SPServices cascading lookups.
What Are Cascading Lookups?
Before going into the implementation, let’s briefly talk about what we’re trying to achieve. Here is the new form for my list:
The list has three lookup fields, Sales Region, Sales Division, and Sales State, each of which is a site column. And of course, the lookups point to the three lists I created above. Theses lookup fields are hierarchical, meaning until I select a region, there are no values to select in division. Once I select a region, only divisions in that region are available as choices in the division field.
And of course, once I choose a division, only states that make sense for that division are available as choices in the state field.
That’s all there is to cascading lookups.
The Cascading Lookups CSR Implementation
Below is the code for the implementation. I’ve highlighted several blocks which I’ll explain in the sub-sections to follow.
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | (function($) { // this structure is the only thing that needs to be modified to override more or different fields var fields = [{ "internalName": "SalesDivision", "parentColumnInternal": "SalesRegion", "childColumnInternal": "SalesDivision", "relationshipList": "SalesDivision", "relationshipListParentColumn": "SalesRegion", "relationshipListChildColumn": "Title", "debug": true }, { "internalName": "SalesState", "parentColumnInternal": "SalesDivision", "childColumnInternal": "SalesState", "relationshipList": "SalesState", "relationshipListParentColumn": "SalesDivision", "relationshipListChildColumn": "Title", "debug": true } ]; // helper because IE doesn't have Array.find function arrayFind(array, value, equals) { for (var i = 0; i < array.length; i++) { if (equals(value, array[i])) return array[i]; } } // class instance that encapsulates CSR functionality for cascading lookups. var cascadDropdownCSR = { // this will be the render method we pass in overrides render: function(ctx) { // get the form context so we can register some callbacks var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx); // get the OOB default rendering var html = cascadDropdownCSR.getDefaultRendering(ctx); // register an init callback for our field formCtx.registerInitCallback(formCtx.fieldName, function() { // find the current config element var config = arrayFind(fields, formCtx.fieldName, function(v, o) { return v === o.internalName; }); // get the list schema for all fields var schema = window[ctx.FormUniqueId + "FormCtx"].ListSchema; // get the display names SPServices wants from the internal names // in our configuration if(config.parentColumnInternal) { config.parentColumn = schema[config.parentColumnInternal].Title; } if(config.childColumnInternal) { config.childColumn = schema[config.childColumnInternal].Title; } // if relationshipWebURL is not specified, assume the root web if(!config.relationshipWebURL) { config.relationshipWebURL = _spPageContextInfo.siteServerRelativeUrl; } // apply SPCascadeDropdowns to our configuration $().SPServices.SPCascadeDropdowns(config); }); // need to do get value callback because lookup updates it's value in an onchange // handler declared in init callback, which we've overridden formCtx.registerGetValueCallback(formCtx.fieldName, function() { var val = $("select[id^='" + formCtx.fieldName + "_']").val(); var text = $("select[id^='" + formCtx.fieldName + "_'] option:selected").text(); return (val == '0' || val == '' ? '' : val + ";#" + text); }); // return the OOB default rendering return html; }, getDefaultRendering: function(ctx) { var templatesByType = SPClientTemplates._defaultTemplates.Fields.default.all.all; var currentTemplates = templatesByType[ctx.CurrentFieldSchema.Type]; var currentRenderFunc = currentTemplates[ctx.BaseViewID]; return currentRenderFunc(ctx); } }; /* * Create an empty overrides object. */ var overrides = { Templates: { 'Fields': {} } }; /* * Add an overrides object for each field we want to customize. */ for (var i = 0; i < fields.length; i++) { var current = fields[i]; overrides.Templates.Fields[current.internalName] = { 'NewForm': cascadDropdownCSR.render, 'EditForm': cascadDropdownCSR.render, }; } // also register templates now for non-MDS and full page loads SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides); // register template overrides for partial page loads if MDS is enabled if (typeof _spPageContextInfo != 'undefined' && _spPageContextInfo != null) { var base = _spPageContextInfo.siteServerRelativeUrl.toLowerCase(); var url = (base === '/' ? '' : base) + '/style%20library/cascadingdropdownscsr.js'; RegisterModuleInit(url.toLowerCase(), function() { SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides); }); } })(jQuery); |
The Configuration
As a developer, I tend to write JavaScript such that I can drop it into a new site connecting to a new list and working with new fields etc., and the only changes to the JavaScript required are some configuration changes at the top of the file. That’s what the fields array is here. Also, there are some things I’m going to standardize right now (as in, use in all future CSR implementations):
- Each CSR will have its fields configured as an array of objects.
- Each object will have an internalName property containing the internal name of the field to which the CSR should be applied.
- The object can contain any number of other properties that are useful to the CSR implementation itself.
This standardization will pay big dividends down the road (as in several posts from now, I’m building towards something here).
The Render Method
Most of the heavy lifting is done in the render method, which seems strange since we’re not even doing any rendering, we’re passing that through to the default renderer, but this is also where you have to register for form callback events. The steps this method performs are:
- First, it gets the form context from the rendering context that was passed in. We’ll need the form context to registered for form event callbacks.
- Then, it calls the clienttemplates.js renderer and stores the result in a variable to return it later. Now in my first crack at this, I didn’t store it, I just ended the method with “return cascadDropdownCSR.getDefaultRendering(ctx);”, and nothing worked (as in my form callbacks never got called back). After stepping through the default renderer I realized it was stepping on my init callback by registering its own. Which brings home the point that you sometimes need to know what the OOB client templates are doing.
- The next step is that it registers an init callback, and this is where the work gets done. An init callback is called once, after the entire form is rendered, and there can only be one for a given field. This differs from an OnPostRender, which gets called once for each field in the form after that field is rendered. Since we’re doing something with two fields here, doing our work after the entire form is rendered simplifies things quite a bit, which is why registering an init callback is better for our purposes.
Note that most of the things SPServices needs passed to it in order to do cascading lookups are in my configuration object (and in the same format, so I can just pass this object as the options). The only things missing are the display names of the fields in this list. I don’t like storing display names as configuration, because some admin changes them and then comes to me and says your form stopped working. And since it’s worked for 2 years and I’ve moved on, it takes me quite a while of wading through my old code to realize how the admin hosed me up. So I store the internal names and use them to look up the display names that SPServices needs from the context. - Then I set up a get value callback. Normally, you only need to do this if you’re also doing the rendering, so why do I need to do this. For starters, because if I don’t do this, the value of my child lookup doesn’t get saved on form submit. That’s the symptom, but the reason is that the OOB render method sets up an event handler to provide this value, so since we’ve overridden the init handler, the onus of providing this functionality is now on us.
- Finally, we returned the HTML to be rendered.
Easy peasy lemon squeezy, right?
Dynamically Configuring the Overrides
The next highlighted section of code just loops through the fields and dynamically creates the Template.Fields property of the overrides object from it, before calling RegisterTemplateOverrides.
Dealing with Minimal Download Strategy
The final highlighted block of code is just some magic to handle partial page loads on sites where Minimal Download Strategy (MDS) has been enabled. You can read more about that on Wictor Wilén’s blog, which I’ll include in references at the end of this post.
Setting the JSLink Property on a Site Column
I’ve included a utility page that will allow you to set the JSLink property of a site column. At the moment I’m just going to show how to use it to get this CSR deployed. I’ll explain it’s inner workings in the next post, mostly because I don’t want this post to be much longer than this.
I mean seriously, so far my average post is at least 10 pages printed. I could have just named my blog TL;DR, and I almost did. I don’t like to do introductory stuff, but I don’t mind explaining it for possibly less experienced developers (or at least less experienced with my current subject matter), and that leads to long posts. If that’s not your thing, move along, they’re probably not going to get any shorter.
Anyway, just drop the attached source into your style library and click on SetJSLinkOnField.aspx, and you should see a form like:
Enter a field group, pick a field, enter site collection relative URLs of the JavaScript files (starting with ~sitecollection) one per line, and hit the set JSLink button and you should now be able to test that any list that has at least two of the consecutive lookups (i.e. region and division and/or division and state) functions correctly as cascading lookups.
Note that if you want to add SalesState, you can technically add it without setting the JSLink on it, because the script only needs to be loaded once. But JSLink is smart enough to only load it once even if the same script is set on several fields in a form. So you should just set it individually on each field you’re going to need it for. Otherwise, some admin is probably going to remove a field and all the sudden 10 other fields don’t work right, at which point they’ll come to you and say your form stopped working ;).
References
- SPCascadeDropdowns, Marc Anderson
- Andrei Markeev on Code Plex
- The correct way to execute JavaScript functions in SharePoint 2013 MDS enabled sites, Wictor Wilén
CascadingDropdownsCSR.zip – the source code for this post.