SharePoint– Related data from REST queries to dynamically generate tables of checkboxes within a panel.

Tuesday, April 22 2014

In this post, we expand on our dynamic checkboxes from the last post.  Managing SharePoint list choice options can be challenging. The benefit of choices is that they provide a consistent means to analyze the list information.  You most likely want to maintain a fairly tight control on the choices so that they can be used for filtering.  Many times, this means that either the IT team members or a business process owner (site admin) must maintain these choices on request.  Additionally, if the forms for the list are customized, changes in the list will likely result in the need to modify the list item pages (new, display and edit).  So how do we leverage custom list item pages and still provide a low maintenance checkbox panel?  In this example, our  Equipment List will have a standard SharePoint choice field type, but the number of items in the list makes using the list as a dropdown data source impractical.  We will instead use filters based on the choice options to provide a dynamic panel of checkboxes for the selected category of equipment.  Adding new equipment to the list will result in the form dynamically adding the new item to the result set  and adding the checkbox to the panel on refresh.

In our solution, we will be looking at building a related list which can be managed by the business unit.  Using the JavaScript Client Object Model and jQuery, we will generate REST queries to return our related items and dynamically build a table of checkboxes within a checkbox panel.  We will also be adding the ability to configure the number of columns in our table to provide better control of the layout of the checkboxes and their labels. Then, we will look at creating a filter checkbox panel which will modify our REST queries to return a subset of the related items.  This allows us the ability to manage much larger related datasets without making the checkbox panels overwhelming to the end user.  In a future post, we’ll look at changing the labels on our dynamic checkboxes to document links, so that we can either create new documents within a library, or retrieve an existing related document if it already exists.  The advantages to this feature is that we can attach workflows to our content types and use our list items page as a central access point for many related parallel business processes.

Let’s dive into “how”.  We will be looking at the equipment requirements and equipment tab. The equipment requirements panel is where we will be selecting the category of the equipment.  These checkboxes will determine the criterion attached to the REST query against the Equipment list in SharePoint.  Then, the dataset returned from the query will be used to dynamically generate a checkbox for each equipment item of the selected categories.  The multi-tabbed interface that houses the equipment panel will be outside the scope of this blog, but there are many excellent examples of this functionality available on the Web.  The state of the checkbox is persisted to a related list that contains the ID if the master record and the equipment label.  After the checkbox panel is generated, then another REST query is run against the ReqEquipment list and the “checked” status is update for each checkbox.  It sounds complex, but let’s break it down piece by piece.

First, let’s look at the HTML markup for our equipment tab.  Not surprisingly, we have chosen a table component for our table of checkboxes.

<div class="noterm" id="tabs-equip" style="width: 98%; margin-right:20px;">
        <table id="Equipment" data-bind="ReqEquipment" data-lookup="Equipment" data-lookupName="EquipmentID" data-masterId="RequestIDId" data-lookupId="EquipmentIDId" class="certTbl">

The table is empty but has declarative binding attributes that will be used by our JavaScript methods to run the queries necessary for the dynamic generation of the elements.  Notice also, that the ”equipment”  id has been assigned to the table element so that the table and its children can be accessed by our code.

In the case of our equipment requirements or filter panel, because of the small number of choices and the fact that the choices were not likely to change, the checkboxes were coded statically into the request list and forms.  There would be no restriction to making these checkboxes dynamic like the equipment checkboxes, but for this solution that approach was too complex and expensive.

<div class="sectionDiv noterm">Equipment Requirements</div>
<table class="noterm" border="0" cellspacing="0" width="100%" style="background-color:#E9EDF1; border: thin white outset; Padding: 10px;">
    <tr><td colspan="5"><span class="filterGuide"> Uncheck all checkboxes see all Equipment Types or select checkboxes to see only the chosen type(s) </span></td></tr>
    <tr id="EquipTypePanel">
        <td class="cbInline">
            <span class="filtlbl">PC</span>
            <SharePoint:FormField runat="server" id="ff31{$Pos}" controlmode="Edit" fieldname="PC" __designer:bind="{ddwrt:DataBind('u',concat('ff31',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@PC')}" />
            <SharePoint:FieldDescription runat="server" id="ff31description{$Pos}" FieldName="PC" ControlMode="Edit"/>
        <td class="cbInline" >
            <span class="filtlbl">Phone</span>
            <SharePoint:FormField runat="server" id="ff29{$Pos}" controlmode="Edit" fieldname="Phone" __designer:bind="{ddwrt:DataBind('u',concat('ff29',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@Phone')}" />
            <SharePoint:FieldDescription runat="server" id="ff29description{$Pos}" FieldName="Phone" ControlMode="Edit"/>
        <td class="cbInline">
            <span class="filtlbl">Mobile</span>
            <SharePoint:FormField runat="server" id="ff30{$Pos}" controlmode="Edit" fieldname="Mobile" __designer:bind="{ddwrt:DataBind('u',concat('ff30',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@Mobile')}" />
            <SharePoint:FieldDescription runat="server" id="ff30description{$Pos}" FieldName="Mobile" ControlMode="Edit"/>
        <td class="cbInline">
            <span class="filtlbl">Office</span>
            <SharePoint:FormField runat="server" id="ff32{$Pos}" controlmode="Edit" fieldname="Office" __designer:bind="{ddwrt:DataBind('u',concat('ff32',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@Office')}" />
            <SharePoint:FieldDescription runat="server" id="ff32description{$Pos}" FieldName="Office" ControlMode="Edit"/>
        <td class="cbInline">
            <span class="filtlbl">Other</span>
            <SharePoint:FormField runat="server" id="ff33{$Pos}" controlmode="Edit" fieldname="Other" __designer:bind="{ddwrt:DataBind('u',concat('ff33',$Pos),'Value','ValueChanged','ID',ddwrt:EscapeDelims(string(@ID)),'@Other')}" />
            <SharePoint:FieldDescription runat="server" id="ff33description{$Pos}" FieldName="Other" ControlMode="Edit"/>
        <td id="libLink" style="float:right; text-align:center">
            <a href="
http://server/operations/NOPA/EmployeeSetupDocs/Forms/AllItems.aspx" target="_blank" >Guides</a></td>

Also, in this application, the “Title” field, which is the key to the master table or list, is a calculated field, but I need the value frequently.  so I added a “masterKey” ID to the element that contains my calculated key value.

<td class="overFieldLbl">
         <span style="font-weight:bold; font-size:10pt">RequestID : </span>
          <span  style="color:#CC0000; font-weight:bold; font-size:10pt" id="masterKey"><xsl:value-of select="
/dsQueryResponse/Rows/Row/@RequestID" /></span>

Another piece which needs to be completed is the ReqEquipment list.  My example is probably more complex than yours will need to be.

Title is generated by our code when a checkbox is checked using the RequestID and EquipmentID in a multi-segmented key. 
ReqId and CID are calculated from the Title and were created to simplify queries.  Because lookups are stored as foreign keys  and are hard to work with in queries on dynamic data, we created these simple fields.
RelatedDocUrl is where we will store the link which will either query and return an existing form required for the option, or open a new form if one does not exist.

Now that we have our underlying data and DOM infrastructure in place, we are ready to look at the JavaScript that performs the magic on the form.

In my applications, I use a wrapper to CSOM that creates a session context and retrieves and stores many of the commonly needed variables from the Client Object Model.  That library is outside of the scope of this blog.  I may be contributed to open source in the future.  For this example you can create a “session” namespace to use.  The following code will create an empty session object to which we can bind our session variables. 

ExecuteOrDelayUntilScriptLoaded($, "sp.js");
$(document).ready(function() {
    var session = session || {}; // create an object for namespacing
    // Our custom code will go here.

This next section, we are going to be looking at a lot of code. Pay attention to the methods and their functions and how they are called by the core modules.

To support dynamically updating the  equipment checkboxes when a filter checkbox was changed, We needed to bind an event to the filter checkboxes.  This was very easy with a little jQuery.

// bind an onChange event to the EquipTypePanel checkboxes
    $($('#EquipTypePanel td input')).change(function(){getEquipment(session);}); //The callback getEquipment() is bound to the change event of the EquipTypePanel

The core of the equipment panel logic is contained within the callback function getEquipment

function getEquipment(session) {
    // Equipment lookup list object
    var Equipment = {
        "name"   :"Equipment",                                  // name of LookUp list
        "restRoot"  :"",                                                 // reserved for REST url
        "onClick"  :"onclick=persistCBChanged(this)",  // click event callback
        "lookupField" :"EquipmentID",                        // column Name of list lookup Field
        "catField"  :"Category",                                    // column Name of the lookup Category
        "labelDesc"  :"Description",                              // column name of List item Description
        "relatedList" :"ReqEquipment",                        // name of the related list
        "numCbCols"  : 3,                                             // number of checkbox columns
        "docLib"  :"",                                                    // library for related documents and workflow - empty = not not used
        "Items"   :{}                                                       // reserved for query results
    getListUrl( Equipment ); // add REST Url to Equipment Object
    Equipment.queryOpts = {};  // this empty object is updated dynamically from the filter checkboxes
    session.Equipment = Equipment;

    var ReqEquipment = {                      // related table which holds the checkbox values
        "name"   :"ReqEquipment",         // name of detail list
        "lookupField" :"EquipmentID",     // column Name of lookup Field
        "keyFieldName" :"Title",              // column Name of key lookup Field
        "restRoot"  :"",                              // reserved for REST url
        "cbID"   :"CID",                             // column Name of list item ID
        "docLink"  :"",                               // link to related document of workflow (RelatedDocUrl in the list)
        "Items"   :{}                                   // reserved for query results
    getListUrl(ReqEquipment ); // returns the RestUrl
    session.ReqEquipment = ReqEquipment;
    // this kicks off an asynchronous query to populate choices used for category filtering options
    getChoiceOptions( session, Equipment );

Now we need our supporting methods for our Equipment Object.  persistCBChanged() is bound to the equipment checkboxes and adds or deletes the ReqEquipment items based on whether the checkbox is checked or cleared.  It uses addListItem() and deleteListItem() to perform REST queries to manage our related list.  It also uses getCB() to create and populate a checkbox object that parses the DOM element for its attributes and sets the detailId for the object.

// Adds or deletes records in the related tables linked to the master table by ID
function persistCBChanged(caller) {
    var cb = getCb(caller);
    var lookupIdName = cb.lookupIdName;
    var fileIdName   =cb.fileIdName;
    var title = "Title";

    if(caller.checked) {
        //use REST to create Record
        var relRec = {};
        relRec[title]    = cb.detailKey;
        relRec[fileIdName]   = session.masterId;
        relRec[lookupIdName]  = cb.lookupId;

        var relationRec = JSON.stringify(relRec);
       addListItem(cb.dataList, relationRec);

    } else {
        // use REST to delete the record
        deleteListItem(cb.dataList, cb.detailId);

function addListItem(listName, item) {

    var listObj = session[listName];
    // Prepping our update
    var restUrl = listObj.restRoot + ;
    //console.log("Adding item with REST-url "+ listObj.path);
            url: restUrl,
            type: "POST",
            contentType: "application/json;odata=verbose",
            data: item,
            headers: {
                "Accept": "application/json;odata=verbose",
                "X-RequestDigest": $("#__REQUESTDIGEST").val()
            success: function (data) {
                return(data); // Returns the newly created list item information
            error: function (jqXHR, textStatus, errorThrown) {
                alert(textStatus + ": " + errorThrown );


// Deleting a List Item based on the ID Using REST

function deleteListItem(listName, itemId) {
    var listObj = session[listName],
        itemUrl = listObj.restRoot + listName +  '(' + itemId + ')';
    // getting our item to delete, then executing a delete once it's been returned
    getListItemWithId(itemId, listName, itemUrl, function (data) {
                url: data.__metadata.uri,
                type: "POST",
                headers: {
                    "Accept": "application/json;odata=verbose",
                    "X-Http-Method": "DELETE",
                    "X-RequestDigest": $("#__REQUESTDIGEST").val(),
                    "If-Match": data.__metadata.etag
                success: function (data) {
                    // success(data);
                error: function (jqXHR, textStatus, errorThrown) {
                    alert(textStatus + ": " + errorThrown );
        function failure(jqXHR, textStatus, errorThrown) {
            alert(textStatus + ": " + errorThrown );

// returns a CB (checkbox) object that contains the parameters needed to process clicks
function getCb(caller) {
    var cb = {}; =;                                           // CheckBox Clicked ID
    cb.parentTable =;
    var pt = '#' + cb.parentTable;
    cb.dataList = $(pt).attr("data-bind");                     // Detail table name  
    cb.dataLookup = $(pt).attr( "data-lookup" );          // Lookup table name
    cb.lookupField = $(pt).attr("data-lookupName");  // Name of field in the lookup table field with which to link
    cb.fileIdName = $(pt).attr( "data-masterId" );        // FieldName to retrieve ItemID from REST against Master Table
    cb.lookupIdName = $(pt).attr( "data-lookupId");
    cb.detailKey = session.masterKey + '-' + ;

    if (!caller.checked) {
        // We just unchecked the box, so we need the detailId for delete
        // T O D O  -- Always check for related docs and Workflows before delete
        cb.detailId = getSpDetailId( cb.dataList, cb.detailKey );
    } else {
        // We just checked the box, there is no record or detailId.
        cb.detailId = 0;
    cb.lookupId = getSpLookupId( cb.dataLookup, );
    return cb;

getEquipment() also calls getListUrl() which build and persists the REST Url to the session object for use throughout the application.

// Sets the REST urls of the listOjects (object literals created by the callback functions)
function getListUrl(listObj){
    var listName =,
        queryOpts = listObj.queryOpts;

    var curPath = window.location;
    var searchParms ="&");
    var searchIdParm = searchParms[0];

    // get the ID of the currently displayed masterItem from the url and persist it to the session
    var searchIdArr = searchIdParm.split("=");
    session.masterId = searchIdArr[1]; // The ID from the URL for the EmployeeSetupRequest List

    // build the REST Url for this listObject
    var pathParts = curPath.pathname.split('\/');
    var path = curPath.protocol + '\/\/' + curPath.hostname ;
    var listNamePos = pathParts.length - 2;
    for(var i = 0 ; i < listNamePos ; ++i) {
        if (pathParts[i] !== 'Lists') {
            path += pathParts[i] + '\/';
    path +=  ("_vti_bin\/listdata.svc" + '\/');
    listObj.restRoot = path;
    listObj.path = path + listName ;
    // persist it back to the session
    session[] = listObj;

    // if query options are passed, parse them into the REST query
    // NOTE!  no queryOpts means NO filter
    if (queryOpts) {
        // options object is passed, but we need the true values
        var filterCount = 0;
        $.each( queryOpts, function ( name, value){
            if( value === true ) {
                filterCount ++;
                if (filterCount === 1) {
                    listObj.path += "?$filter="+ listObj.catField + "Value eq '" + name + "' ";
                } else {
                    listObj.path += 'or ' + listObj.catField + "Value eq '" + name + "' ";

getChoiceOptions()  is an asynchronous method to retrieve the choices from a choice field and create an object of queryOpts and the choice option status.  This object is used to populate the choice checkboxes.

// Function to return all of the choices from the Choice column passed in
getChoiceOptions = function (session, listObj) {
    var fieldChoices = [],
        ctx = new SP.ClientContext.get_current(),
        web = ctx.get_web(),
        list = web.get_lists().getByTitle(,
        field = list.get_fields().getByInternalNameOrTitle(listObj.catField),
        choiceField = ctx.castTo(field, SP.FieldChoice);

    /* Call executeQueryAsync and pass in success and failure
     anonymous functions */
    ctx.executeQueryAsync(function () {
            // Return array of all of the choices in the choice field
            var catChoices = choiceField.get_choices();
            /* Since call is asynchronous, call function to interact
             with the choices */
            onFieldLoaded( session, listObj, catChoices );
        // This will be called when executeQueryAsync fails
        function (sender, args) {

function onFieldLoaded( session, listObj, catChoices ) {
    listObj.Choices = catChoices;
    // REST query filter categories are async and must be delayed till success
    var queryOpts = listObj.queryOpts;
    $.each(catChoices, function ( index, value ){
        var name = value;
        var cb = $('input[title="' + name +'"')[0];
        if (cb.checked != 'undefined') {
            var cbValue = cb.checked;
            queryOpts[name] = cbValue;
        } else{
            console.log(' support Note!!!  DOM CB Items MUST have the title of the List Choice categories ');
            alert('No Checkbox with the name ' + name + ' was found on the form!\r\nPlease contact the helpdesk!');
    listObj.queryOpts = queryOpts;
    session[].queryOpts = queryOpts;
    // persist our list object to the session
    session[] = listObj;
    // populate our panel with checkboxes
    if (session.masterKey) {
        // set the 'checked' status of each checkbox for this master's instance
        // must be  called AFTER getListUrl and buildCbArray
       setDetailStatus( session[listObj.relatedList], session.masterKey, listObj.restRoot );

onFieldLoaded() is call by getChoiceOptions() when the CSOM request succeeds.  It then creates the panel with a call to getCBPanel() and checks the appropriate checkboxes by calling setDetailStatus().  These functions are the workhorses of the application that work with the DOM and inject our dynamic content.  All of this DOM update is encapsulated into these methods for performance reasons.

// Using a REST call, this function queries the lookup table and creates the checkbox panel
// the listObj from the callback function contains a "numCbCols" value which sets the number
// of columns in the panel.
function getCbPanel(listObj) {
   var thisList = listObj;
   var cbName =;
   var getNewDocLink = function(docLib) { // in this case, listObj is the related table
       // this should only be called when docLib !== undefined

        var docPath =  session.siteUrl + docLib;

        var pathArray = docLib.split('\/');
        var pathLen = pathArray.length;
        libPath = session.siteUrl;
        for(var i = 0 ; i < pathLen-1 ; ++i ) {
            if( pathArray[i] === '' ) {
                libPath += '\/';
            } else {
                libPath += pathArray[i];
        docPathEnc = docLinkEncode(docPath);
        libPathEnc = docLinkEncode(libPath);

        var newDocLink = "CoreInvoke('createNewDocumentWithRedirect2',event,'" + docPathEnc + "','"+ libPathEnc + "',";
        newDocLink += "'SharePoint.OpenDocuments', false, '" + session.siteUrl + "_layouts/CreateNewDocument.aspx?id=" + docPath + "',true, 1); return false;";
        return newDocLink ;

    var cbPanel =  document.getElementById(;
    if(!session.masterKey) {
        var curRow = 0;
        var curRowId = cbName + 'row_' + curRow;
        curRowId = cbName + 'row_'+ curRow;
        $(cbPanel).append('<tr id=' + curRowId + '></tr>');
        var cbTd = '<td><label>The Request must be saved before you can use the checkbox panels</label></td>';
        $('#' + curRowId).append(cbTd);
        $('#tabs').prop('disabled', true);
        type: 'GET',
        contentType: "application/json;odata=verbose;",
        url: listObj.path,
        async: false,
        processData: false,
        data: listObj.items,
        dataType: "json",
        success: function( data, listObj ) {
            var curRow = 0;
            var xhrResult = data.d.results;
            $(xhrResult).each(function (idx){
                var curRowId = cbName + 'row_' + curRow;
                // DO NOT CHANGE ANY of this code or you will break the Checkbox-Table component.
                if (idx %(thisList.numCbCols)=== 0) {
                    curRow ++;
                    curRowId = cbName + 'row_'+ curRow;
                    $(cbPanel).append('<tr id=' + curRowId + '></tr>');

                var cbLabelTag = thisList.lookupField;

                var cbDescTag  = thisList.labelDesc;
                var cbId = this[cbLabelTag];
                var cbLabel = '';
                if(cbLabelTag === "TrainingID" || cbLabelTag === "AppID" ) {
                    cbLabel = this[cbDescTag];
                } else {

                    cbLabel = cbId  +  ' - ' + this[cbDescTag];

                var cbTd = '<td><input id="' + cbId + '" type="checkbox" ' + thisList.onClick;
                // disable checkboxes in display mode
                if (session.pageMode==='Display') {
                    cbTd += " disabled='disabled'";

                cbTd += '></input>&nbsp;&nbsp;<label>' + cbLabel + '</label></td>';
                //var cbTd = '<td><input id=' + this[cbLabelTag] + ' type="checkbox" ' + listObj.onClick + '></input>&nbsp;&nbsp;<label>' + cbLabel + '</label></td>');
                $('#' + curRowId).append(cbTd);
        fail: function () {
            return "No Items Found";

// After the checkbox panel is built, this function will select the related records and check the checkboxes for
// all related records.
function setDetailStatus( listObj, masterKey, restRoot ) {
    // restRoot allows override of current site for the ajax call.
    listObj.query = restRoot + + ("?$filter=ReqID eq '" + masterKey + "'");
    var query = listObj.query;
    var items = listObj.items;
        type: 'GET',
        contentType: "application/json;odata=verbose;",
        url: query,
        processData: false,
        data: items,
        dataType: "json",
        success: function (data) {
            var xhrResult = data.d.results;
            $(xhrResult).each(function (idx){
                var cbIdTag = listObj.cbID;
                $( '#' + this[cbIdTag] ).prop('checked', true);
                // listObj.docLink
                /// T O D O add logic to initialize DocLinks from the listObj
        fail: function () {
            alert("No Items Found");

docLinkEncode()  is a helper function to provide HTML encoding for some of the special and reserved characters in our document links.

function docLinkEncode(str) {
    if(null===str || typeof(str)=='undefined'){
        return "";
    var strIn=new String(str);
    var strOut=new Array();
    var ix=0;
    var max=strIn.length;
    for (ix=0; ix < max; ix++)
        var charCode=strIn.charCodeAt(ix);
        if (charCode > 0x0fff)
        else if (charCode > 0x00ff)
        else if (charCode > 0x007f)
            var c=strIn.charAt(ix);
            switch (c)
                case '/':
    return strOut.join('');

For the final setup for the application, determine the  business unit users that will have the ability to edit the choice list and give them contribute permissions, removing that permission from the rest of the users of the site.  Now we have a very simple interface for the users to manage their choices that will not require any IT engagement or form updates.

That was a lot to cover for one post.  I’ve left the wiring of the document links to the checkbox labels for the future.  In my project, the users decided to use  the Related Forms panel  and keep all the forms together rather than having dynamic links on the labels.


I hope you’ve found this post useful and feel that I’ve given you a lot of code to reference for your projects.  Check back in the future and until then “Good Coding”

1 comment(s)

Tushar wrote on January 23, 2008

I use infopath to customize edit form of a list. Is this also modified with the help of infopath?

Hi Tushar, If I understand your question, then yes. If the list (related data file used for the lookup) is updated using InfoPath, the checkbox panel will still work. The checkbox panel uses a REST query against the related list (in the example, equipment list). The REST query works the same no matter how the related list is edited. If you are asking if this code can be used inside InfoPath forms, the answer is not so simple. It depends how good your skills are at managing both JavaScript and XSLT programming. Technically it is possible. The example given was created using SharePoint Designer and a custom list item form. I lean more heavily on HTML and JavaScript for my custom forms. If this doesn't answer your question, please try rephrasing it and giving me more details regarding what you would like to do.