Contents
This blog will teach you how to write UI Scripts for Configurable Workspaces using the correct syntax.
Greetings Script
I created a script to greet and farewell my users. I wanted it to be reusable, so I made a UI script.
var sn_nerd = sn_nerd || {}; sn_nerd.greetings = (function() { function getFullName() { return g_user.fullName; } return { hello: function() { g_form.addInfoMessage("Hello " + getFullName()); }, goodbye: function() { g_form.addInfoMessage("Goodbye " + getFullName()); }, type: 'Greetings' }; })();
An onLoad Client script fires off the greeting:
And an onSubmit Client Script says farewell:
These work great in the Classic UI (i.e. not a Workspace, which you navigate to via the Workspaces menu in NextExperience).
However, my users have recently migrated to a Configurable workspace, and my scripts no longer work! I must fix this so my users feel welcome wherever they are!
First, look at how UI Scripts work in the Classic UI.
Classic UI
Recommended UI Script Format
ServiceNow suggests using the following syntax when creating UI scripts in an application scope:
var sn_sow_itsm_cont = sn_sow_itsm_cont || {}; sn_sow_itsm_cont.myscript = (function() { "use strict"; /* 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_sow_itsm_cont.sharedVar = 0; Then access them in your scripts the same way. For example: function get_shared() { return sn_sow_itsm_cont.sharedVar; } */ return { /* set your public API here. For example: incrementAndReturnPrivateVar: function() { return private_function(); }, */ type: "myscript" }; })();
The same default UI script pattern can also be applied to Global scope scripts, which I have used for my Greetings UI Script.
But how do we use these scripts? It is generally considered a leading practice to leave the global checkbox unticked so the script is only loaded in the context in which it is required, resulting in faster page load times. In this case, we need to write code to load the script.
Calling UI Scripts via Client Scripts
One or more UI Script can be called by using the ScriptLoader API.
My onLoad Client Script to load and run my greeting can be seen below:
function onLoad() { ScriptLoader.getScripts("Greetings.jsdbx", sayHello); function sayHello() { sn_nerd.greetings.hello(); } }
Once the script is loaded, it can be called in other Client Scripts, such as my onSubmit:
function onSubmit() { // Show goodbye message to user sn_nerd.greetings.goodbye(); }
Configurable Workspaces
Calling UI Scripts via Client Scripts
For Client Scripts to run in Configurable Workspaces, you must set the UI Type to Mobile/Desktop Portal or All. They are also loaded using the g_ui_scripts API, which works similarly with a few differences:
- You can only load one script simultaneously.
- The script is returned as a parameter to the asynchronous function.
Our Client Script ends up looking something like this:
The script still won’t work for multiple reasons that we will return to later.
Firstly, you may observe the following error in your browser console:
Recommended UI Script Format
For UI Scripts to run in Configurable Workspaces, you must set the UI Type to Mobile/Desktop Portal or All. Since the UI Script formats for both are entirely different, I suggest leaving it as Mobile/Desktop Portal.
Your scripts must also follow a different UI Script format and return a function or an object, as shown below:
// UI script - myUIScript (function() { return { myUIScriptMethod: function() { alert("This is an alert."); } }; })();
The best way to achieve this is through a Self-Invoking function or an Immediately Invoked Function Expression (IIFE), which will execute automatically when the script is first loaded.
Converting my UI script into an IIFE looks like this:
(function() { var sn_nerd = sn_nerd || {}; function getFullName() { return g_user.fullName; } return { hello: function() { g_form.addInfoMessage("Hello " + getFullName()); }, goodbye: function() { g_form.addInfoMessage("Goodbye " + getFullName()); }, type: 'Greetings' }; })();
UI Scripts in Configurable Workspaces are run in a sandbox mode, also wrapped with another anonymous function to protect it from the outside world. This means that global ServiceNow APIs such as g_form and g_user are no longer available to you, and the declared sn_nerd is unavailable to client scripts. We need to return a function that lets you initialise the existing script and pass in the required globals to fix this.
This makes a lot more sense when you look at the actual script that ServiceNow is running:
(function anonymous(window, document) { return (function() { /* YOUR UI SCRIPT */ }).call(this); })
Our UI Script now looks like this:
(function() { var Greetings = function(/*Your globals here */g_form, g_user) { var sn_nerd = sn_nerd || {}; sn_nerd.greetings = (function() { function getFullName() { return g_user.fullName;; } return { hello: function() { g_form.addInfoMessage("Hello " + getFullName()); }, goodbye: function() { g_form.addInfoMessage("Goodbye " + getFullName()); }, type:'Greetings' }; })(); return sn_nerd; }; function initialiseScript(g_form, g_user) { // Return intialise script to client script so globals can be passed in globalThis.sn_nerd = Greetings(g_form, g_user); } return initialiseScript; })();
globalThis is a way of accessing the global object so we can use our API across our client scripts.
It is worth noting that you may not wish to set your API to globalThis for other use cases where your script is only being called in the one client script.
Our greeting code should be working again with some changes to our onLoad Client Script!
function onLoad() { g_ui_scripts.getUIScript('Greetings').then(function(script) { script(g_form, g_user); sayHello(); }); function sayHello() { sn_nerd.greetings.hello(); } }
Or not!
Beware differences in API
UI Scripts in Configurable Workspaces don’t throw errors like in the Classic UI. This makes debugging code very difficult, as the code will often ‘not work’ without good reason. In this case, this issue was with a difference in API.
While we now have access to g_user in our script, both have slightly different API when compared to the different user interfaces. For example (and for reasons beyond me) the g_user API in Configurable Workspaces does not have the full_name property. It does, however have the function getFullName(), so we need to use that instead. There are some differences in the g_form API too that are not relevant for this example, but may be relevant for yours!
TLDR
That was a lot to take in!
If you want to know how to do it for yourself without going through the journey, follow these steps:
- Take a copy of your existing UI Script and change UI Type to Mobile/Service Portal.
- Wrap your current code inside a function with the required globals passed in as parameters, declared as a variable with the same name as your UI Script.
- Create a function called initialiseScript to initialise your script with the required globals passed in as parameters, which sets your existing API to the above variable.
- Make any required changes to account for differences in API of ServiceNow globals.
- Wrap all of the above into an Immediately Invoked Function Expression that returns the initialise function.
- Create a new Client Script with UI Type of Mobile/Service Portal and use the g_ui_scripts API to load the script.
- Call the returned script, passing in the required globals.
(function() { var UIScriptName = function(/*Your globals here */g_form, g_user) { // COPY UI SCRIPT HERE }; function initialiseScript(g_form, g_user) { // Return intialise script to client script so globals can be passed in globalThis.old_api_name = Greetings(g_form, g_user); } return initialiseScript; })();
I’ve set out the steps in a particular way so they could be adapted to work in both Classic UI and Configurable Workspaces using the same Client Scripts (but they won’t quite yet).
I will outline exactly why and how to do this in Part 2!