Update on SharePoint and Office Development - 2016 Feb edition

We rotate through different topics and presenters in the Sydney SharePoint user group.  But I finally got my turn to present a developer topic, and I wanted to do a quick primer on all things Office Developer related.

This turned out to be a REALLY complex talk.  I wanted to cover ALL the cool new stuff in Office Dev.  But as it turns out every month (and now, every week) new things come out. 

I ended up with:

  • Brief Introduction to dev.office.com
  • Brief Introduction to PnP
  • Where we have been with SharePoint Add-Ins
  • Where we are going with Office Addins: 
  • NEW API: Microsoft Graph API
  • NEW AUTH: OAuth 2.0 and ADAL(js)
  • NEW TOOLS: New tools with Node, NPM, and Yeoman Generator (YO OFFICE)
  • Build a demo Office Addin that talks to both SharePoint Online and Microsoft Graph
  • Run on web and desktop

The presentation clocked in at 1 hour and 20mins - I started a bit earlier.  And kind of flopped off at the end as I run out of steam after the demo :-O

Then we gave out all the swag from dev.office.com so all is happy faces.

The PowerPoint presentation is here:

2015 February Presentation - Update on SharePoint and Office Development

 

Demo Fail

So at the end, the Office Add-In didn't load from the App Catalog on my desktop.  I went home and got it to work, here are screenshots to proof it.

Oh NO - no apps

Add SharePoint App catalog to trust center

 

Here it is.  The same addin working on desktop - talking to Microsoft Graph and showing Group Conversations

 

Set Window Title in Nintex Forms for O365

Disclaimer.  This post is about an unsupported hack.

Over at Nintex Community (registration required), there was a question about how to change the Nintex Form for Office 365 window title.  So a user can differentiate the different forms.  Here's a picture that shows the problem.

My buddy Dan Stoll poked me all the way from America in some ungodly hour.  But in my delirious state, I still remember I wanted to write about window.postMessage this year and the problem seems related.

So I went and did some

Investigative Digging

My Office 365 Form is hosted on

https://johnliu365-724b9f945a8c4d.sharepoint.com/FormsApp/NFLaunch.aspx?...

The form part itself is hosted on

https://formso365.nintex.com/Pages/FormsPart.aspx?...

Because the domains are different - JavaScript within the form can not affect the parent window.  So, as noted in the community thread:

window.parent.document.title = "test"; //wouldn't work

To go beyond IFrames, we need to look for PostMessage - this method allows javascript from any frame to send messages to another frame.  The drawback?  The parent page has to be listening to it - otherwise it'll just be discarded.

So I find the FormsPart (child) page, has this function

PostMessageToParent("openWindow", urlToOpen);

That posts json messages to the window.parent frame.  In the form:

{
  action: “openWindow”,
  data: “urlToOpen”
}

And NFLaunch.aspx (parent) page has these functions:

// first receive here
var onmessage = function (e)...

// then here
var SPAppIFramePostMsgHandler = function(e) ...

The function seems to only handle resize.  But I have an idea how to go on.

Hack

Remember, you can only hack what you have control over.  In this case, you have your own Tenant. 

1. Pop open NFLaunch.aspx
Tip:  go to https://johnliu365-724b9f945a8c4d.sharepoint.com/FormsApp/_catalogs/masterpage/
Then you can open in explorer via WebDAV
Then in explorer, navigate back up to root


2. Insert this helper function to 'listen' to a particular setTitle message.

//take a peek at the data send up, handle "setTitle"
//is totally unsupported by nintex
//read about this hack at http://johnliu.net/blog/2015/12/set-window-title-in-nintex-forms-for-o365 
var peekData = JSON.parse(e.data);
if (peekData && peekData.action == "setTitle") {
    document.title = peekData.data;
}
//end peeking

3. Save NFLaunch.aspx

Now back in the Nintex Form Designer

4. Send messages from the child to the parent

Result

Check out the Window Title!

Summary

  • Nintex Forms already do a bit of messaging between Frames
  • We piggyback it to send our own message
  • Needs a small unsupported hack in the parent form
  • If child form sends message that nobody understands at the parent level, nothing would happen.  So the hack should be quite safe.

Happy New Years

On the last day of 2015, I hope the year has been good to you.  Here's to an amazing 2016 and I'm sure there will be much joy from Microsoft, SharePoint and partners like Nintex and us at SharePoint Gurus.

 

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.

Thinking with JS Promise and Promises

 

Here's a real life example of a quick design iteration that we went through with promises this week.

In AngularJS (but this applies to any JavaScript), we have a function that calls the server to do a long running process.

function longProcess() {
    // dataservice returns a promise and we return that to our caller.
    return dataservice.longservice();
}

As this could potentially take nearly a minute, we want to add a waiting dialog.

AngularJS UI provides a modal dialog service $modal

function longProcess() {
  // create dialog
  var dialog = $modal.open({ templateUrl: 'wait.html' });

  var promise = dataservice.longservice();

  promise.then(
    function(){
      // success
      dialog.close();
    },
    function() {
      // fail
      dialog.close();
    }
  );
  return promise;
}

 

This works great.
But sometimes, the dialog flashes past too fast!  While the user is still trying to read it, it disappears.

Before we run off into code, let's stop a do a bit of thinking.

What we need is to combine the original promise, with a second new timer promise (of say 5seconds).  When both the service call has finished and the timer is up, we will resolve the promises.

So in pseudo code, it'd look like:

var p1 = service();
var p2 = $q.timer(5000);
var all = $q.all([p1,p2]);
all.then( /* resolve */ );

Note: $q is AngularJS' lightweight implementation of a Promise/A library Q.  The pseudo-code assumption was that $q would provide a timer-based promise that detonates once time is up.  In reality, $q doesn't provide such a method.  But the $timeout service in AngularJS returns a promise for exactly this scenario.

 

function longProcess() {
  var dialog = $modal.open({ templateUrl: 'wait.html' });

  var p1 = dataservice.longservice();
  var p2 = $timeout(angular.noop, 5000);
  var arrayOfPromises = [p1, p2];
  var promises = $q.all(arrayOfPromises);
  promises.then(
    function(){
      // success
      dialog.close();
    },
    function() {
      // fail
      dialog.close();
    }
  );

  // returns a combined promise to our caller.
  return promises;
}

 

Summary

The dialog opens and shows wait.html - it closes when the service is complete and when the timer has hit 5 seconds.