Client‐Side Data Backfilling with UI Scripts and GlideAjax - ben-vargas/servicenow-wiki GitHub Wiki
I cannot take credit for this design, I noted this code approach because it was different than typical onChange Client Scripts.
This article explains how to use a ServiceNow client script in conjunction with a UI Script and GlideAjax call to dynamically backfill data on a catalog item form. This is especially useful for managing complex relationships between variables, such as locations, geographies, and countries, where selections in one variable need to update other related fields.
The Challenge
Service Catalog forms often require complex interdependencies between variables. When a user makes a selection in one variable (e.g., a geography), you might need to automatically populate related variables (e.g., countries and locations) based on the initial selection. Without a targeted method for doing this, updating these related variables can be complex, cumbersome, and inefficient.
The Solution
This solution employs a client script triggered onChange
that:
- Asynchronously Loads a UI Script: Loads a UI Script that contains shared functions.
- Extracts Variable Values: Retrieves values from multiple list collector variables from the form.
- Constructs a JSON Payload: Builds a JSON payload with the selected variable values.
- Initiates GlideAjax Call: Performs a GlideAjax call to a server-side script to process the JSON data and retrieve backfilled values.
- Updates List Collector: Updates a list collector variable on the form with the data from the server-side script.
Client Script
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') {
return;
}
// Load the HSLocationBackfill UI Script, runs asynch using a callback.
try {
ScriptLoader.getScripts('HSLocationBackfill10.jsdbx', scriptLoaderCallback);
} catch (e) {
console.error(e);
}
// Implement logic with HSLocationBackfill functions in scope.
function scriptLoaderCallback() {
try {
// Get the values for the 3 List Collectors we need to check.
var geographies = g_form.getValue('variables.geographies');
var countries = g_form.getValue('variables.countries');
var locations = g_form.getValue('variables.locations');
// Build the JSON used by the server-side backfill logic.
var locJSON = hsBuildLocationJSON(geographies, countries, locations);
console.log('locJSON: ' + locJSON);
// Invoke the server-side logic to run the backfill.
var ga = new GlideAjax('HSLocationAjax');
ga.addParam('sysparm_name', 'getBackfill');
ga.addParam('sysparm_locationJSON', locJSON);
ga.getXMLAnswer(function(answer) {
if (answer) {
console.debug('answer: ' + answer);
// Set the countries List Collector variables, params: JSONString, target_variable, source_obj
hsSetLocationVariable(answer, 'countries', 'u_countries');
}
});
} catch (e) {
console.error(e);
}
}
}
Detailed Explanation
-
function onChange(control, oldValue, newValue, isLoading) { ... }
:- This is a client script's
onChange
function that is triggered when a specified field's value changes. The script is intended to load an external script, then retrieve and update the field values.control
: An object referencing the UI element that was changed on the form.oldValue
: The previous value of the field.newValue
: The new value of the field that triggered the script.isLoading
: A boolean indicating if the form is currently loading.
- This is a client script's
-
Initial Check:
if (isLoading || newValue == '') { return; }
- This check exits the script immediately if the form is loading (
isLoading
istrue
) or if thenewValue
is empty (newValue == ''
), to prevent the script from doing work during initialization or when a field is cleared.
- This check exits the script immediately if the form is loading (
-
Asynchronous UI Script Loading:
try { ScriptLoader.getScripts('HSLocationBackfill10.jsdbx', scriptLoaderCallback); } catch (e) { console.error(e); }
- The code attempts to asynchronously load a UI script named
HSLocationBackfill10.jsdbx
usingScriptLoader.getScripts
. This is done to make the code modular, and to separate common functions into another file. ScriptLoader.getScripts
loads the script, and then executes the functionscriptLoaderCallback()
when the load is complete.- The
try...catch
block is used to prevent the script from breaking if the UI Script fails to load, and to log an error to the browser console if one occurs.
- The code attempts to asynchronously load a UI script named
-
scriptLoaderCallback()
Function:- This callback is triggered once the UI Script has loaded and is ready for use.
- The backfill logic is contained within this function, and has an additional try/catch to prevent further errors.
-
Retrieve Form Values:
var geographies = g_form.getValue('variables.geographies'); var countries = g_form.getValue('variables.countries'); var locations = g_form.getValue('variables.locations');
- These lines use
g_form.getValue()
to obtain the values from three List Collector type variables, using their variable names.
- These lines use
-
Construct JSON Payload:
var locJSON = hsBuildLocationJSON(geographies, countries, locations); console.log('locJSON: ' + locJSON);
- This line calls a function called
hsBuildLocationJSON()
from the loaded UI Script, passing the values from the list collectors, which is expected to build a JSON object representing the locations, and then logs the output to the browser console.
- This line calls a function called
-
GlideAjax Call:
var ga = new GlideAjax('HSLocationAjax'); ga.addParam('sysparm_name', 'getBackfill'); ga.addParam('sysparm_locationJSON', locJSON); ga.getXMLAnswer(function parseAnswer(answer) { ... });
- This code initiates a GlideAjax call to the
HSLocationAjax
Script Include, using thegetXMLAnswer
to process the results as XML data.ga.addParam('sysparm_name', 'getBackfill');
: sets the method name to be called in the Script Include.ga.addParam('sysparm_locationJSON', locJSON);
sets the parameter for the location data created in the previous step.ga.getXMLAnswer(function parseAnswer(answer) { ... });
: sends the GlideAjax call to the server side and provides the name of the function to be called on response.
- This code initiates a GlideAjax call to the
-
parseAnswer()
Function:if (answer) { console.debug('answer: ' + answer); // Set the countries List Collector variables, params: JSONString, target_variable, source_obj hsSetLocationVariable(answer, 'countries', 'u_countries'); } });
- This function is the callback that processes the server-side response.
- It checks to see if the response (answer) has a value, and if so:
- It logs the answer to the console using
console.debug
. - It calls a function called
hsSetLocationVariable
from the loaded UI Script, passing in the server response, the name of the client-side variable to populatecountries
, and the name of the source objectu_countries
. This function is expected to process the XML and update the specified client-side field.
- It logs the answer to the console using
-
Error Handling:
- Each of the main logic steps are wrapped in try/catch blocks to prevent errors from propagating and to log the error to the console in the event of a failure.
Overall Functionality
This code snippet is designed to:
- Detect Changes: Monitor a field (not explicitly stated) in a catalog item form for changes.
- Load a UI Script: Asynchronously load a specific UI Script that has helper functions.
- Extract Data: Extract values from three List Collector type variables on the form.
- Format Data: Transform the data from the List Collectors into a JSON format using the
hsBuildLocationJSON
function from the loaded UI Script. - Call Server-Side Logic: Send the formatted JSON data to a server-side Script Include called
HSLocationAjax
and execute the methodgetBackfill
usingGlideAjax
. - Process Results: Process the response from the server, which is returned as XML, and use it to update the list collector variable of
countries
, via the function call tohsSetLocationVariable
.
Potential Improvements
- Explicit Error Handling: While there are
try...catch
blocks, consider providing more user-friendly messages if something fails. It also uses console logging which is better for debugging rather than actual user facing errors. - Data Validation: Add validation for the structure of data coming from the server to ensure it has the data that you are expecting.
- Type Checking: Make sure that type checking is implemented on the data you are pulling from the form.
- Input sanitization: Always sanitize data passed from the client to ensure no bad data makes it into the system.
- Logging: Add more logging using
gs.info
orgs.debug
to provide greater insight into the runtime execution of the code, as this will help with troubleshooting. - Code Clarity: Adding more detailed comments to the code would improve overall understanding and maintainability.
- Code Duplication: It is common to see an
if (answer)
check before calling a callback. This logic should be added to thegetXMLAnswer
callback to ensure that the callback is only ever executed when a valid value is returned from the server.
Best Practices
- Asynchronous Loading: Using
ScriptLoader.getScripts
ensures that the UI script is loaded asynchronously, preventing UI blockage during load. - Error Handling: The use of
try...catch
blocks in all the main sections ensures robust handling of errors and prevents issues. - Clear Logging: The script uses
console.log
andconsole.debug
for client-side logs which helps when debugging. - Modular Code: Separating the backfill logic into a dedicated function and using a UI Script promotes modularity and reusability.
- Server-Side Processing: Using GlideAjax ensures complex processing is performed on the server to minimize performance issues on the client, and utilizes the server side scripting capabilities of ServiceNow.
- Use JSON: Passing data as JSON objects is better than splitting up the parameters, as the JSON object provides a clear way of defining the data passed into the script.
Important Considerations
- UI Script Existence: Ensure that the UI Script
HSLocationBackfill10.jsdbx
is uploaded and accessible to the form. HSLocationAjax
: Ensure that a Script Include with the nameHSLocationAjax
exists, is correctly configured, and has a method calledgetBackfill
which processes the JSON input.HSBuildLocationJSON
andhsSetLocationVariable
: Confirm that those functions exists in the UI ScriptHSLocationBackfill10.jsdbx
and are doing what is expected.g_form
prefix: Note that theg_form
object is only available client side. It should not be used on the server side.- Client Side Scope:
ScriptLoader
andalert
are client side APIs. They should not be called on the server side. - User Feedback: Provide user feedback when loading or processing complex operations, so the user understands something is happening.
Security Considerations
- Input Validation: Make sure to perform proper input validation within the server-side Script Include. This is a great place to sanitize the data being passed from the client to prevent cross site scripting or other issues.
- GlideAjax Scope: Ensure that the GlideAjax call is properly scoped and authorized to access the intended server side data.
Conclusion
This article provides a detailed explanation of a client-side script that leverages UI Scripts and GlideAjax to dynamically backfill data in a ServiceNow Service Catalog form. By combining these methods, you can ensure a more streamlined user experience, and provide flexibility when working with complex data relationships. Remember to always test the code in a non-production environment prior to deploying it to production.