In my previous blog post Scoped Applications and Client Scripts: A Primer  I point out some of the differences between the APIs available to a scoped client script and a global one. One of the APIs that is explicitly removed is client-side GlideRecord. There are a few reasons that access to this particular API has been removed. A few of the top reasons are:

  • It results in large data transfers
  • Its almost exclusively used to get the value of a single field
  • Almost everyone uses it synchronously
  • It can hold open a database connection

 

I wrote another post about Using utilities in Scoped Applications to replace inaccessible APIs . The example in that case was replacing an auto-generated encoded query string which used an inaccessible API directly within the query. We can expand on that concept, this time on the client rather than the server.

 

UI Script as a utility class

Keeping within the spirit laid out in Background and Philosophy of Scoped Applications  we want a lightweight, asynchronous robust method for making REST calls. Also, we don't want to be limited to just the table APIs. We will create a single UI Script in this post, but this would work with multiple scripts as well.

 

Our client-side scripts (such as onChange and onLoad client scripts) can access the ScriptLoader class. This is an asynchronous JavaScript library that is used for just-in-time loading UI scripts. Thanks to the default structure of our UI Scripts, we can load a UI Script once and access it from multiple places. Additionally, the ScriptLoader knows what it has loaded already, so we can safely ask it to load our utilities multiple times and be sure that it will do so only once.

 

Understanding the UI Script template

To start with, we need a UI Script. When we create a UI Script in our application, we get a template that we can build on. A new UI Script would look like this:

Screen Shot 2016-05-25 at 1.13.08 PM.PNG

 

There are a lot of comments in the code, but it looks a lot more complicated than it is. Before we actually write our UI Script, let's break down what this template is telling us.

 

var sn_ui_script_util = sn_ui_script_util || {};

 

This line creates a variable named sn_ui_script_util, and sets its value to itself, or to an empty object. This is code that runs on the browser, so this is saying If we already have an object in the page named after our scope, we're going to use that. Otherwise, let's create an empty object.

 

sn_ui_script_util.Utilities = (function() {
  "use strict";

 

These two lines add a "Utilities" property to our scope object, and set its value to the result of a function execution. The function itself must run in strict mode. This prevents us from accidentally creating global variables and gives us errors when we make mistakes.

 

/* set your private variables and functions here. For example: 
  var privateVar = 0; 
  function private_function() {
  return ++privateVar;
  }
*/


/* Share variables between multiple UI scripts by adding them to your scope object. For example: 
  sn_ui_script_util.sharedVar = 0; 


 Then access them in your scripts the same way. For example: 
  function get_shared() {
  return sn_ui_script_util.sharedVar;
  }
*/

 

There are three different but connected things going on in the lines above. Firstly, we create a private variable that can only be accessed within this UI Script, and function that changes that variable. Nothing outside of the code written in this UI Script can read the value of that private variable, and nothing can change it.

 

Second, we create another variable, but this one is public. It exists on our scoped object, but it can be accessed by outside scripts. They can read and write to this shared variable, but it's not polluting the global namespace.

 

Lastly, we define a function that will return a reference to our shared variable. This is nothing new or groundbreaking, and since other scripts can access out variable directly, it's not strictly necessary. However, it is impolite to trample someone else's variables and properties, so if they give you a function to access them, you should use that function.

 

    return {
        /* set your public API here. For example:
        incrementAndReturnPrivateVar: function() {
            return private_function();
        },
        */
        type:  "Utilities"
    };

 

 

Here, we are defining the public API. In this example, we are saying that our API is one function named "incrementAndReturnProvateVar". Scripts can call this method and they will increment the value of the private variable we declared earlier, and see the result. We could also add our get_shared function if we wanted to.

 

})();

Finally, we just close out our function definition, and execute it.

 

Creating the utility

So, with that framework, we can create our REST utility. I wrote this today and haven't put a lot of testing into it, but it should work as an example. Note that, while I'm happy to accept fixes and comments about things which might not work with it, I am not offering to support it. This is a demonstration, not a full-fledged solution.

 

var sn_ui_script_util = sn_ui_script_util || {};

sn_ui_script_util.Utilities = (function () {
    "use strict";

    function xhrSupportsJSON() {

        if (typeof XMLHttpRequest == 'undefined') {
            return false;
        }

        var xhr = new XMLHttpRequest();
        xhr.open('get', '/', true);

        try {
            // some browsers throw when setting `responseType` to an unsupported value
            xhr.responseType = 'json';
        } catch (error) {
            return false;
        }

        return 'response' in xhr && xhr.responseType == 'json';
    }

    function getXHR() {
        var xhr = typeof XMLHttpRequest != 'undefined' ?
            new XMLHttpRequest() :
            new ActiveXObject('Microsoft.XMLHTTP');
        return xhr;
    }

    function getParamString(params, q) {
        if (!q)
            q = '';
        else
            q = '?';

        var parts = [];
        for (var key in params) {
            if (params.hasOwnProperty(key))
                parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
        }
        return parts.length ? q + parts.join('&') : '';
    }

    function urlIsRelative(url) {
        return !url.startsWith('http');
    }

    return {

        rest: function (type) {
            var params = {};
            var headers = {};
            var parts = [];
            var successHandler = function (data) {
                try {
                    console.log("Success!");
                    console.log(data);
                } catch (e) {
                    //boo
                }
            };

            var errorHandler = function (data) {
                try {
                    console.log("Error!");
                    console.log(data);
                } catch (e) {
                    //boo
                }
            };

            var obj = {
                get: function (url) {
                    if (!url)
                        errorHandler();

                    url = url + getParamString(params, true);

                    if (urlIsRelative(url))
                        headers['X-UserToken'] = g_ck;

                    var xhr = getXHR();
                    var useJSON = type == 'json' && xhrSupportsJSON();
                    if (useJSON)
                        xhr.responseType = 'json';

                    xhr.open('get', url, true);
                    for (var hdr in headers)
                        if (headers.hasOwnProperty(hdr))
                            xhr.setRequestHeader(hdr, headers[hdr]);

                    xhr.onreadystatechange = function () {
                        var status;
                        var data;

                        if (xhr.readyState == 4) { // `DONE`
                            status = xhr.status;

                            if (status == 200) {
                                data = useJSON ? xhr.response : type == 'json' ? JSON.parse(xhr.responseText) : xhr.responseXML;
                                successHandler && successHandler(data);
                            } else {
                                errorHandler && errorHandler(status);
                            }
                        }
                    };

                    xhr.send();
                },

                post: function (url) {
                    if (!url)
                        errorHandler();

                    if (urlIsRelative(url))
                        headers['X-UserToken'] = g_ck;

                    var xhr = getXHR();
                    var useJSON = type == 'json' && xhrSupportsJSON();
                    if (useJSON)
                        xhr.responseType = 'json';

                    xhr.open('POST', url, true);
                    for (var hdr in headers)
                        if (headers.hasOwnProperty(hdr))
                            xhr.setRequestHeader(hdr, headers[hdr]);

                    xhr.onreadystatechange = function () {
                        var status;
                        var data;
                        if (xhr.readyState == 4) { // `DONE`
                            status = xhr.status;

                            if (status == 200) {
                                data = useJSON ? xhr.response : type == 'json' ? JSON.parse(xhr.responseText) : xhr.responseXML;
                                successHandler && successHandler(data);
                            } else {
                                errorHandler && errorHandler(status);
                            }
                        }
                    };

                    xhr.send(getParamString(params));
                },

                addParam: function (name, value) {
                    params[name] = value;
                },

                addHeader: function (name, value) {
                    headers[name] = value;
                },

                success: function (successCallback) {
                    successHandler = successCallback;
                },

                error: function (errorCallback) {
                    errorHandler = errorCallback;
                }
            }

            return obj;
        },
        type: "Utilities"
    };
})();

 

What?

OK, so that is a lot of code. But you should be able to break it down, using the explanation provided earlier. This is a UI Script with some private functions and variables, which returns a public API that has one method named 'rest'. When we call that, we get an object that allows us to make GET and POST requests, and to accept either XML or JSON. All the rest is just "the stuff that makes that work".

 

Using our Utility

Now that we have a utility defined, we want to make use of it. We have a fairly generic way to make REST requests, and we want to replace at least some of the functionality of client-side GlideRecord. Let's use a common example: get the value of a single field off a referenced record. Let's get the Department of an incident caller, and add that to the work notes of an incident. We'll do that onChange of the Caller field.

 

Screen Shot 2016-05-25 at 3.29.33 PM.PNG

Here is the code we are using:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        return;
    }
    ScriptLoader.getScripts('sn_ui_script_util.Utilities.jsdbx', getDepartment);

    function getDepartment() {
        var req = sn_ui_script_util.Utilities.rest('json');
        req.addParam("sysparm_query", "sys_id=" + newValue);
        req.addParam("sysparm_fields", "department");
        req.addParam("sysparm_display_value", true);
        req.success(updateNotes);

        req.get("/api/now/table/sys_user");
    }

    function updateNotes(data) {
        g_form.setValue("work_notes", data.result[0].department.display_value);
    }
}

 

We have the ScriptLoader fetching our utility script for us, and then calling our function to get the value of the caller's department. When that comes back, we'll update the worknotes field.

You can combine multiple UI Scripts under one namespace (your scope object on the client-side), and multiple scripts can call the ScriptLoader to load them. Think of it like saying "I require the REST utility, so if it isn't already loaded, go get it."

 

This is just one example of a way you can write your own utilities which are scope-safe, asynchronous, and take advantage of the power of ServiceNow's built-in REST API's. There is nothing stopping you from calling your own script REST endpoints, too. And with scoped apps and dependencies, you can keep all these nifty scripts and tools in one application, and simply depend on them for your other apps. It's a very handy way to write-once, use-everywhere.