- I implemented my OnPreRender to only get called back for a single field because on forms that’s usually what I want. But on views, OnPreRender is only called once, on the field LinkTitle, and that’s not a field I want to override. So I added OnPreRenderAll to my return from getClientTemplates to indicate I want to be called on every call to OnPreRender regardless of context, just like the OOB implementation. Because I do need to do something on pre-render in Views.
- The overrides building code creates a new empty IntelliPoint.clientTemplatesConfig object if it does not exist (i.e. the configuration file hasn’t been written yet), and if an implementation configuration is undefined it creates an empty array of field overrides.
With these changes, no changes were required in SetCSRConfig.aspx in order to accommodate the new template implementation.
Here is the code for the dynamic CSR template for the rating fields. The implementation’s for the three callbacks which will be called by SPClientTemplates have been removed and will be shown and described separately below.
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 | /** * Instance encapsulating the rating CSR implementation. */ var grade = { ////////////////////////////////////////////////////////////////////// // BEGIN INTERFACE FOR SetCSRConfig.aspx ////////////////////////////////////////////////////////////////////// name: "rating", displayName: "Rating", canEdit: false, types: ["Number"], jsLink: [ "~sitecollection/Style Library/jquery.js", "~sitecollection/Style Library/intellipoint_clienttemplatesconfig.js", "~sitecollection/Style Library/intellipoint_clienttemplates.js", ], /** * Construct a new configuration node and call the callback with the new * configuration node. * @param string - the internal name of the field. * @param config - the currently saved configuraiton for the field, which * is null if the field is new. * @param callback - the callback function to which to pass the configuration * node when complete. * The configuration node (in and out) is just one object from our fields * array, so it should look like: * { * internalName: <fieldInternalName> * } */ configure: function(internalName, config, callback) { callback({ internalName: internalName, }); }, /** * Get an array of objects describing the callbacks for a field. * @fields - the field configuration for this CSR implementation. * @return [object] where the objects look like: * { * internalName: <name of field>, * OnPreRender: <callback>, * OnPostRender: <callback>, * NewForm: <callback>, * EditForm: <callback>, * DisplayForm: <callback>, * View: <callback> * } * Any defined callbacks will be included in the overrides object passed * to RegisterTemplateOverrides. Unlike normal CSR, OnPreRender and OnPostRender * will only be called for the field they're configured on, not all fields * on the form (this is usually what I want, and it normalizes things for * undocumented changes in the way these callbacks are handled on office 365). */ getClientTemplates: function(fields) { var result = []; $.each(fields, function(i, v) { result.push({ internalName: v.internalName, OnPreRenderAll: grade.preRender, NewForm: grade.edit, EditForm: grade.edit, DisplayForm: grade.display, View: grade.display, }); }); return result; }, ////////////////////////////////////////////////////////////////////// // END INTERFACE FOR SetCSRConfig.aspx ////////////////////////////////////////////////////////////////////// preRender: function(ctx) { // TODO }, edit: function(ctx) { // TODO }, display: function(ctx) { // TODO } }; // attaching to client templates exposes this implementation to the utility page and // the overrides construction IntelliPoint.clientTemplates[grade.name] = grade; |
If you’ve followed along with the series, this should look fairly familiar, so I’m just going to mention a couple things that are different from previous posts.
- First, the implementation of the configure method is trivial, it just creates a node and calls the callback, because there is no implementation-specific configuration for this template.
- For the same reason, canEdit is set to false, which means the edit button next to previously configured fields with this template will be disabled.
- As previously mentioned, getClientTemplates overrides the NewForm, EditForm, DisplayForm, View, and on pre render methods, which is everything that can be overridden except on post render. The edit method will override both NewForm and EditForm, and the display method will override both the DisplayForm and View.
- The on pre render handler uses the newly created OnPreRenderAll so it will get called back every time SPClientTemplates calls OnPreRender regardless of context. This means it will get called once for each field on forms, and once for LinkTitle on views. If it didn’t use OnPreRenderAll, it wouldn’t get called on views at all.
Below is the SetCSRConfig.aspx page with our new template added to the template implementations. Note the pencil icon is disabled because there is nothing to configure:
Here is the pre-render callback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | addedCss: false, preRender: function(ctx) { if (!grade.addedCss) { grade.addedCss = true; // prepend css to the body, multi-line string trick - Andrey Markeev $("body").prepend((function() {/*<style type='text/css'> .green { color: green !important; font-weight: bold; } .yellow { color: gold !important; font-weight: bold; } .red { color: red !important; font-weight: bold; } </style>*/}).toString().slice(14, -3)); } }, |
This method just prepends some CSS onto the body of the HTML document. A lot of samples, like those from Office365 Patterns and Practices, insert a link into the head and load their CSS from an external file, which technically is a best practice but it leads to a very jumpy form. The HTML loads and then sometime later the browser gets the styles and everything changes. By embedding the CSS in my JavaScript and then shoving it into the page, my styles are there before the HTML and it’s a much more pleasing experience for end users. And with the help of a trick from Andrey Markeev I embed the CSS as a multi-line comment, which makes it easy to do the CSS in a separate file with syntax highlighting and IntelliSense, and just copy it here when I’m done. If I have a build process like Node/Webpack, I could even automate keeping these in sync.
And here is the NewForm and EditForm override for rating fields:
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 | edit: function(ctx) { var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx); var currentValue = formCtx.fieldValue; // Construct an html text input var result = $("<input/>"). attr("id", "input_" + formCtx.fieldName). attr("type", "text"). attr("value", currentValue); // create a span for validation messages var validation = $("<div/>"). attr("id", "validation_" + formCtx.fieldName + "_rating"). addClass("ms-formvalidation"); // color the input as needed if (currentValue) { result.addClass(grade.value2Class(currentValue)); } // add deferred event handler to change the input color as needed $("#s4-bodyContainer").on("change", "input[id='input_" + formCtx.fieldName + "']", function(e) { var input = $(this); input.removeClass(); input.addClass(grade.value2Class(input.val())); }); // Register a callback to retrieve the value, called just before submit formCtx.registerGetValueCallback(formCtx.fieldName, function() { return $("#input_" + formCtx.fieldName).val(); }); // Register all validation callbacks grade.registerValidation(formCtx); // Render the input and validation div, there's no outerHTML in jQuery, so tack the // input onto a temp span and return it's inner html return $("<span/>").append(result).append(validation).html(); }, value2Class: function(value) { var result; switch (value) { case "10": case "9": case "8": result = "green"; break; case "7": case "6": result = "yellow"; break; default: result = "red"; } return result; }, |
- First it constructs a couple of jQuery objects whose HTML will be returned from this function to be rendered later by SPClientTemplates.
- Then it initializes the input with a class obtained by the helper value2Class also shown above, which will be used by the display override as well.
- It then creates a deferred event handler to handle onchange events and update the input class based on the new value. Deferred event handlers are a nice way to handle these kind of things, because you can’t just attach an event handler directly since the controls aren’t in the DOM yet. The alternative is to attach an event handler later using either OnPostRender or registerInitCallback, but by doing it inline here, it creates a closure so the context that was passed into edit is available to the handler too. The context is somewhat different in OnPostRender.
- Since I’m doing the rendering, the form has no way of knowing how to get a value for my controls on submit, so I have to register a get value callback to do that.
- Then it calls registerValidation, which is not shown here. It is complicated enough to deserve it’s own section below.
- And finally, it returns the HTML that I want SPClientTemplates to render for this field.
Now here is the callback for DisplayForm and View for rating fields:
1 2 3 4 5 6 7 8 9 10 11 | display: function(ctx) { var currentValue = ctx.CurrentItem[ctx.CurrentFieldSchema.Name]; // get the class to use based on the current value var color = grade.value2Class(currentValue); // build the html var result = $("<span/>").append($("<span/>").addClass(color).text(currentValue)); return result.html(); }, |
Not much to this one. It just returns some HTML to view the field value, using the same value2Class method used in edit to determine what color the field should be. Keep in mind that the context is different at different points of the life cycle and also in different forms. The edit callback used formCtx.fieldValue to get the current value, but here I use the somewhat more complex syntax ctx.CurrentItem[ctx.CurrentFieldSchema.Name]. The reason is that formCtx.fieldValue will work fine for DisplayForm, but it is null on Views; ctx.CurrentItem is available on both forms and views.
And last, but certainly not least, is the registerValidation implementation for rating fields:
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 | registerValidation: function(current) { // register a callback method for the validators current.registerValidationErrorCallback(current.fieldName, function(error) { if(error.validationError) { $('#validation_' + current.fieldName + '_rating').attr('role', 'alert').html(error.errorMessage); } else { // added 7/23 to clear validation error messages when fixed $('#validation_' + current.fieldName + '_rating').attr('role', '').html(''); } }); // create a validator set var fieldValidators = new SPClientForms.ClientValidation.ValidatorSet(); // create a custom validator as an object literal fieldValidators.RegisterValidator({ Validate: function(value) { var isError = false; var errorMessage = ''; if (!/^[0-9]*$/.test(value)) { isError = true; errorMessage = "Value must contain only digits."; } else { var num = parseInt(value); if (num < 1 || num > 10) { isError = true; errorMessage = "Value must be between 1 and 10 inclusive."; } } return new SPClientForms.ClientValidation.ValidationResult(isError, errorMessage); } }); // if required, add a required field validator if (current.fieldSchema.Required) { fieldValidators.RegisterValidator(new SPClientForms.ClientValidation.RequiredValidator()); } // register the validators current.registerClientValidator(current.fieldName, fieldValidators); }, |
- If you do the rendering, you almost certainly need to call registerValidationErrorCallback. SharePoint doesn’t know what your field looks like, so it doesn’t know where to put error messages. If you don’t do this, validation errors will prevent the form from being saved, but no error message will be displayed explaining why.
- Ultimately, we’re going to call registerClientValidator on the form context, and it takes in an instance of SPClientForms.ClientValidation.ValidatorSet, so we need to create one of them.
- Now we’ll create a custom validator to ensure the input is a number between 1 and 10 inclusive. A validator is just an object instance with a Validate method, which takes as it’s only argument the current value as a string. By creating this as an object literal, we create a closure so the form context is available to the validate method, which can be useful although we don’t need it here.
- There’s no such thing as taking partial responsibility for validation, it’s all or nothing. Which means if we don’t validate required, the field is not required, even if it is configured to be required through the list settings. Also, don’t just add a RequiredValidator all the time. If it’s there, the form will treat it as required even if it’s not configured that way through the list settings. So add a required validator only if the field is configured to be required.
- Now that all of our validators are attached to the validator set, call registerClientValidator passing it our validator set.
I’ve now covered all of the override callbacks from my rating field overview. I’ve also called the form context registered callbacks except registerFocusCallback and registerHasValueChangedCallback. And to be honest, I’ve never used either of these and have never seen any bad side effects caused by not using them. I guess someday I’ll have to go back to CSRSpy to try and reverse engineer what these are for, because I’m not holding my breath waiting for documentation. Anyway, if I want to be notified for focus events, I usually just attach my own deferred event handler. And while registerHasValueChangedCallback seems like it might be useful, I haven’t seen any documentation explaining when it gets called.
This pretty well wraps up what I wanted to cover regarding using Client-side Rendering to modify SharePoint forms. If I write any more posts about form templates, I’ll probably just describe what the thing does and attach the source with no detailed explanation of the code. Armed with the knowledge in these 6 posts, and the various posts I’ve referenced, you can now do pretty much anything you want with CSR form modifications, limited only by your knowledge and skill with HTML, CSS, and JavaScript. At some point I’ll write some more posts about View display templates, search display templates, and combining CSR with setting JSLink on content types.
Reference
#javascript multiline trick, Andrey Markeev
RatingCSR.zip – the complete source code.