As in, write the template once, drop it on the site, and then allow administrators to apply it to appropriate fields based on SPFieldType, in the browser, without ever having to edit a JavaScript file. So when I first started learning CSR, I was looking for it to be a replacement for custom field types from SharePoint 2010. But it isn’t really that. If I created an Autocomplete custom field type and deployed it, an administrator could then go through the browser and create as many instances of that field type as they wanted. But with CSR, the fields to which it applies are hard-coded in the JavaScript, which means if an administrator wants to apply a template to another field, she has to open the JavaScript and modify it. That’s not really comparable to custom fields. It’s just not as accessible for non-developer administrators. In this post, I’m going to show a utility page which implements what I’m calling Dynamic CSR, whereby an administrator can apply a template to a site column through the browser without even having to know that it’s implemented in a JavaScript file.
First I’m going to just show what the finished product looks like and how to use it. Here is the utility page:
When an administrator comes to this page, they can select a CSR type and add it to a field (i.e. site column). Now you can’t really just add any CSR template to any field. It’s Dynamic CSR, but not that Dynamic. For instance, my AutocompleteCSR implementation from the last post isn’t going to work particularly well if applied to a Person or Group field or a Managed Metadata field. So when you select the CSR type, the field drop down is limited to only site columns that work with the selected CSR Type (in the case of autocomplete, that would be single line of text fields), like so:
Now select a field and hit the ‘Add Client Template’ button, and you will get prompted with a dialog box to enter any implementation specific configuration:
Enter that information and hit Ok and the new field will be added to the Dynamic CSR implementation table below, complete with edit and delete buttons:
When you hit the save button, a couple of things happen. First, the code loops through each configured field and sets its JSLink to the appropriate JavaScript references for that implementation. Second, the configuration is written back to a file on the server, from which it is used by each of the Dynamic CSR implementations, and also by the utility page in order for it to show you all previously configured fields when you come back to it.
If you now go to a list that uses that field, the autocomplete column should work as expected, even if that list is on a sub-site. Of course, if the current user does not have permission to read from the list with the autocomplete data (or to read the JavaScript files themselves), then it won’t work. And if the CSR implementation has other dependencies that aren’t met, it won’t work. For instance, the cascading lookup implementation depends on a parent column and a relationship list. If either of these is missing, it will do nothing.
The rest of this post is going to explain parts of this solution, starting with how the CSR implementations from the previous posts had to be refactored to make this work, and then showing very briefly how to generically work with these implementations in order to do stuff like register template overrides or show all currently configured fields. The entire source for the utility page is too large to either show or explain in a blog post (but the complete source code is attached).
The Dynamic CSR Configuration
The CSR implementations I’ve shown previously just had their configuration as an array of fields (objects) at the top of the file, but that’s not going to work so well anymore. Instead, all of the configuration for each of the implementations will be combined into a single file, which will get loaded by JSLink before the implementation is loaded. The individual configurations for each implementation are exactly the same as before, an array of fields (objects), but now the outer configuration is a map of CSR implementation names to arrays of fields. Here is the complete configuration file:
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 | window.IntelliPoint = window.IntelliPoint || {}; IntelliPoint.clientTemplatesConfig = { "autocomplete": [ { "internalName": "Company", "webURL": "/", "lookupList": "Companies", "lookupField": "Title" }, { "internalName": "JobTitle", "webURL": "/", "lookupList": "JobTitles", "lookupField": "Title" } ], "cascadingDropdown": [ { "internalName": "SalesDivision", "parentColumnInternal": "SalesRegion", "childColumnInternal": "SalesDivision", "relationshipList": "SalesDivision", "relationshipListParentColumn": "SalesRegion", "relationshipListChildColumn": "Title", "relationshipWebURL": "/" }, { "internalName": "SalesState", "parentColumnInternal": "SalesState", "childColumnInternal": "SalesDivision", "relationshipList": "SalesState", "relationshipListParentColumn": "SalesDivision", "relationshipListChildColumn": "Title", "relationshipWebURL": "/" } ] } |
This configuration is processed by the CSR implementations on forms to dynamically construct the overrides to be passed into SPClientTemplates.RegisterTemplateOverrides. It is also processed by the utility page on load to show everything that has already been configured, and overwritten by the utility page with the new configuration on each save.
The Dynamic CSR Implementations
The actual render, or pre-render, or post-render, etc. code in the Autocomplete and Cascading Lookup CSR implementations is much the same as it was before so I’m not going to show that again. But in order for the utility page and the code that dynamically builds the overrides object to be able to work with them in a generic fashion, these implementations did need to be genericised somewhat, as follows:
- All Dynamic CSR implementations need to be in a single JavaScript source file. This is because the code that rolls up all of the implementations is at the bottom of this file, and it needs all of the implementations to be defined before it looks through them and constructs the template overrides.
- Each of the implementations should be encapsulated into a single literal object instance. The cascading lookup implementation was already written this way, but the autocomplete was not.
- Each of the implementations needs to implement a common interface. This is so the utility page, for instance, can ask an implementation what kind of SPField types it can work with, or tell an implementation to popup it’s implementation-specific configuration dialog box, without having to know anything implementation specific.
- All of these implementations need to be attached to a map of implementation names to implementation instances, so the utility page or overrides building code can easily look through them. The name of the implementation, the key in this map, and the key in the configuration all must be the same.
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 | // Instance encapsulating the autocomplete CSR implementation. var ac = { ////////////////////////////////////////////////////////////////////// // BEGIN INTERFACE FOR SetCSRConfig.aspx ////////////////////////////////////////////////////////////////////// name: "autocomplete", displayName: "Autocomplete", canEdit: true, types: ["Text"], jsLink: [ "~sitecollection/Style Library/jquery.js", "~sitecollection/Style Library/jquery-ui.js", "~sitecollection/Style Library/jquery.spservices.js", "~sitecollection/Style Library/intellipoint_clienttemplatesconfig.js", "~sitecollection/Style Library/intellipoint_clienttemplates.js", ], configure: function(internalName, config, callback) { // create the dialog ui if ($("#autocompleteDialog").length === 0) { var modal = $("<div/>").attr("id", "autocompleteDialog"); modal.append($("<span/>").addClass("label").text("Lookup List Title:")); modal.append($("<span/>").addClass("input").append( $("<input/>").attr("id", "lookupListTitle"))); modal.append("<br/>"); // ... a couple more inputs go here $("#dialogs").append(modal); } // initialize the dialog inputs, config is null on new if (config) { if (config.webURL) { $("#lookupWebUrlInput").val(config.webURL); } else { $("#lookupWebUrlInput").val(_spPageContextInfo.siteServerRelativeUrl); } $("#lookupListTitle").val(config.lookupList); $("#lookupFieldName").val(config.lookupField); } else { $("#lookupWebUrlInput").val(_spPageContextInfo.siteServerRelativeUrl); $("#lookupListTitle").val(""); $("#lookupFieldName").val(""); } // launch the dialog $("#autocompleteDialog").dialog({ title: "Add/Edit Autocomplete Field", modal: true, width: 600, buttons: [{ text: "Ok", click: function() { callback({ internalName: internalName, webURL: $("#lookupWebUrlInput").val(), lookupList: $("#lookupListTitle").val(), lookupField: $("#lookupFieldName").val(), }); $(this).dialog("close"); } }, { text: "Cancel", click: function() { $(this).dialog("close"); } } ], }); }, getClientTemplates: function(fields) { var result = []; $.each(fields, function(i, v) { result.push({ internalName: v.internalName, OnPreRender: ac.preRender, OnPostRender: ac.postRender, }); }); return result; }, ////////////////////////////////////////////////////////////////////// // END INTERFACE FOR SetCSRConfig.aspx ////////////////////////////////////////////////////////////////////// preRender: function(ctx) { }, }; // attaching to client templates exposes this implementation to the utility page and // the overrides construction IntelliPoint.clientTemplates[ac.name] = ac; |
First, there are some properties with implementation name, display name, the field types this implementation works with, and the JSLink files to load for this field implementation. The meat is in two methods, configure and getClientTemplates. Configure just pops up a dialog to get implementation-specific configuration, and then calls a callback with the newly configured node when done. getClientTemplates just returns an array of objects, one for each field, with the callbacks I want to override for that field. The pre-render and post-render methods haven’t changed at all, except the configuration is nested slightly deeper, so I’m not showing them again.
How to Work With Implementations Without Knowing Their Details
The first thing we need to do is register our template overrides with SPClientTemplates. To do that, we need to loop through our implementations and ask them what they want to override. That would look like this:
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 | // create an overrides object var overrides = { Templates: { Fields: {} } }; // Loop through each implementation and based on the configuration construct // the Template.Fields. var keys = Object.keys(IntelliPoint.clientTemplates); $.each(keys, function(i, template) { // get the CSR implementation var templateImpl = IntelliPoint.clientTemplates[template]; if (templateImpl) { // get the configuration for this implementation var templateConfig = IntelliPoint.clientTemplatesConfig[template]; if (templateConfig) { // call the implementation and ask what it wants to override? var implOverrides = templateImpl.getClientTemplates(templateConfig); // for each override $.each(implOverrides, function(j, o) { if (o.View || o.DisplayForm || o.NewForm || o.EditForm) { // create an Template.Fields override object to override the new, // edit, display and/or view rendering for the field var newOverrides = {}; if (o.View) newOverrides.View = o.View; if (o.NewForm) newOverrides.NewForm = o.NewForm; if (o.EditForm) newOverrides.EditForm = o.EditForm; if (o.View) newOverrides.DisplayForm = o.DisplayForm; overrides.Templates.Fields[o.internalName] = newOverrides; } }); } } }); // Register the overrides with SPClientTemplates SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides) |
Similarly, the utility page draws it’s interface by looping through each implementation like so:
1 2 3 4 5 6 7 8 9 10 11 12 | // for each template implementation, add an option to the csrType select, // and display the current configuration $.each(Object.keys(IntelliPoint.clientTemplates).sort(), function(i, templateName) { var templateImpl = IntelliPoint.clientTemplates[templateName]; // add an option for the impl to the csr type dropdown $("#csrType").append($( "<option/>", { "value": templateName }).text(templateImpl.displayName)); // draw a table for each implementation showing it's configured fields templateConfigSetter.drawTemplateTable(templateImpl); }); |
And when the user hits the add CSR button and the utility page needs to popup the implementation-specific configuration dialog, that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // get the CSR implementation by name, using the name in the #csrType drop down list var templateImpl = IntelliPoint.clientTemplates[$("#csrType").val()]; // call the implementation's configure method, which will construct the configuration node // for the field (who's name is obtained from the #field drop down list). The implementation decides how // to construct the configuration, but usually it's hard-coded if simple and if it requires user input // it will popup a dialog to collect it. When the implementation is done, it calls back the function // passed into it as argument 3, passing it the configuration node. That callback then adds the node to // the appropriate place in IntelliPoint.clientTemplatesConfig (described above). templateImpl.configure($("#field").val(), null, function(node) { // When I get called back, the node is fully configured, and I // should update the configuration with it. }); |
And that’s really all I’m going to say about the code, because really, who’s still reading this TL;DR post anyway? And if you know HTML and JavaScript, the rest of it is more of the same anyway. A lot more of the same, but still just more of the same. In my next post I’m going to show building a more complete CSR display template, with rendering on new, edit, display, and view, plus validation and more, and plugging it into this architecture.
SetCSRConfig.zip – the complete source code.