PhantomJS for SharePoint and Office365 at Collab365

Great news everyone!

Collab 365 Global Conference

Collab 365 is a free online conference that's just around the corner - this is run by the team behind last year's successful SP24 conference (which was the craziest 24hours - around the clock and around the world).

https://collab365.conferencehosts.com/SitePages/GlobalConference.aspx

This time, the guys are less crazy and will instead have 12hours spanning over two days.  (So a total of 24 hours).

PhantomJS: Headless Browser for SharePoint and Office 365

I have been having a lot of fun locally presenting this topic in the Office 365 Saturday events around Australia, and will be presenting this online at Collab 365. 

My session is scheduled for 08 October (day 1 - my time), 12PM-1PM.  Please check your local time.  In Australia, this conference spans 08-09 October. 

https://collab365.conferencehosts.com/sitepages/agenda.aspx

PhantomJS is an interesting tool.  It is free - and it is basically browser that you can script to automate many things, without a UI.  I cover scenarios and scripts that will allow you to use it effectively with SharePoint On-Premises and Office 365.

If you are a developer - this is a great tool to add to your toolset.  Even if you aren't - as long as you aren't scared of a bit of scripting, you will still find the scenarios for PhantomJS interesting and, well, different.

There are several other sessions I wanted to catch up on, and plenty of MVPs and Microsoft presenting on all things Office 365. 

I hope to see you at Collab 365!

 

 

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.



AngularJS - Logging Client Side Stack Trace

Nothing beats mythical errors that happens on the client side, but not on your development machine.

This post follows yesterday's post on configuring AngularJS to catch and log exceptions back to the server via an AJAX request.

Logging the Exception

Quick recap of the logging error handler.

function error(message, data, title) {
  var $http = angular.injector(['ng']).get('$http');

  data['url'] = document.location.href;

  var request = {
    method : 'POST',
    url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyService.svc/LogMessage',
    data: {
      'log': message,
      'meta': angular.toJson(data),
      'evt': 'error'
    },
    dataType: "json",
    headers: {
      "Content-Type": "application/json; charset=utf-8"
    }
  };
  var promise = $http(request);            

  promise['catch'](function(){
    // send to console log if can't log to webservice
    $log.error('Error: ' + message, data);
  });
}
{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'Client' of undefined or null reference",
    "description":"Unable to get property 'Client' of undefined or null reference",
    "number":-2146823281
  },
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=3#/client"
}

Yay it's logging.  But actually, it's pretty hard to figure out where the error is coming from.  "What we need", says the developer to the tester, "is the stack trace.  If we have the stack trace we'd be able to repo this and fix it."

stacktraceJS

Grab: https://github.com/stacktracejs/stacktrace.js/blob/stable/stacktrace.js

function error(message, data, title) {
  var $http = angular.injector(['ng']).get('$http');
  
  // work out the stacktrace
  var stack = printStackTrace({e: data, guess: true});
  data['stack'] = stack;
  // attach to the logged data

  data['url'] = document.location.href;

  var request = {
    //...
  };
  var promise = $http(request);            
}

The following JSON gets logged.

{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference",
    "description":"Unable to get property 'exist' of undefined or null reference",
    "number":-2146823281
  },
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=138#/",
  "stack":
  [
    "{anonymous}(#object,\"other\")",
    "printStackTrace(#object)",
    "error(\"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference\",#object)",
    "{anonymous}(?)",
    "{anonymous}(#function)",
    "{anonymous}(#object)",
    "{anonymous}(#object)",
    "{anonymous}(?)"
  ]
} 

Tweaks

"Those #function and #object, they look pretty stupid." commented the developer, who now has the stacktrace, but still thinks it doesn't help all that much.

A few hacks to stacktrace.js

if (arg.constructor === Object) {
  //result[i] = '#object';
  result[i] = arg.constructor.toString();
}
...
else {
   //result[i] = '?';
   result[i] = arg.constructor.toString();
}

This gives more readable stacktrace:

{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference",
    "description":"Unable to get property 'exist' of undefined or null reference",
    "number":-2146823281
  },
  "stack":
  [
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n,\"other\")",
    "printStackTrace(\nfunction Object() {\n    [native code]\n}\n)",
    "error(\"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference\",\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}(TypeError)",
    "{anonymous}(#function)",
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}([object Event])"
  ],
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=138#/"
}

 

Future of stacktraceJS

The latest version of StacktraceJS (as of August 2015) is marching towards a full promise/A pattern where you request the stacktrace, and it returns you a promise.  Then promise resolves to an array of stack frames, which you can then pretty-print to your own liking.

var callback = function(stackframes) {
    var stringifiedStack = stackframes.map(function(sf) { 
        return sf.toString(); 
    }).join('\n'); 
    console.log(stringifiedStack); 
};
var errback = function(err) { console.log(err.message); };
StackTrace.get().then(callback, errback)

Expect the syntax to change, soon.

I'm also assuming for modern browsers, it'll even look into requesting SourceMap files for minified versions of JavaScript/TypeScript and resolve those lines in the stacktrace produced.   Currently though, it doesn't do all those things.  What it does manage so far, cross browser, is already fairly impressive and a great starting point.

 

 

AngularJS $http and logger Circular Dependency

Error

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

TLDR:

If you are looking for RequireJS's easy way to late-bind a dependency, here's how you do it in AngularJS.

var $http = angular.injector(['ng']).get('$http');

Story

I imagine this could be a fairly common scenario.

You implemented AngularJS, with a logger service.

 angular
     .module('blocks.logger')
     .factory('logger', logger);

 logger.$inject = ['$log'];
 function logger($log) {
        var service = {
            error   : error,
            log     : $log.log
        };
        return service;

        /////////////////////

        function error(message, data, title) {
            $log.error('Error: ' + message, data);
        }
}

You want to try to catch errors and log it back to your webservice
So add a REST call.

logger.$inject = ['$log', '$http'];
function logger($log, $http) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);

        var request = {
            method : 'POST',
            url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
            data: {
                'log': message,
                'event': 'error',
                'meta': angular.toJson(data)
            },
            dataType: "json",
            headers: {
                "Content-Type": "application/json; charset=utf-8"
            }
        };
        $http(request);           
    }
}


Error!

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

I actually quite like this error.  It's telling me the dependency tree starting from routehelper and looping around to $rootScope

Bingle will tell you to re-architect your solution so you have proper separation of concerns.  But to be honest, I just want to log an error…

So here's how you do it.

var ng = angular.injector(['ng']);
var $http = ng.get('$http');

Final Code Summary

logger.$inject = ['$log'];
function logger($log) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);
        
        // inject $http here.
        var $http = angular.injector(['ng']).get('$http');
        if (!$http) return;
            var request = {
                method : 'POST',
                url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
                data: {
                    'log': message,
                    'event': 'error',
                    'meta': angular.toJson(data)
                },
                dataType: "json",
                headers: {
                    "Content-Type": "application/json; charset=utf-8"
                }
            };
            $http(request);           
        }
}