Second Life of a Hungarian SharePoint Geek

August 29, 2016

Permission-based Rendering Templates, Part 2: The Synchronous Solution

Filed under: CSR, JavaScript, jQuery, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:14

In my recent post I’ve illustrated how can you implement a permission-based custom rendering template using the JavaScript client object model (JSCOM)  and jQuery. That rendering template was implemented using the standard asynchronous JavaScript patterns via a callback method to not block the UI thread of the browser. In a fast network (in a LAN, for example) however, a synchronous implementation can function as well. Although there are some unsupported methods to make a JSCOM request synchronously, the JavaScript client object model was designed for asynchronous usage (see its executeQueryAsync method). To send our requests synchronously, we utilize the REST / OData interface in this post, and send the requests via the ajax function of jQuery.

To understand the original requirements and the configuration (field and list names, etc.), I suggest to read the first part first.

To enable using of jQuery selectors containing the dollar sign ($), we use the same escapeForJQuery helper function that we’ve created for the first part.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. var adminGroup = "MyGroup";
  9.  
  10. custom.escapeForJQuery = function (value) {
  11.     var newValue = value.replace(/\$/g, "\\$");
  12.     return newValue;
  13. }

Instead of simply wrapping the standard display template of choice fields (SPFieldChoice_Edit), the editFieldMethod function is responsible to get the HTML content of the field control, as it would be rendered without the customization by invoking the SPFieldChoice_Edit function, then we determine the group membership of the user by calling the synchronous isCurrentUserMemberOfGroup function (more about that a bit later), finally we alter the HTML content by hiding the adequate options by calling the hideOptions function (see it later as well).

  1. custom.editFieldMethod = function (ctx) {
  2.     var fieldSchema = ctx.CurrentFieldSchema;
  3.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  4.     var html = SPFieldChoice_Edit(ctx);
  5.  
  6.     var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  7.     if (isCurrentUserInGroup) {
  8.         html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  9.     }
  10.     else {
  11.         html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  12.     }
  13.  
  14.     return html;
  15. }

The hideOptions function loads the HTML source of the control into the DOM and removes the options that should be hidden for the given group. Finally it returns the HTML source of the altered control:

  1. custom.hideOptions = function (html, ctrlId, restrictedValues) {
  2.     var parsedHtml = $(html);
  3.     restrictedValues.forEach(function (rv) {
  4.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  5.         $(parsedHtml).find(selector).remove();
  6.     });
  7.     var result = $(parsedHtml).html();
  8.  
  9.     return result;
  10. }

The isCurrentUserMemberOfGroup function sends a synchronous REST request via the the ajax function of jQuery to determine the group membership of the current user:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2.  
  3. custom.isCurrentUserMemberOfGroup = function (groupName) {
  4.     var isMember = false;
  5.  
  6.     $.ajax({
  7.         url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  8.         type: "GET",
  9.         async: false,
  10.         contentType: "application/json;odata=verbose",
  11.         headers: {
  12.             "Accept": "application/json;odata=verbose",
  13.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  14.         },
  15.         complete: function (result) {
  16.             var response = JSON.parse(result.responseText);
  17.             if (response.error) {
  18.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  19.             }
  20.             else {
  21.                 var groups = response.d.results;
  22.                 groups.forEach(function (group) {
  23.                     var loginName = group.LoginName;
  24.                     console.log(String.format("Group name: {0}", loginName));
  25.                     if (groupName == loginName) {
  26.                         isMember = true;
  27.                     }
  28.                 });
  29.             }
  30.         }
  31.     });
  32.  
  33.     return isMember;
  34. }

In this case we simply register the editFieldMethod for both the ‘EditForm’ and for the ‘NewForm’ mode of the Status field, there is no need for the OnPostRender method:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     var adminGroup = "MyGroup";
  13.  
  14.     custom.escapeForJQuery = function (value) {
  15.         var newValue = value.replace(/\$/g, "\\$");
  16.         return newValue;
  17.     }
  18.  
  19.     custom.hideOptions = function (html, ctrlId, restrictedValues) {
  20.         var parsedHtml = $(html);
  21.         restrictedValues.forEach(function (rv) {
  22.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  23.             $(parsedHtml).find(selector).remove();
  24.         });
  25.         var result = $(parsedHtml).html();
  26.  
  27.         return result;
  28.     }
  29.  
  30.     custom.editFieldMethod = function (ctx) {
  31.         var fieldSchema = ctx.CurrentFieldSchema;
  32.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  33.         var html = SPFieldChoice_Edit(ctx);
  34.  
  35.         var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  36.         if (isCurrentUserInGroup) {
  37.             html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  38.         }
  39.         else {
  40.             html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  41.         }
  42.  
  43.         return html;
  44.     }
  45.  
  46.     var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  47.  
  48.     custom.isCurrentUserMemberOfGroup = function (groupName) {
  49.         var isMember = false;
  50.  
  51.         $.ajax({
  52.             url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  53.             type: "GET",
  54.             async: false,
  55.             contentType: "application/json;odata=verbose",
  56.             headers: {
  57.                 "Accept": "application/json;odata=verbose",
  58.                 "X-RequestDigest": $("#__REQUESTDIGEST").val()
  59.             },
  60.             complete: function (result) {
  61.                 var response = JSON.parse(result.responseText);
  62.                 if (response.error) {
  63.                     console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  64.                 }
  65.                 else {
  66.                     var groups = response.d.results;
  67.                     groups.forEach(function (group) {
  68.                         var loginName = group.LoginName;
  69.                         console.log(String.format("Group name: {0}", loginName));
  70.                         if (groupName == loginName) {
  71.                             isMember = true;
  72.                         }
  73.                     });
  74.                 }
  75.             }
  76.         });
  77.  
  78.         return isMember;
  79.     }
  80.  
  81.     var customOverrides = {};
  82.     customOverrides.Templates = {};
  83.  
  84.     customOverrides.Templates.Fields = {
  85.         'Status': {
  86.             'EditForm': custom.editFieldMethod,
  87.             'NewForm': custom.editFieldMethod
  88.         }
  89.     };
  90.  
  91.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  92.  
  93. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate2.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate2.js"
$field.Update()

Note, that (in contrast to the script introduced in the first part of this post) there is no need for the JSCOM JavaScript files (sp.runtime.js and sp.js) in this case.

Permission-based Rendering Templates, Part 1: The Asynchronous Solution

Filed under: CSR, JavaScript, jQuery, JSCOM, SP 2013 — Tags: , , , , — Peter Holpar @ 21:49

Recently I read a question on SharePoint StackExchange about how one can restrict the available options in a choice field based on the group membership of the current user. My answer was to create a custom client rendering templates (CSR) and set it via the JSLink property of the choice field. If you are new in the usage of custom rendering templates, you can find several superb introduction on the topic on the web, like this one. At the time of my answer I had no sample code ready to publish (honestly, I was rather surprised that I have not found any such example on the web), but in the meantime I prepared two various implementations for the same problem. In this post I describe the first possible approach, an asynchronous solution based on the JavaScript client object model (JSCOM) and jQuery. The other solution will be discussed in a later post.

Both approaches share the same custom list: it is a list having a standard Title field and choice field called Status that has three state options: ‘Approved’, ‘Rejected’ and ‘Resubmit’. If the current use is member of a specific SharePoint group (let’s say ‘MyGroup’), the options ‘Approved’ and ‘Rejected’ should be displayed in the editable mode (that means on ‘EditForm’ and on ‘NewForm’), otherwise only the option ‘Resubmit’.

In our JavaScript rendering template we define the custom namespace, that includes the member properties and methods of the template. The same editFieldMethod function will be used in both editable modes. It’s simply a wrapper around the standard display template of choice fields (SPFieldChoice_Edit), the single extra work it performs is to store the ID of the corresponding HTML element (select in this case) into a member property called controlId. The standard format of the Id is NameOfTheChoiceField_GuidOfTheChoiceField__$DropDownChoice, for example in my case it is Status_fb5a9aac-5fdb-442e-96ac-ab7161cc4208_$DropDownChoice. We store its value to be able to find the HTML element and it children option elements via jQuery later in our asynchronous callback method.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. custom.editFieldMethod = function (ctx) {
  9.     var fieldSchema = ctx.CurrentFieldSchema;
  10.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  11.     var html = SPFieldChoice_Edit(ctx);
  12.     return html;
  13. }

We created a simple escapeForJQuery helper function to escape the dollar sign ($) in the ID, as I found IE 11 and  jQuery have issues with that character when used in selectors.

  1. custom.escapeForJQuery = function (value) {
  2.     var newValue = value.replace(/\$/g, "\\$");
  3.     return newValue;
  4. }

Note: you might have problems with the underscore (_) as well, especially if you use old browser versions, however I have not experienced such problems.In this case you should extend the escapeForJQuery helper function. See this guide:

Given this fact, authors who write CSS often attempt to employ the underscore in a similar fashion when creating class and ID names. This should not be done. Although underscores are, as of this writing, technically permitted in class and ID names, there are many historical and practical reasons why they should be avoided.

We utilize the escapeForJQuery function in our next helper function. The hideOptions method hides those options of a specific HTML element with ID specified in the ctrlId parameter that have any of the the values specified in the restrictedValues array parameter:

  1. custom.hideOptions = function (ctrlId, restrictedValues) {
  2.     restrictedValues.forEach(function (rv) {
  3.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  4.         $(selector).remove();
  5.     });        
  6. }

We use a third helper function called isCurrentUserMemberOfGroup to determine via CSOM if the current user is member of a group. This function – borrowed from this answer – has two parameters: the name of the group (groupName) and a callback method (OnComplete).

  1. custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  2.  
  3.     var clientContext = new SP.ClientContext.get_current();
  4.     var currentUser = clientContext.get_web().get_currentUser();
  5.  
  6.     var userGroups = currentUser.get_groups();
  7.     clientContext.load(userGroups);
  8.  
  9.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  10.  
  11.     function OnSuccess(sender, args) {
  12.         var isMember = false;
  13.         var groupsEnumerator = userGroups.getEnumerator();
  14.         while (groupsEnumerator.moveNext()) {
  15.             var group = groupsEnumerator.get_current();
  16.             if (group.get_title() == groupName) {
  17.                 isMember = true;
  18.                 break;
  19.             }
  20.         }
  21.  
  22.         OnComplete(isMember);
  23.     }
  24.  
  25.     function OnFailure(sender, args) {
  26.         OnComplete(false);
  27.     }
  28. }

The isCurrentUserMemberOfGroup function is invoked by the applyPermissions function. In the callback function we hide the adequate options based on the group membership of the user.

  1. var adminGroup = "MyGroup";
  2.  
  3. custom.applyPermissions = function (ctx) {
  4.     custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  5.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  6.  
  7.         if (custom.controlId) {
  8.             if (isCurrentUserInGroup) {
  9.                 custom.hideOptions(custom.controlId, restrictedValues1);
  10.             }
  11.             else {
  12.                 custom.hideOptions(custom.controlId, restrictedValues2);
  13.             }
  14.         }
  15.     });
  16. };

In our rendering template we register the custom editing method editFieldMethod, and set the applyPermissions function to be called as OnPostRender:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. customOverrides.Templates.OnPostRender = custom.applyPermissions;
  12.  
  13. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     custom.editFieldMethod = function (ctx) {
  13.         var fieldSchema = ctx.CurrentFieldSchema;
  14.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  15.         var html = SPFieldChoice_Edit(ctx);
  16.         return html;
  17.     }
  18.  
  19.     custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  20.  
  21.         var clientContext = new SP.ClientContext.get_current();
  22.         var currentUser = clientContext.get_web().get_currentUser();
  23.  
  24.         var userGroups = currentUser.get_groups();
  25.         clientContext.load(userGroups);
  26.  
  27.         clientContext.executeQueryAsync(OnSuccess, OnFailure);
  28.  
  29.         function OnSuccess(sender, args) {
  30.             var isMember = false;
  31.             var groupsEnumerator = userGroups.getEnumerator();
  32.             while (groupsEnumerator.moveNext()) {
  33.                 var group = groupsEnumerator.get_current();
  34.                 if (group.get_title() == groupName) {
  35.                     isMember = true;
  36.                     break;
  37.                 }
  38.             }
  39.  
  40.             OnComplete(isMember);
  41.         }
  42.  
  43.         function OnFailure(sender, args) {
  44.             OnComplete(false);
  45.         }
  46.     }
  47.  
  48.     custom.escapeForJQuery = function (value) {
  49.         var newValue = value.replace(/\$/g, "\\$");
  50.         return newValue;
  51.     }
  52.  
  53.     custom.hideOptions = function (ctrlId, restrictedValues) {
  54.         restrictedValues.forEach(function (rv) {
  55.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  56.             $(selector).remove();
  57.         });        
  58.     }
  59.  
  60.     var adminGroup = "MyGroup";
  61.  
  62.     custom.applyPermissions = function (ctx) {
  63.         custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  64.             console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  65.  
  66.             if (custom.controlId) {
  67.                 if (isCurrentUserInGroup) {
  68.                     custom.hideOptions(custom.controlId, restrictedValues1);
  69.                 }
  70.                 else {
  71.                     custom.hideOptions(custom.controlId, restrictedValues2);
  72.                 }
  73.             }
  74.         });
  75.     };
  76.     
  77.     var customOverrides = {};
  78.     customOverrides.Templates = {};
  79.  
  80.     customOverrides.Templates.Fields = {
  81.         'Status': {
  82.             'EditForm': custom.editFieldMethod,
  83.             'NewForm': custom.editFieldMethod
  84.         }
  85.     };
  86.  
  87.     customOverrides.Templates.OnPostRender = custom.applyPermissions;
  88.  
  89.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  90.     
  91. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/_layouts/15/sp.runtime.js|~sitecollection/_layouts/15/sp.js|~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate.js"
$field.Update()

Stay tuned, the second part of the post including a synchronous approach should come soon.

Blog at WordPress.com.