Add a ScriptLink on the host web from a SharePoint hosted app with JSOM

In my Resizing your add-in showing in a modal dialog I have said that it was not possible to add a ScriptLink from a SharePoint hosted Add-In. But checking my own assumption I found out that it is possible to do so. To get this working the user must have a Full Control permission level * on the host site collection and the Add-In must request FullControl permission on the Site Collection. So this does limit who can add a ScriptLink, and this cannot be done in an Add-In only context, as this is not available for JSOM and SharePoint hosted apps. Also, the Add-In will not be allowed in the App Store, see policy 5.9 in Validation policies for apps and add-ins submitted to the Office Store.

Now you know these limitations, we can now take a look at how to add a ScriptLink to your host web from within the SharePoint hosted Add-In. First we need to initialize a hostContext to make JSOM calls to the host web. A context object was already created in a global variable.

function ensureHostContext(next) {
    if (hostContext) {
        if (next)
            next();
    }
    else {
        var appSiteUrl = _spPageContextInfo.siteAbsoluteUrl;
        var scriptbase = hostUrl + '/_layouts/15/';
        $.getScript(scriptbase + 'SP.RequestExecutor.js', function () {

            var factory = new SP.ProxyWebRequestExecutorFactory(appSiteUrl);
            context.set_webRequestExecutorFactory(factory);
            hostContext = new SP.AppContextSite(context, hostUrl);

            if (next)
                next();
        });
    }
}

In this sample I created methods that contain method parameters, as this makes reading the executed code a whole lot easier, and makes it easier reuse methods. But for now, you can see that you first need to ensure that the SP.RequestExecutor.js script is loaded, and then create a SP.ProxyWebRequestExecutorFactory with the app site url and a SP.AppContextSite with the hostUrl (a globally declared variable in my script). You can find good samples of this at Cross Domain and SharePoint Hosted Apps using CSOM and Work with host web data from JavaScript in the add-in web.

Now we have a hostContext, we can check if the ScriptLink was already added:

function checkScriptLink(name, nextExists, nextNotAvailable) {
    var web = hostContext.get_web();
    var userCustomActions = web.get_userCustomActions();
    context.load(userCustomActions);

    context.executeQueryAsync(function () {
        var enumerator = userCustomActions.getEnumerator();
        while (enumerator.moveNext()) {
            var ca = enumerator.get_current();
            var caName = ca.get_name();
            if (caName == name) {
                if (nextExists)
                    nextExists(ca);
                return;
            }
        }
        if (nextNotAvailable)
            nextNotAvailable();
    }, onCsomFailed);
}

Here you check if a UserCustomAction with a given name is already added to the host web. You can add two follow-up functions one for when the custom action exists, and one if it does not exist. You can see how easy it is to reuse this method in all kinds of contexts, just by adding different follow up actions. I have also declared a onCsomFailed global method to handle all failed queries. Depending on your needs you can also think about adding this function as a parameter.

Next if we haven't got the ScriptLink UserCustomAction, we should ensure that we have a javascript file that we can use to set as the ScriptLink scriptSrc:

function getScriptFileContents(next) {
    $.get('../Scripts/' + scriptFileName, function (data) {
        scriptContents = data;
        if (next)
            next();
    }, 'text');
}

function addScriptFile(urlPart, next) {
    var web = hostContext.get_web();
    var lists = web.get_lists();
    context.load(lists, 'Include(RootFolder)');
    context.executeQueryAsync(function () {
        var list = null;
        var enumerator = lists.getEnumerator();
        while (enumerator.moveNext()) {
            var checkList = enumerator.get_current();
            var listUrl = checkList.get_rootFolder().get_serverRelativeUrl();
            if (listUrl.indexOf(urlPart) >= 0) {
                list = checkList;
                continue;
            }
        }
        if (list != null) {
            var fci = new SP.FileCreationInformation();
            fci.set_overwrite(true);
            fci.set_url(scriptFileName);
            fci.set_content(btoa(scriptContents));
            list.get_rootFolder().get_files().add(fci);
            context.executeQueryAsync(function () {
                if (next)
                    next();
            }, onCsomFailed);
        }
        else {
            logToConsole('List with url part ' + urlPart + ' not found.');
        }

    }, onCsomFailed);
}

We first get the contents of the script file deployed in the app web, this scriptContents is declared as a global variable, but you can also think about sending this as a parameter to the next function. Then if we have the contents, we will create a file in the host site with the given scriptFileName, and set the content of the script in Base64 encoding using btoa. Here I add the file to the SiteAssets library. I use the ServerRelativeUrl to check if it contains /SiteAssets as otherwise I need to worry about language specific list titles.

Next we will add the ScriptLink:

function addScriptLink(name, scriptSrc, next) {
    var web = hostContext.get_web();
    var userCustomActions = web.get_userCustomActions();

    var userCustomAction = userCustomActions.add();
    userCustomAction.set_name(name);
    userCustomAction.set_location('ScriptLink');
    userCustomAction.set_sequence(1001);
    userCustomAction.set_scriptSrc(scriptSrc);
    userCustomAction.update();

    context.executeQueryAsync(function () {
        if (next)
            next();
    }, onCsomFailed);
}

This is pretty straight forward, set the name that we check for, the location, sequence and the scriptSrc. This scriptSrc, if you don't get this right, you can get a nasty error "Cannot make a cache safe URL for" in your host site. That is why you also need functionality to remove the ScriptLink if something is wrong. This is also why I placed the script file in the host sites SiteAssets library, and used '~siteCollection/SiteAssets/Sample.js' as a ScriptLink.

So to remove the ScriptLink:

function removeScriptLink(name) {
    ensureHostContext(function () {
        checkScriptLink(name, function (ca) {
            ca.deleteObject();
            context.executeQueryAsync(function () {
                $('#message').html('<p>ScriptLink Removed</p>' + addParagraph);
            }, onCsomFailed);
        }, function () {
            $('#message').text('ScriptLink not found.');
        });
    });
}

Here I reuse the checkScriptLink (should rather be called checkCustomAction) to check and get back the CustomAction as a parameter and continue with deleting the ScriptLink. Now I think it gets clear why using function parameters makes reusability easy, and it is also much easier when working with async calls.

Now for adding or removing the ScriptLink from the UI, we add some link actions dynamically:

var removeParagraph = '<p><a href="#" onclick="removeScriptLink(\'' + scriptLinkName + '\'); return false;">Click to remove ScriptLink</a></p>';
var addParagraph = '<p><a href="#" onclick="createScriptLink(); return false;">Click to add ScriptLink</a></p>';

function createScriptLink() {
    ensureHostContext(function () {
        getScriptFileContents(function () {
            addScriptFile('/SiteAssets', function () {
                addScriptLink(scriptLinkName, '~siteCollection/SiteAssets/' + scriptFileName, function () {
                    $('#message').html('<p>ScriptLink Added</p>' + removeParagraph);
                })
            })
        })
    });
}
// This code runs when the DOM is ready and creates a context object which is needed to use the SharePoint object model
$(document).ready(function () {
    $('#back').html('<a href="' + hostUrl + '">Back to host site</a>');
    ensureHostContext(function () {
        checkScriptLink(scriptLinkName, function () {
            $('#message').html('<p>ScriptLink Already Set</p>' + removeParagraph);
        }, function () {
            $('#message').html('<p>ScriptLink Not Set</p>' + addParagraph);
        })
    });
});

This makes it clear that the functionality is easy to read, and you have a good overview of what is done in each method, and this without much worry about async calls.

For your convenience you can download the add-in AddScriptLinkSample.app or the full javascript file App_AddScriptLinkSample.js or even download the full solution AddScriptLinkSample.zip. If you try the app, and you can add the ScriptLink succesfully, the script added with the ScriptLink will show a short notification 'The AddScriptLinkSample Works...' on every page loaded.

For a reference on what is available in JSOM I found DefinitelyTyped to be a good resource. You can also just look at the CSOM documentation, and remember that the javascript JSOM methods always start with a lower case character, and properties have a get_ and set_ function followed by the property name also beginning with a lower case character. If you are still not sure your debugger can help you greatly, just open the JSOM object, go to [prototype], then to [Methods], and now you can see what is available. Only look at the human readable methods, those should be the supported ones.

Use Script Debugger

I hope you found this useful.

Update 5/16/2016

One reference I forgot to mention was JavaScript API reference for SharePoint 2013. It's a good reference for knowing what namespaces and objects are available, but the details are lacking, for example properties are lacking the get_ and set_ something you would expect to be there when consulting a JavaScript API reference.

One part of my code is a good candidate for refactoring, namely the 'addScriptFile' method. This method not only adds the file to the list, but is also used to select the list that the file will be added to. These two things are best separated, so you get a function that selects the list, and passes this list to the next function, and a function that adds the file to the list passed to it. Then you could also very easily switch the method that passes a selected list, selected by a part of the url, to for example one, that selects the list by name.

Here is the 'addScriptFile' refactored, I have tested this, but not updated the sample:

function getListByUrlPart(urlPart, next, nextNotFound) {
    var web = hostContext.get_web();
    var lists = web.get_lists();
    context.load(lists, 'Include(RootFolder)');
    context.executeQueryAsync(function () {
        var list = null;
        var enumerator = lists.getEnumerator();
        while (enumerator.moveNext()) {
            var checkList = enumerator.get_current();
            var listUrl = checkList.get_rootFolder().get_serverRelativeUrl();
            if (listUrl.indexOf(urlPart) >= 0) {
                list = checkList;
                continue;
            }
        }
        if(list != null) {
            if (next)
                next(list);
        }
        else {
            if(nextNotFound)
                nextNotFound();
        }
    }, onCsomFailed);
}

function addScriptFile(list, next) {
    var fci = new SP.FileCreationInformation();
    fci.set_overwrite(true);
    fci.set_url(scriptFileName);
    fci.set_content(btoa(scriptContents));
    list.get_rootFolder().get_files().add(fci);
    context.executeQueryAsync(function () {
        if (next)
            next();
    }, onCsomFailed);
}

// then the calling method would be like:

function createScriptLink() {
    ensureHostContext(function () {
        getScriptFileContents(function () {
            var urlPart = '/SiteAssets';
            getListByUrlPart(urlPart, function (list) {
                addScriptFile(list, function () {
                    addScriptLink(scriptLinkName, '~siteCollection/SiteAssets/' + scriptFileName, function () {
                        $('#message').html('<p>ScriptLink Added</p>' + removeParagraph);
                    })
                })
            }, function () {
                $('#message').text('List with url part ' + urlPart + ' not found.');
            })
        })
    });
}

I hope you can see how using function parameters is a flexible way for dealing with reusability, and that it is also making it easier dealing with the async part of JSOM.

Update 6/3/2016

I have noticed an issue with office web apps and javascripts added with a scriptlink. When debugging script for a Word Add-In I noticed error messages ReferenceError: _spBodyOnLoadFunctions is not defined. I also found that also my own scripts gave out these errors. So I have decided to update all samples where I add a scriptlink to include more defensive programming and avoiding the error. I also update the Sample.js scriptlink file here, for good practice and example.

As I updated the files, now the refactored code is also included in the samples.

* To install the app, the user needs to be site collection administrator, to use it to get the expected result, the user needs to have full control permissions.