One Quick Tip for Single Page Apps in Service Portal

Those with a web development background will already understand the difference between a single-page app and a multi-page app. If you’ve ever used Gmail, you’ve used a single-page app. The basic premise is that with a single-page app, a single-page load happens when you visit the application’s URL, and then further content is loaded in by making background HTTP calls and changing the page using JavaScript code.

Single-page apps lead to an great user experience, as only the information that’s needed is transmitted over the network, and the browser only needs to make changes to the area of the page which need to change. This is in contrast to a multi-page app, where the entire content of the page needs to either be transmitted over the network or loaded from cache, and the full page needs to be re-drawn. The benefits of single-page apps are especially advantageous on mobile devices which have limited internet connectivity and less processing power than full desktops.

Service Portal is absolutely a single-page app; however, it has some characteristics which make it behave more like a multi-page app. I’m going to take you through one of these characteristics that I came across recently, why it was causing me an issue, and a simple way to disable it.

Within the header of an application I was building was a chat button. This button, when clicked would toggle a chat box which would slide down from the top of the page to take up the whole screen. This is all done with JavaScript, and was working nicely, however what happens when you reload the page? The chat box isn’t open anymore because the page resets to it’s default state.

When developing single-page apps, these issues can be worked around by storing the state of these types of things in a URL parameter. This means that when the page reloads, the URL parameter can be read and the application can return to that state (e.g. with the chat box open).

In AngularJS, the best way to modify URL parameters is with the $location service, and specifically the search() method on it. To use the $location service you first need to inject it into your widget’s client script like so:

function myWidgetCtrl ($location) {
    var c = this;
    
    // your code here
}

Or within your widget’s link function like so:

function myWidgetLink (scope, elem, attrs) {

    var $location = $injector.get('$location');
    
    // your code here
}

The $location service is also what’s used to change pages in Service Portal, by altering the value of the id URL parameter.

To add our URL parameter to store the state of the chat box, when you click the button to open the chat box we can run this code:

$location.search('myapp_chat', 'true');

You can see we’re using the myapp_chat URL parameter to store the state of the chat box. This can be anything you want, but it’s a good idea to prefix it with something unique to avoid clashes with other URL parameters that may be added/used by other widgets.

In your function to perform the close action for the chat box, you can run this bit of code to remove that URL parameter:

$location.search('myapp_chat', null);

Upon loading the chat widget, you can run this code to see if the URL parameter exists, and show/hide the chat box depending on it’s value.

var chatState = $location.search('myapp_chat');

The issue I ran into with using this approach in Service Portal is that every time I would toggle the state of the chat widget all the widgets on the page would reload! If any of the widgets on the page were modifying data upon load (which they were), every time you toggled the chat box showing that code would be run. This is obviously not something we want!

I started researching why this was happening, and found a post on the community mentioning the spa URL parameter (note that “spa” is short for “single-page app”). If you look through the ServiceNow code in your browser’s developer tools, within js_includes_sp.jsx you’ll see this section of code in there:

$rootScope.$on('$locationChangeSuccess',
        function(e, newUrl, oldUrl) {
            locationChanged = (oldUrl != newUrl);
            var s = $location.search();
            var p = $location.path();
            if (oldPath != p) {
                $window.location.href = $location.absUrl();
                return;
            }
            if (angular.isDefined($scope.containers) && oid == s.id && s.spa) {
                return;
            }

What’s happening on line 10 of the above code excerpt is that if the id parameter doesn’t change, and the spa parameter is set, it will actually abort reloading all the widgets. This is exactly what we need!

So to build a true single-page app in Service Portal, you should ensure that the spa URL parameter is set at all times. This can be done anywhere on your page, or somewhere in an Angular module. For the time being in my application I’m setting it within the header widget of my Service Portal using this line of code:

$location.search('spa', '1');

However I will likely move this into an Angular module somewhere once I expand the application out more.

Thanks for taking the time to read this article, I hope this helps! Feel free to leave feedback in the comments below, or reach out to me on twitter with my handle @dylanlindgren if you have any questions!