The easiest way to add Script and Brand your SharePoint and SharePoint Online
/This is the first of a few blog posts I wanted to cover in my 2015 Christmas break on the future of branding.
I'll probably devote the next article to compare the various different ways to brand your SharePoint site - masterpage? CustomCss? On-premises or Online? Would the technique work in SharePoint 2010? What about display templates or JSLink?
But I want to march forward and cover what I think is the safest way to brand your SharePoint right now:
Inject JavaScript to any Site or Site Collection via a User Custom Action's ScriptLink property.
This is a great technique because it lets you:
- Add JavaScript anywhere - scoped to Site Collection or Site.
- Add CSS via JavaScript
- You can add more than 1 CSS file
- Order them the way you want via Sequence
- You can combine this to load your initial JavaScript file which can be a RequireJS setup and then hand off the controls to RequireJS config
- Does not modify MasterPage
- Works in SharePoint 2010, 2013, 2016 and SharePoint Online
- Only need Site Collection permissions to set up - you don't need to have a Farm Solution or Add-In Model. The permission is only required to set up the ScriptLink.
- The object model provides a way for an administrator to check all the User Custom Actions attached to any site/site collection, so there's a level of oversight available if you want to check if your customizations are ready for migration.
There are various ways to attach a script via User Custom Actions.
- Remote Provisioning (Pattern and Practice) uses it via C# CSOM
- PowerShell remote provisioning
- Farm Solution can invoke the API
- Sandbox Solution can invoke the Client Side Object Model API (*with permission)
- Add-In can invoke the CSOM API as well (with Site Collection - Full Control permission)
The unfortunate part is, there's no UI for a power user to add or view ScriptLinks directly. You need to spin up SharePoint Manager or read it via PowerShell.
And that brings me to today's post.
I build a simple config page in JavaScript.
Then I did a load of work to make sure everything runs from One Page.
How does it work?
- Drop it into a SiteAssets or SitePages library.
- he JavaScript on the page detects and loads some dependencies (jQuery, SP.js etc).
- Provided you have site collection permissions, it'll list all existing User Custom Actions
- You can specify a filename (including any subfolders like spg/hello.js) and give it a sequence number (default to 1000). Then you can install a Custom Action to Site Collection or Current Web. All via the magic of JavaScript.
I also brand it to look a bit like SharePoint. Just a bit.
This is still a developer blog. So we're going to talk about code:
function listUserCustomAction(siteOrWeb) { siteOrWeb = (siteOrWeb=="site"? "site":"web"); // ajax call to userCustomActions and order by Sequence // this function can do either _api/site or _api/web var p1 = $.ajax({ url: hostweburl + "/_api/"+siteOrWeb+"/userCustomActions?$orderby=Sequence", dataType: "json", contentType: 'application/json', headers: { "Accept": "application/json; odata=verbose" }, method: "GET", cache: false }); p1.then(function (response) { // use jQuery to do a bit of simple UI update $("ul#"+siteOrWeb+"-user-custom-actions").empty(); $.each(response.d.results, function (i, result) { $("ul#"+siteOrWeb+"-user-custom-actions").append( "<li>" + " [" + result.Location + "] " + (result.Title || result.Name || "") + " ScriptSrc=" + result.ScriptSrc + " Sequence=" + result.Sequence + "</li>"); }); }); return p1; }
First function. listUserCustomAction will show existing Custom Actions attached to either the site collection or the current web. Interestingly - this will also list custom actions attached by other solutions you may have installed in your farm.
spg.installUserCustomAction = function(siteOrWeb) { // switch to JSOM to install userCustomActions var webContext = SP.ClientContext.get_current(); var userCustomActions; if (siteOrWeb == "site") { userCustomActions = webContext.get_site().get_userCustomActions(); } else { userCustomActions = webContext.get_web().get_userCustomActions(); } webContext.load(userCustomActions); // read srcurl and sequence from textboxes srcurl = $("#scriptlink-name").val(); srcsequence = parseInt($("#scriptlink-sequence").val()) || 1000; var action = userCustomActions.add(); action.set_location("ScriptLink"); action.set_title(srcurl); // my tool always attach script from ~sitecollection/SiteAssets/ // you can use subfolders // but if you want to use Style Library or some other // folder you'll have to change this. action.set_scriptSrc("~sitecollection/SiteAssets/" + srcurl); action.set_sequence(srcsequence); action.update(); webContext.load(action); // my SP.ClientContext has a special promise enabled callback // lets me pipe one promise to the next return webContext.executeQueryPromise().pipe(function () { return spg.listUserCustomActions(); }); };
Reminder: http://johnliu.net/blog/2015/12/convert-sharepoint-jsoms-executequeryasync-to-promise-in-the-prototype I wrote previously how to change SharePoint JSOM's ExecuteQueryAsync to just return a Promise. This is important, because you can do stupidly fun things like chaining one asynchronous callback to another one and don't even need to pull out your hair.
spg.uninstallUserCustomAction = function(siteOrWeb) { var webContext = SP.ClientContext.get_current(); var userCustomActions = webContext.get_site().get_userCustomActions(); if (siteOrWeb == "site") { userCustomActions = webContext.get_site().get_userCustomActions(); } else { userCustomActions = webContext.get_web().get_userCustomActions(); } webContext.load(userCustomActions); srcurl = $("#scriptlink-name").val(); var p1 = webContext.executeQueryPromise(); var p2 = p1.pipe(function () { var i = 0, count = userCustomActions.get_count(), action = null; for (i = count - 1; i >= 0; i--) { action = userCustomActions.get_item(i); // look for any script that has the same url as yours // and delete only those if (action.get_scriptSrc() == "~sitecollection/SiteAssets/" + srcurl) { action.deleteObject(); } } // more chaining promise fun return webContext.executeQueryPromise().pipe(function () { return spg.listUserCustomActions(); }); }); return p2; };
So here we are. A page that you can use to install and uninstall ScriptLinks from SharePoint Online and SharePoint 2013/2016.
* SharePoint 2010 needs a few tweaks with the _layout/15 path. Also, the SP.UI.ChromeControl doesn't work so the page will have to look simpler.
What does the result look like?
Here's what it looks like in a browser Network tab
They are not found because they aren't really there! But you can see it's looking for the files in ~sitecollection/siteassets/hello.js
And the Sequence is important. I have hello1.js at 999 and hello.js at 1000, here's what they look like in the <head> tag of the SharePoint page.
SharePoint inserted them into the page at runtime, and I never touched the MasterPage. And that's a big win.
Download
https://github.com/johnnliu/UserCustomActionsConfigPage/tree/master/dist