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:

  1. Asynchronously Loads a UI Script: Loads a UI Script that contains shared functions.
  2. Extracts Variable Values: Retrieves values from multiple list collector variables from the form.
  3. Constructs a JSON Payload: Builds a JSON payload with the selected variable values.
  4. Initiates GlideAjax Call: Performs a GlideAjax call to a server-side script to process the JSON data and retrieve backfilled values.
  5. 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

  1. 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.
  2. Initial Check:

    if (isLoading || newValue == '') {
        return;
    }
    
    • This check exits the script immediately if the form is loading (isLoading is true) or if the newValue is empty (newValue == ''), to prevent the script from doing work during initialization or when a field is cleared.
  3. 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 using ScriptLoader.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 function scriptLoaderCallback() 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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 the getXMLAnswer 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.
  8. 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 populate countries, and the name of the source object u_countries. This function is expected to process the XML and update the specified client-side field.
  9. 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:

  1. Detect Changes: Monitor a field (not explicitly stated) in a catalog item form for changes.
  2. Load a UI Script: Asynchronously load a specific UI Script that has helper functions.
  3. Extract Data: Extract values from three List Collector type variables on the form.
  4. Format Data: Transform the data from the List Collectors into a JSON format using the hsBuildLocationJSON function from the loaded UI Script.
  5. Call Server-Side Logic: Send the formatted JSON data to a server-side Script Include called HSLocationAjax and execute the method getBackfill using GlideAjax.
  6. 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 to hsSetLocationVariable.

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 or gs.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 the getXMLAnswer 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 and console.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 name HSLocationAjax exists, is correctly configured, and has a method called getBackfill which processes the JSON input.
  • HSBuildLocationJSON and hsSetLocationVariable: Confirm that those functions exists in the UI Script HSLocationBackfill10.jsdbx and are doing what is expected.
  • g_form prefix: Note that the g_form object is only available client side. It should not be used on the server side.
  • Client Side Scope: ScriptLoader and alert 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.