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?

  1. Drop it into a SiteAssets or SitePages library.
  2. he JavaScript on the page detects and loads some dependencies (jQuery, SP.js etc). 
  3. Provided you have site collection permissions, it'll list all existing User Custom Actions
  4. 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

Related Posts

Convert SharePoint JSOM's ExecuteQueryAsync to Promise in the Prototype

Today's blog is about adding an additional method to SharePoint JavaScript Object Model (JSOM)'s ClientContext object, so we can use it directly like a promise.

I call it "executeQuery" (instead of executeQueryAsync)

Wrapper with jQuery's $.Deferred

SP.ClientContext.prototype.executeQuery = function() {
   var deferred = $.Deferred();
   this.executeQueryAsync(
       function(){ deferred.resolve(arguments); },
       function(){ deferred.reject(arguments); }
   );
   return deferred.promise();
};

Wrapper with AngularJS's $q

SP.ClientContext.prototype.executeQuery = function() {
   var deferred = $q.defer();
   this.executeQueryAsync(
       function(){ deferred.resolve(arguments); },
       function(){ deferred.reject(arguments); }
   );
   return deferred.promise;
};

How do you use this?

var ctx = SP.ClientContext.get_current();
var web = ctx.get_web();
ctx.load(web);
var promise = ctx.executeQuery(); // look!  :-)

promise.done(function(){
  console.log(web.get_title());
});
promise.then(function(sArgs){
  //sArgs[0] == success callback sender
  //sArgs[1] == success callback args
}, function(fArgs){
  //fArgs[0] == fail callback sender
  //fArgs[1] == fail callback args.
  //in JSOM the callback args aren't used much - 
  //the only useful one is probably the get_message() 
  //on the fail callback
  var failmessage = fArgs[1].get_message();
});

This may seem to be just small syntactic sugar, but now you have JSOM returning a promise that you can chain, loop, combine and juggle to your heart's content!

Remember, jQuery.ajax, SPServices, AngularJS, and now JSOM all returns promise objects now.

Racing to the Races - Putting our Office App out there

As I'm posting this blog entry, our (SharePoint Gurus) first Office App (Add-In) would be available on the store.

I might let you in on a secret - it has in fact been in the store in the last few days, but as it is our company's first Add-In, we had some hiccups and had to push out subsequent updates.  We are pleased with this version and we'll run with it to the actual Melbourne Cup race, which actually isn't all that far away.  It would be on November 3, 2015, and the horses list would be available on October 31 - a Saturday, yes that means our Add-In would prompt you to automatically update data on Monday morning.

Feedback through the week from our clients has been very supportive.  This could turn out great (or a great learning experience).  But either way, we have fun and we hope our clients and friends have fun with our App too.

The Team

We are all consultants and this Add-In is something we wanted to build for a long time, but never could tear ourselves away from our great clients to just stop and write this Add-In.

  • We learn AngularJS along the way
  • We became pros at JavaScript Promises... chaining promises, grouping promises, catching error promises and retrying them.
  • Everyone in the company got involved.  We are not a large company, but this one Add-In has 100% contribution from the entire team.
  • We had different people deploying to their own developer sites, both On-Premises and Office 365. 
  • We use TFS but had an open checkout policy (you have to merge any changes).  This turned out not as disastrous as we think, it gave us freedom to work on the project when we can, without having to wait for a certain colleague to check in first.
  • We started the journey a long time ago with Wiki pages, Task lists and Yammer discussion group.  We are now on Office 365 OneNote (available anywhere, offline, and synchronized) and Office 365 Groups for conversation.  We use the Outlook Groups app when we are on the run.  If Office 365 Planner had been available, I'm sure we would be all over it too.  We had a white-board with moving tasks and Post-It notes.

The Stack

The Add-In is a SharePoint-Hosted App. 

Sweepstake Horse says SAAI.&nbsp; Also, horse is sorry he didn't say on-premises

Sweepstake Horse says SAAI.  Also, horse is sorry he didn't say on-premises


The Learning (so far)

  • v2 will be provider hosted.  The complexity would lie in provisioning, and also not all our consultants are fluent with ASPNET MVC or C#
  • The benefits are to do with ease of updating the various components, and hiding core logic.
  • We may tackle NodeJS instead

You

You should download our Add-In and give it a whirl.  Come next Monday, hopefully we hear good things from you.

We already have people asking to do a Rugby World Cup one next year, which would have been fun, this weekend is finals between Australia vs. New Zealand.

Office 365 Saturday 2015 - Melbourne

I visited sunny Melbourne (so strange!) for Office 365 Saturday last weekend, covering PhantomJS and SharePoint.  We also geeked out in general around the lunch desk and debated the direction of Add-Ins and the future of SharePoint development.

It was so good to see many of the locals turn up - familiar faces and new faces.  We geeked out on PhantomJS and why you need to know about this tool for your toolbox, even if you don't need it right now.

We then flowed onto Colin's session on making a million dollars (in progress) in the App Store. 

Downloads to my presentation and zip files.

Melbourne was so enjoyable.  I had to leave early to catch my flight home.  Next stop is Adelaide:

Office 365 Saturday Adelaide - 12 September 2015

http://www.o365saturdayaustralia.com/adelaide

Fixing SharePoint Promoted Link's New Tab Launch Behaviour

There's a bug with Promoted Links web part in SharePoint 2013 and Office 365, and I have no idea how long it's been there.  This post is about a simple Javascript hack that will fix it.

Story

Robert Crane [O365 MVP] found this one.  Out of the blue, he asks me, hey why does the new tabs always open in the same tab.  I thought, no way, it's always been working.  So naturally, I tried it out and check the anchor tag.  Expecting target="_blank"

TLDR

  • Promoted Links spits out target="="_blank"" for who knows how long.
  • Here's a fix.

SharePoint's Promoted Links Web Part

A quick introduction - this is the Promoted Links web part.  If you have seen SharePoint 2013 or Office 365's SharePoint Online, then you have seen this web part.  It is used everywhere for navigation.

Promoted Link is backed by a regular SharePoint list.  Of interest, is the Launch Behavior column, which lets you decide what to do when a tile is clicked.  You can navigate there Replacing Current Page, open in SharePoint's ModalDialog, or open in a New tab.

Open in New Tab

To prevent spammers running Javascript and creating lots of tabs when you visit a page, a browser gives no way for Javascript to create new tabs.  The only way to do this, is natively in HTML, the tag looks like this:  <a href="http://bing.com" target="_blank">B</a>

SP2013 (and Office 365) spits out this:

What would this do?  Because target="="_blank"" is not a name that the browser knows about, it thinks, ah you want a new tab target, with that name.  So the first click is OK, you navigate to that URL in a new tab.

The problem is that if you click multiple Promoted Tiles, they will all be loaded in the same tab, instead of different new Tabs.

IE11 seems OK

Curiously, IE11 creates new tabs.  This however doesn't work on Edge.

Culprit

There seems to be a bug in sp.ui.tileview.js, adding target= into the attribute.

 

Fix

This can be fixed with a small bit of Javascript.

Edit the Page where your promoted list is giving you trouble.  Say the Home Page.  Insert "Embed Code" in a Web Part zone at the bottom of the page.  (This short cut creates a Script Editor Web Part there)

Paste this bit of script.

<script language='javascript'>
function fixATargetBlank(){
    // using sharepoint's mQuery because I don't want to have
    // dependency on jQuery in this quick script
    m$("a[target][clickaction]").forEach(function(a){a.target="_blank";});
}
// add this function to tell SharePoint to run it on load
_spBodyOnLoadFunctionNames.push("fixATargetBlank");
</script>

The Script Editor will appear blank.  That's OK.  Save the page.

Result

The target is fixed to _blank.  And clicking on the promoted links now opens them in separate new tabs, in all browsers.