My last project required creation of a dynamic list of approvers for the approval process (a coincidence?), based on a location and volume threshold. And some other parameters, but this is not a case. At first I naturally thought about a list, that would hold such mappings for me. Then I thought to query that list within a workflow, using filtering to gather only a specific subset and then, using a state machine, to go through and assign tasks.
But there was a catch! Customer expected, that the form should allow to display that list of dynamically gathered approvers and then to show how each one expressed approval. And with the possibility to add or remove existing ones!
Naturally, I decided to use the repeating section control, but I didn't know how to fill it dynamically. I managed to do that however, and the results look like this:
And the source list for data lookups:
How such dynamic repeating section can be made?
Step by step. Let's begin!
The data structure
- Locations (just a simple list with title)
- ApprovalThresholds - a list built of the following fields:
- TItle
- Approver (person field, many users allowed)
- Location (single lookup)
- Threshold (number)
- OrderNo (number, in my case it was used to identify a group to which specific user belongs)
- WorkingList (it was called differently, but for the demo let's stay with this name )
- Title
- Location (single lookup)
- Volume (number)
- Approvers (multiline text field, plain text)
The form
Then I created a form for the WorkingList list. With some enchantments of course:
- Volume field was given a JavaScript variable name: var_Volume;
- Location field was given a JavaScript variable name: var_Location;
- And then I deleted the default "Approvers" field, the textarea, and replaced it with a repeating section:
The repeating section has added a CSS class: approvers:
It is built of three input fields:
- Approver name - is given a CSS class: approversApprover
- Approver email - is given a CSS class: approversApproverEmail
- Approval group - is given a CSS class: approversOrderNo
There are also two checkboxes, one is having a JavaScript variable name: var_IsEditMode, the other var_IsNewMode, to pass information to the script how to behave, having set the "Default value" to "Expression":
And basically that's it for the form. All the magic is handled by a jQuery script.
The script
Script is doing the following things:
- It binds change and blur listeners to the Volume and Location fields;
- It defines function that is triggered by the listeners events;
- It adds dynamic controls to the repeating section once it is in the edit mode.
- It also allows to hide or leave untouched controls for the repeating section (hideNativeRepeatingSectionControlls variable)
AD. 1 - listeners and bindings
var clientContext = new SP.ClientContext();
var siteurl = _spPageContextInfo.webAbsoluteUrl;
var hideNativeRepeatingSectionControlls = 0;
NWF$(document).ready(function () {
//hide "add row" link in repeating section
if (hideNativeRepeatingSectionControlls) NWF$(".approvers").find('.nf-repeater-addrow').css("visibility", "hidden");
//trigger if location, CapitalExp or Volume is changed - recalculate list of Approvers
NWF$("#" + var_Location).change(function () { retrieveApprovers(); });
NWF$("#" + var_Volume).blur(function () { retrieveApprovers(); });
if (NWF$("#" + var_IsEditMode).prop("checked")) redrawRepeatingTableEditMode();
});
AD. 2- function handling the changes
function retrieveApprovers() {
var oList = clientContext.get_web().get_lists().getByTitle('ApprovalThresholds');
var camlQuery = new SP.CamlQuery();
var locationArr = NWF$("#" + var_Location).val().split(";#");
var location = locationArru1];
var locationCaml = '<Eq><FieldRef Name="Location" /><Value Type="LookupMulti">' + location + '</Value></Eq>';
var volume = NWF$("#" + var_Volume).val().replace(/e,]/gi, ");
if (!volume) volume= 0;
camlQuery.set_viewXml('<View><Query><Where><And>' + locationCaml +
'<Leq><FieldRef Name="Threshold" /><Value Type="Number">' + volume+ '</Value></Leq>' +
'</And></Where>' +
'<OrderBy><FieldRef Name="Title"/><FieldRef Name="GroupOrderNo"/></OrderBy></Query></View>');
this.collListItem = oList.getItems(camlQuery);
clientContext.load(collListItem);
clientContext.executeQueryAsync(
Function.createDelegate(this, this.onQuerySucceeded),
Function.createDelegate(this, this.onQueryFailed)
);
}
What id does, is gathering information from the form and construction of a CALM query, that it sends to SharePoint to get a list of approvers. If it succeeds it triggers the "onQuerySucceeded" function.
What that function does is making a loop through each returned row.
Then, for each approver (as there may be more than one) it calls SharePoint endpoint for additional information (like login or email):
function onQuerySucceeded(sender, args) {
// Redraw the existing table, remove everything what exists, leave fresh instance
redrawRepeatingTable();
var listItemEnumerator = collListItem.getEnumerator();
while (listItemEnumerator.moveNext()) {
var oListItem = listItemEnumerator.get_current();
var approvers = oListItem.get_item('Approvers');
var approvalOrder = oListItem.get_item('GroupOrderNo');
NWF$(approvers).each(function (idx, obj) {
var person = JSON.stringify(obj);
person = JSON.parse(person);
// get user's display name and ID
var approverId = person.$1T_1;
var approverName = person.$4K_1;
var userData = ";
// ask for users additional data
NWF$.ajax({
url: siteurl + "/_api/web/getuserbyid(" + approverId + ")",
method: "GET",
async: false,
headers: { "Accept": "application/json; odata=verbose" },
error: function (data) {
console.log("Error: " + data);
}
Once it gets it (done) it pushes the information to the last, found row (.nf-repeater-row:last) using the '.approvers' class as the selector for the repeating field control.
This is a key point here. The class name is the only way for the script to reach the control and then its contents.
If the "hideNativeRepeatingSectionControlls" is set to true, it also removes the "X" icon from the row, so that it cannot be deleted using the UI. It also covers it with the overlay, so the user won't be able to change the values.
You cannot set fields to be disabled or hidden using the CSS or Forms rules, as the hidden or disabled fields, for some reason, are not being taken into consideration during the form saving. So if you put a value into such field, that value won't get saved to SharePoint.
Instead use "visibility:hidden" (rather than "display:none") and a calculated field or overlay to make a field "disabled".
After it fills all the field in a row it simulates the "click" on the "add row" link that is underneath the repeating section, to create a new, blank row:
}).done(function (userData) {
NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input').val(approvalOrder);
NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input.nf-associated-control').attr("style", NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input.nf-associated-control').attr("style") + "background-color: transparent !important;");
NWF$(".approvers .nf-repeater-row:last").find('.approversApproverEmail input').val(userData.d.Email);
NWF$(".approvers .nf-repeater-row:last").find('.approversApprover input').val(approverName);
// remove image for row deletion
if (hideNativeRepeatingSectionControlls) NWF$(".approvers .nf-repeater-row:last").find('.nf-repeater-deleterow-image').css("visibility", "hidden");
// append overlay to avoid editting ;) fields must be enabled to allow proper save
if (hideNativeRepeatingSectionControlls) NWF$(".approvers .nf-repeater-row:last").append('<div class="approverOverlay"></div>');
//add next row
NWF$(".approvers").find('a').click();
});
});
}
Then it removes the last, empty row, as it will always be added. It also adds, to every row generated by the process an additional class "toRemoveOnReload" so that the "redrawRepeatingTable()" function will know, what should be deleted, once the repeating section requires to be re-created.
// remove last, empty row, as it is always empty
NWF$