Update Custom SharePoint Forms with JavaScript

If you did not believe that custom SharePoint forms could be updated with client side code, guess again. The script below utilizes the SP.FileCreationInformation?object available in SP 2013 to write file content to whichever forms are specified by the user, to as many subsites as desired within a site collection.

This script can be extremely handy if you ever have the misfortune to be in a situation in which you must update many duplicate SharePoint custom forms. For example, if there are ten subsites, each with duplicate lists, and each list has various custom forms in place (i.e, “New”, “Display”, and “Edit”), this means there are thirty forms. If these forms need to be updated, the typical process of updating a form, (by opening it in SP Designer in “Advanced Mode”, saving, and repeating for each form) can be extremely tedious.

This process typically takes about four minutes to make a single update per form, per site. And with thirty forms, this would take two hours. It goes without saying that doing updates like this is extremely error prone and a waste of a developer’s time. I have developed this script to automate this tedious form updating process.

However, only use this script if you have no other options. Ideally if you have duplicate lists and custom forms spread out across many subsites within a site collection, the “XSL Link” should be utilized in each forms web part to reference an XSL stylesheet. This stylesheet contains all the elements which serve to create the form. With that system in place, editing a single XSL stylesheet would instantly update all forms which reference it, like so:

Benefits of this script

  • Allows for the automatic updating of any number of custom forms associated with a list.
  • Automatically finds all subsite URL’s within the site collection.
  • Accomplishes the work that could typically take several hours in around 20-30 seconds.

Drawbacks

  • No backups are made of the target forms! If there is an error in the updating process there is no way to alternative plan.
  • In order to use this script properly, you must make your desired updates to one of each of the forms you wish to update, and save those .aspx files as .txt files. Then specify the file paths to those source text files.

The script is organized into four parts:

  1. Collect all subsite URL’s within the site collection (optional).
  2. Collect the list GUID’s from each subsite’s list.
  3. Collect the web part ID’s, source file content, and create the new custom forms based on the template form provided.
  4. Update and replace each target form with the new form.

The updated forms you wish to use must be saved in advance as .txt files because .aspx files requested via Ajax will not be able to display the data from the XSL stylesheet stored in the form. By making an Ajax request for the .txt files, the XSL data can be properly collected.

Initialize

Below is a simple example of how you can initialize the script. If you leave the sites parameter blank, all subsite URL’s will be gathered and tested to see if the list exists, and then proceed to begin the updating.

// Example of how to initialize the script.
var init = function() {

    var listName = 'Appointments';
    var sourceFilePath = "https://example.sharepoint.com/assets";

    var sourceFiles = [
        { filePath: sourceFilePath + '/NewForm.txt',  title: 'NewForm'  },
        { filePath: sourceFilePath + '/EditForm.txt', title: 'EditForm' },
        { filePath: sourceFilePath + '/DispForm.txt', title: 'DispForm' }
    ];
    
    var sites = [
        "https://example.sharepoint.com/subsite1",
        "https://example.sharepoint.com/subsite2",
        "https://example.sharepoint.com/subsite3",
        "https://example.sharepoint.com/subsite4"
    ];

    return CustomFormUpdater(listName, sourceFiles, sites);
}();

Part 1: Gather all Subsite URL’s

// Collect all URL's of subsites in a site collection

function GetSubSiteUrls() {

    // borrowed from Vadim Gremyachev: https://sharepoint.stackexchange.com/questions/137996/get-all-subsites-and-rootweb-of-a-site-collection-using-jsom#answer-137998
    function enumWebs(propertiesToRetrieve, success, error) {
        var ctx = new SP.ClientContext.get_current();
        var rootWeb = ctx.get_site().get_rootWeb();
        var result = [];
        var level = 0;
        ctx.load(rootWeb, propertiesToRetrieve);
        result.push(rootWeb);
        var colPropertiesToRetrieve = String.format('Include({0})', propertiesToRetrieve.join(','));
        var enumWebsInner = function (web, result, success, error) {
            level++;
            var ctx = web.get_context();
            var webs = web.get_webs();
            ctx.load(webs, colPropertiesToRetrieve);
            ctx.executeQueryAsync(function () {
                for (var i = 0; i < webs.get_count(); i++) {
                    var web = webs.getItemAtIndex(i);
                    result.push(web);
                    enumWebsInner(web, result, success, error);
                }
                level--;
                if (level == 0 && success)
                    success(result);
            }, fail);
        };
        enumWebsInner(rootWeb, result, success, error);
    }

    function success(sites) {
        var urls = [];
        for (var i = 1; i < sites.length; i++) {
            urls.push(sites[i].get_url());
        }

        return GetListGuidsForSubsites(urls);
    }

    function fail(sender, args) {
        console.log(args.get_message());
    }
    enumWebs(['Url', 'Fields'], success, fail);
}

Part 2: Filter URL’s and Collect List GUID’s

If a subsite URL has the mentioned list name, collect the list GUID and add it to the main object.

/*
* Part 2.
* Filter down array of subsite URL's and return the ones that have the specified list.
* @param is an array of strings, each of which is a subsite URL.
*/

function GetListGuidsForSubsites(sites) {
    var targetSites = [];
    var siteIndex = 0;

    function controller() {
        if (siteIndex < sites.length) {
            getListGuid(sites[siteIndex]);
        } else if (targetSites.length > 0) {
            console.log('Done collecting list guids.');
            return GetListAndFormData(targetSites);
        }
    }
    controller();

    function createTargetFiles(siteURL) {
        var arr = [];

        for (var i = 0; i < sourceFiles.length; i++) {
            arr.push({ filePath: siteURL + '/Lists/' + listName + '/' + sourceFiles[i].title + '.aspx', title: sourceFiles[i].title });
        }
        return arr;
    }

    function getListGuid(siteURL) {
        var currentcontext = new SP.ClientContext(siteURL);
        var list = currentcontext.get_web().get_lists().getByTitle(listName);
        currentcontext.load(list, 'Id');
        currentcontext.executeQueryAsync(
            function() {
                var listGuid = list.get_id().toString();
                console.log('List found at ' + siteURL);

                targetSites.push({
                    subsiteURL: siteURL,
                    listGUID: listGuid,
                    listName: listName,
                    targetForms: createTargetFiles(siteURL)
                });
                siteIndex++
                return controller();
            },
            function (sender, args) {
                cosole.warn(args.get_message());
                siteIndex++
                return controller();
            });
    }
}

Part 3: Create the Updated Forms

  1. Get source file content (from the provided .txt files).
  2. Get web part ID for each form, for each subsite.
  3. Create updated form with the web part ID’s, list GUID’s, and site URL.
/*
* Create new target forms for each target site.
* a) get source file content (from .txt files)
* b) get web part ID for each form
* c) create new form with the web part ID's, listGuids, and siteURL
* @param is an array of objects, each containing the subsite URL, list name, and the list GUID specified to that site.
*/

function GetListAndFormData(targetSites) {

    var sourceFormData = [];
    var siteIndex = 0;
    var formIndex = 0;

    if (siteIndex === 0 && formIndex === 0) {
        getSourceFormData();
    }

    function getSourceFormData() {
        return (sourceFormData.length < sourceFiles.length) ? getFileContent(sourceFiles[formIndex].filePath, createSourceFile) : init();
    }

    function createSourceFile(sourceFileUrl, responseText) {
        var _title = sourceFiles[formIndex].title;

        sourceFormData.push({
            url: sourceFileUrl,
            title: _title,
            fileContent: responseText
        });

        formIndex++;
        return getSourceFormData();
    }

    function init() {
        if (siteIndex < targetSites.length) {
            console.log('Finished collecting all source forms. Proceeding to prepare form data for: ' + targetSites[siteIndex].subsiteURL);
            formIndex = 0;
            controller();
        } else {
            console.log( targetSites );
            console.log( 'Finished preparing all form data. Ready to begin updating...');
            UpdateForms(targetSites);
        }
    }

    function controller() {
        return (formIndex < sourceFiles.length && formIndex < targetSites[siteIndex].targetForms.length) ? getFileContent(targetSites[siteIndex].targetForms[formIndex].filePath, getWebPartId) : updateSiteObjsWithNewFile();
    }

    function getWebPartId(index, response) {
        var webPartId = '';
        var regex = new RegExp(/(?<=\<div\sWebPartID=")(.*?)(?=\")/, 'ig');
        if (regex.test(response)) {
            webPartId = response.match(regex)[1].toUpperCase(); // the first match is a guid with all zero's. If the web part containing the form appears on a page with many web part's, make sure you specify the proper index.
        targetSites[siteIndex].targetForms[formIndex].webPartId = webPartId;
        formIndex++;
        return controller();
    }

    function getFileContent(siteURL, callback) {
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function () {
            if (this.readyState == 4 && this.status == 200) {
                return callback(siteURL, this.responseText);
            }
        }
        xhttp.open("GET", siteURL, true);
        return xhttp.send();
    }

    function updateSiteObjsWithNewFile() {

        for (var i = 0; i < targetSites[siteIndex].targetForms.length; i++) {
            for (var j = 0; j < sourceFormData.length; j++) {
                if (targetSites[siteIndex].targetForms[i].title === sourceFormData[j].title) {
                    targetSites[siteIndex].targetForms[i].newContent = sourceFormData[j].fileContent;
                    break;
                }
            }
        }
        parseSourceFileContent(targetSites[siteIndex]);

        console.log('Finished preparing form data for: ' + targetSites[siteIndex].subsiteURL);
        siteIndex++;
        return init();
    }

    function parseSourceFileContent(targetSite) {

        for (var i = 0; i < targetSite.targetForms.length; i++) {

            var targetFile = targetSite.targetForms[i].newContent;

            var webPartElem = targetFile.match(/<WebPartPages:DataFormWebPart(.*?)>/i)[1];
            var oldGuid = webPartElem.match(/ListName="{(.*?)}"/i)[1];
            var oldWebPartId = webPartElem.match(/__WebPartId="{(.*?)}"/i)[1];

            var newWebPartId = targetSite.targetForms[i].webPartId;
            var oldGuidRegex = new RegExp(oldGuid, 'ig');
            var oldWebPartIdRegex = new RegExp(oldWebPartId, 'ig');
            var originalSiteName = targetFile.match(/<ParameterBinding Name="weburl" Location="None" DefaultValue="(.*?)"\/>/i)[1];
            var origSiteNameRegex = new RegExp(originalSiteName, 'ig');

            if (newWebPartId && targetSite.subsiteURL !== originalSiteName) {
                targetSite.targetForms[i].newContent = targetFile.replace(origSiteNameRegex, targetSite.subsiteURL).replace(oldGuidRegex, targetSite.listGUID).replace(oldWebPartIdRegex, newWebPartId);
            }
        }
    }
}

Part 4: Initialize the Form Updating Process

/*
* Update the forms for each target site, one at a time.
* @param is an array of objects, each of which
*/

var UpdateForms = function(targetSites) {

    var siteIndex = 0;
    var formIndex = 0;

    function init() {
        return (siteIndex < targetSites.length) ? controller() : console.log('Finished the update!');
    }
    init();

    function controller() {
        if (formIndex < targetSites[siteIndex].targetForms.length) {
            console.log( 'Updating: ' + targetSites[siteIndex].targetForms[formIndex].title );
            return updateFile(targetSites[siteIndex]);
        } else {
            formIndex = 0;
            siteIndex++;
            return init();
        }
    }

    function updateFile(targetSite) {
        var clientContext = new SP.ClientContext(targetSite.subsiteURL);
        var list = clientContext.get_web().get_lists().getByTitle(targetSite.listName);
        clientContext.load(list);

        var fileCreateInfo = new SP.FileCreationInformation();
        fileCreateInfo.set_content(new SP.Base64EncodedByteArray());
        fileCreateInfo.set_url(targetSite.targetForms[formIndex].filePath);
        fileCreateInfo.set_overwrite(true);

        for (var i = 0; i < targetSite.targetForms[formIndex].newContent.length; i++) {
            fileCreateInfo.get_content().append(targetSite.targetForms[formIndex].newContent.charCodeAt(i));
        }

        var newFile = list.get_rootFolder().get_files().add(fileCreateInfo);
        clientContext.load(newFile);
        clientContext.executeQueryAsync(
            function() {
                console.log('Successfully updated: ' + targetSites[siteIndex].targetForms[formIndex].title + ' on: ' + targetSites[siteIndex].subsiteURL);
                formIndex++;
                controller();
            }, 
            Function.createDelegate(this, errorHandler));
    }

    function errorHandler(sender, args) {
        formIndex++;
        controller();
        console.log(args.get_message());
    }
}

View Repo on GitHub