kevinanderson
Giga Guru

In my learning the ServiceNow platform as a part of MetLife, one thing I have been tasked with is optimizing various client scripts on our catalog item pages to improve page-load time.   A recent ServiceNow ACE report showed we had a large number of client scripts using the XMLWait function.   My hope is that   the scripts I am developing here will help in that optimization effort.

As I was recently working with a new catalog, I found that I would like to be able to inspect the full list of variables on the page, and since DOM inspection is a no-no in service now, I found myself confronting the need to write some script to query the various tables holding variable info.

My first approach was to try and bootstrap the data in via the g_scratchpad variable.   This seemed like a great idea, but I quickly hit a brick wall due to onDisplay business rules not firing on catalog items.

So I want to share with you the scripts I developed to push my simple variable_pool data structure into the clients.

I have added a few global UI scripts to make my life easier, those being underscore.js, and loglevel.js, both are available on github, and I highly recommend them.

variable_pool_ajax_diagram.png

Script Include — CatalogVariableList.js

  • Performs glide record queries against item_option_new, io_set_item to get variables, and variables sets on the catalog item

/**

  Class that provide glidejax endpoint where catalog client scripts can retrieve a list of all the variables on the form

  @class CatalogVariableList

  @author kevin anderson

  @date 8-26-2015

*/

var CatalogVariableList = Class.create();

CatalogVariableList.prototype = Object.extendsObject(AbstractAjaxProcessor, {

      /**

      process the catalog sysid parameter and fetch the name and sysid for each variable defined on the catalog item

      @method get

      @param {string} cat_item_sysid - note: parameter may also be found in the class getParameter method (ajax call)

      */

      get: function(cat_item_sysid) {

              var json = new JSON();

              var ajax_param = json.decode(this.getParameter('sysparm_data'));

              var result = {

                      error: 'false',

                      message: '',

                      payload: {}

              };

              try {

                      if (ajax_param && this._has(ajax_param, 'catalog_item_sysid') && this._isString(ajax_param.catalog_item_sysid)) {

                              // function called via ajax from client, read parameter from getParameter method

                              cat_item_sysid = ajax_param.catalog_item_sysid;

                      }

                      if (this._isString(cat_item_sysid) && cat_item_sysid.length) {

                              // get the variables defined on the catalog item

                              var list = this._getCatalogVariables(cat_item_sysid);

                              // add the variable sets assigned to this catalog item

                              result.payload = list.concat(this._getCatalogVariableSets(cat_item_sysid));

                      } else {

                              result.message = 'The parameter "catalog item sys_id" is not of type string';

                              result.error = true;

                      }

              } catch (e) {

                      result.message = 'Error occured attempting to retrieve catalog item variable list - ' + e.message;

                      result.error = true;

              }

              return json.encode(result);

      },

      /**

        get the list of variables defined on the catalog item

        @method _getCatalogVariables

        @param {string} sysid of the catalog item to query the variable list for

        @returns {array}

      */

      _getCatalogVariables: function(cat_item_sysid) {

              // query for all catalog_item variables

              var catalog_item_vars_rec = new GlideRecord('item_option_new');

              catalog_item_vars_rec.addNotNullQuery('cat_item');

              catalog_item_vars_rec.addQuery('cat_item', cat_item_sysid);

              catalog_item_vars_rec.query();

              var data = [],

                      variable_info;

              while (catalog_item_vars_rec.next()) {

                      variable_info = {

                              'name': catalog_item_vars_rec.name.getDisplayValue(),

                              'label': catalog_item_vars_rec.getDisplayValue(),

                              'sys_id': catalog_item_vars_rec.sys_id.getDisplayValue(),

                              'set_name': '',

                              'set_sys_id': ''

                      };

                      //gs.log('catalog vars: '+variable_info.label+' : '+variable_info.name+' : '+variable_info.sys_id, '_getCatalogVariables::CatalogVariableList');

                      data.push(variable_info);

              }

              return data;

      },

      /**

        get a list of all the variable sets that belong to a catalog item

        @method _getCatalogSetVariableInfo

        @param {string} cat_item_sysid sysid of the catalog item to query the variable list for

        @returns {array} each array element is an object with the keys "sys_id" and "name"

      */

      _getCatalogSetVariableInfo: function(cat_item_sysid) {

              // get variable sets on the catalog

              var data = [],

                      variable_info;

              var cat_varset_rec = new GlideRecord('io_set_item');

              cat_varset_rec.addNotNullQuery('cat_item');

              cat_varset_rec.addQuery('sc_cat_item', cat_item_sysid);

              cat_varset_rec.query();

              while (cat_varset_rec.next()) {

                      // get the variable set name and sysid and lookup all its related variables

                      variable_info = {

                              'name': cat_varset_rec.variable_set.name.getDisplayValue(),

                              'sys_id': cat_varset_rec.variable_set.sys_id.getDisplayValue(),

                      };

                      if (variable_info.name && variable_info.sys_id) {

                              //gs.log('varset info: '+variable_info.name+' - '+variable_info.sys_id,'_getCatalogSetVariableInfo::CatalogVariableList');

                              data.push(variable_info);

                      }

              }

              return data;

      },

      /**

        get list of all the variables in the variable sets that belong to a catalog item

        @method _getCatalogVariableSets

        @param {array} array of variable sets, each array element is an object with the keys "sys_id" and "name"

        @returns {array}

      */

      _getCatalogVariableSets: function(cat_item_sysid) {

              this._array_polyfill();

              var data = [];

              var variable_sets = this._getCatalogSetVariableInfo(cat_item_sysid);

              gs.log("test array : " + variable_sets.length, '_getCatalogVariableSets::CatalogVariableList');

              variable_sets.forEach(function(element, index, array) {

                      if (this._has(element, 'sys_id') && this._isString(element.sys_id)) {

                              var varset_name = '';

                              if (this._has(element, 'name') && this._isString(element.name)) {

                                      varset_name = element.name;

                              }

                              // query for all the variable set variables

                              var varset_rec = new GlideRecord('item_option_new');

                              varset_rec.addQuery('variable_set', element.sys_id);

                              varset_rec.query();

                              while (varset_rec.next()) {

                                      variable_info = {

                                              'name': varset_rec.name.getDisplayValue(),

                                              'label': varset_rec.getDisplayValue(),

                                              'sys_id': varset_rec.sys_id.getDisplayValue(),

                                              'set_name': varset_name,

                                              'set_sys_id': element.sys_id

                                      };

                                      //gs.log(variable_info.label+' : '+variable_info.name+' : '+variable_info.sys_id+' : '+variable_info.set_name+' : '+variable_info.set_sys_id,'_getCatalogSetVariables::CatalogVariableList');

                                      data.push(variable_info);

                              }

                      }

              }, this); // pass 'this' reference to the array.foreach method

              return data;

      },

      /**

      private method to test if an object contains a   property

      @method _has

      @param {object} obj

      @param {string} prop object key to verify exist in object

      */

      _has: function(obj, prop) { // this function is not client callable      

              var result = false;

              if (typeof obj === "object" && typeof prop === 'string' && obj && prop.length) {

                      if (obj.hasOwnProperty(prop)) {

                              result = true;

                      }

              }

              return result;

      },

      /**

        Is a given variable an object?

        @method isObject

        @param {object} obj

        @link https://github.com/jashkenas/underscore/blob/master/underscore.js

      */

      _isObject: function(obj) {

              var type = typeof obj;

              return type === 'function' || type === 'object' && !!obj;

      },

      /**

        determine if parameter is a string

        @method _isString

        @param {object} str

        @link https://github.com/jashkenas/underscore/blob/master/underscore.js

      */

      _isString: function(str) {

              return Object.prototype.toString.call(str) === '[object String]';

      },

      /**

        polyfill for the array.each method which is an es5 component

        @method _array_polyfill

        @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

      */

      _array_polyfill: function() {

              // Production steps of ECMA-262, Edition 5, 15.4.4.18

              // Reference: http://es5.github.io/#x15.4.4.18

              if (!Array.prototype.forEach) {

                      Array.prototype.forEach = function(callback, thisArg) {

                              var T, k;

                              if (this == null) {

                                      throw new TypeError(' this is null or not defined');

                              }

                              // 1. Let O be the result of calling ToObject passing the |this| value as the argument.

                              var O = Object(this);

                              // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".

                              // 3. Let len be ToUint32(lenValue).

                              var len = O.length >>> 0;

                              // 4. If IsCallable(callback) is false, throw a TypeError exception.

                              // See: http://es5.github.com/#x9.11

                              if (typeof callback !== "function") {

                                      throw new TypeError(callback + ' is not a function');

                              }

                              // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.

                              if (arguments.length > 1) {

                                      T = thisArg;

                              }

                              // 6. Let k be 0

                              k = 0;

                              // 7. Repeat, while k < len

                              while (k < len) {

                                      var kValue;

                                      // a. Let Pk be ToString(k).

                                      //     This is implicit for LHS operands of the in operator

                                      // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.

                                      //     This step can be combined with c

                                      // c. If kPresent is true, then

                                      if (k in O) {

                                              // i. Let kValue be the result of calling the Get internal method of O with argument Pk.

                                              kValue = O[k];

                                              // ii. Call the Call internal method of callback with T as the this value and

                                              // argument list containing kValue, k, and O.

                                              callback.call(T, kValue, k, O);

                                      }

                                      // d. Increase k by 1.

                                      k++;

                              }

                              // 8. return undefined

                      };

              }

      }

});

UI Script — LoadCatalogItemVariablePool.js

  • Provides reusable class to make Ajax call to DB and parse response

/**

  query the DB via glidejax to provide the client a list of all the variables and variable sets

  UI script

  @class LoadCatalogItemVariablePool

  @filename load_catalog_item_var_pool

  Requires underscore.js and loglevel.js

*/

function LoadCatalogItemVariablePool(obj) {

      // ie8 protection

      if (!window.console) {

              console = {

                      log: function() {}

              }

      }

      // incase the loglevel module gets disabled

      if (!window.log) {

              window.log = {};

              window.log.error = console.log;

              window.log.info = console.log;

              window.log.warn = console.log;

      }

      /**

        provide browser namespace for metlife functions

        @namespace metlife

      */

      if (!window.metlife) {

              window.metlife = {};

      }

}

LoadCatalogItemVariablePool.prototype = {

      constructor: LoadCatalogItemVariablePool,

      /**

              contents of the ajax response after json parse and extract

        @property {object} data

 

      */

      data: [],

      /**

              if a server-side db error message is generated, the error message from the ajax response stored here

        @property {string} error_msg

 

      */

      error_msg: '',

      /**

        start the process to query database for the catalog item variables and variable sets

        @method getVariablePool

      */

      getVariablePool: function() {

              try {

                      // set the logging level - for production this should be "disableAll"

                      log.enableAll();

                      /**

                          anonymous function to maintain "this" scope for ajax callback

                          @method callback

                          @param {object} data response from ajax request

                      */

                      var context = this;

                      var callback = function(data) {

                              context.callback_load_variable_pool(data);

                      };

                      var payload = {

                              'catalog_item_sysid': g_form.getParameter("sysparm_id")

                      };

                      log.info("catalog sysid: " + payload.catalog_item_sysid + ' - getVariablePool::LoadCatalogItemVariablePool');

                      // send the catalog item sys_id from the form to the server endpoint to feth all form variable data

                      var ga = new GlideAjax('CatalogVariableList');

                      ga.addParam('sysparm_name', 'get');

                      ga.addParam('sysparm_data', JSON.stringify(payload));

                      ga.getXML(callback);

              } catch (err) {

                      log.error('Error occured: ' + err.message + ' - getVariablePool::LoadCatalogItemVariablePool (UI Script)');

              }

      },

      /**

        ajax callback handler to save the variable pool

        value saved to the form

        @method callback_load_variable_pool

        @param {object} response ajax server reply

      */

      callback_load_variable_pool: function(response) {

              try {

                      var parse_result = this.parseAjaxResponse(response);

                      if (!parse_result) {

                              // parsing failed, sent the stored error message to the error handler

                              throw this.error_msg;

                      }

                      //log.info(this.data);

                      // loosely validate the response data and save to the global namespace

                      if (_.isArray(this.data) && this.data.length && _.isObject(this.data[0]) && _.has(this.data[0], 'sys_id')) {

                              window.metlife.variable_pool = this.data;

                              log.info('saved variable pool data to window.metlife.variable_pool - callback_load_variable_pool::LoadCatalogItemVariablePool');

                              // fire the event variable_pool::received after the variable pool saved to window.metlife object

                              // any scripts that depend on this data set will receive the notification and begin processing

                              // http://api.prototypejs.org/dom/Element/fire/

                              // http://stackoverflow.com/questions/5823782/prototype-custom-event-not-on-a-dom-element

                              document.fire("variable_pool::received");

                      } else {

                              throw 'unexpected format for the parsed database response object';

                      }

              } catch (e) {

                      log.error('Error occured: ' + e.message + ' - callback_load_variable_pool::LoadCatalogItemVariablePool');

              }

      },

      /**

        extract the contents of the ajax response for either the error message or the payload

        returns true if successfuly extracted ajax payload data

        @method parseAjaxResponse

        @param {string} response raw XML string from servicenow GlideAjax

        @param {string} resp_key object key the repsonse data is stored under, defaults to 'payload'

        @returns {boolean}

      */

      parseAjaxResponse: function(response, resp_key) {

              var result = false;

              var ajax_data, resp_str;

              if (!(_.isString(resp_key) && resp_key.length)) {

                      resp_key = 'payload';

              }

              try {

                      resp_str = response.responseXML.documentElement.getAttribute("answer") + '';

                      if (!_.isString(resp_str)) {

                              throw 'Ajax response is not of type "string"';

                      }

                      //log.info(resp_str + ' - parseAjaxResponse::LoadCatalogItemVariablePool');

                      ajax_data = JSON.parse(resp_str);

                      // verify the ajax parsing passed

                      if (!(ajax_data && _.isObject(ajax_data))) {

                              throw 'Ajax parsing result is not of type "object"';

                      }

                      // check for server DB error message in the ajax response

                      if (_.has(ajax_data, 'error') && typeof ajax_data.error === 'boolean' && ajax_data.error && _.has(ajax_data, 'message')) {

                              throw 'Ajax server-side error occurred: "' + ajax_data.message + '"';

                      }

                      // verify the parsed ajax response object contains the 'payload' attribute

                      if (!(_.has(ajax_data, resp_key) && ajax_data[resp_key] && _.isObject(ajax_data[resp_key]))) {

                              throw 'invalid object parameter "payload" in the ajax response';

                      }

                      // if we got here, everything is good with the ajax parsing

                      result = true;

                      this.data = ajax_data[resp_key];

              } catch (e) {

                      this.error_msg = 'Error: ' + e.message;

              }

              return result;

      }

};

Catalog Client Script — load_catalog_variable_pool

  • Inject the UI script onto the page and instanciate the class, begin the ajax process

// include the class for performing database insert via glide ajax

document.write('<script><\/script>');

/**

    perform ajax request back to db to retrieve the list of variables on this catalog

    @method onload

*/

function onLoad() {

      var lcvp = new LoadCatalogItemVariablePool();

      lcvp.getVariablePool();

};

Catalog UI Policy — hide_device_recipient_variables_onload

  • Very basic example client script that shows how the variable pool can be used

function onCondition() {

      /**

        when the recipient reference field is empty, hide the email and employee id fields

        note:

        Due to the fact the variable data is delivered via an ajax request, we may have to

        create an event handler to listen for the variable data to arrive from the server

        condition - user reference field is empty (default when page loads)

        @method onCondition

 

      */

      if (window._ === 'undefined') {

              throw "underscore.js is undefined";

      }

      try {

              // set the user reference related variables to hidden

              if (_.has(window.metlife, 'variable_pool')) {

                      window.metlife.set_user_ref_fields_hidden();

              } else {

                      // execute via callback after variable data loads from ajax call

                      // listen for the event notification that the variable data exists on window.metlife.variable_pool

                      // uses prototype custom events

                      document.observe("variable_pool::received", function() {

                              window.metlife.set_user_ref_fields_hidden()

                      });

              }

      } catch (e) {

              // ie8 protection

              if (!window.console) {

                      console = {

                              log: function() {}

                      }

              };

              // incase the loglevel module gets disabled

              if (!window.log) {

                      log = {

                              error: console.log

                      };

              }

              log.error('Error: ' + e.message + ' - UI Policy hide recipient fields');

      }

};

/**

  namespace functions added for processing various form   data

  @property window.metlife

*/

if (!window.metlife) {

      window.metlife = {};

}

/**

  process the variable list and set the recipient user fields hidden

  @method window.metlife.set_user_ref_fields_hidden

*/

window.metlife.set_user_ref_fields_hidden = function() {

      log.info('Hide Device Recipient fields - onCondition::Catalog UI Policy');

      var target_variables = ['recipient_email', 'recipient_employee_id'];

      // verify the expected variables exist on the form before processing

      if (_.isArray(window.metlife.variable_pool) && window.metlife.variable_pool.length) {

              var observed_variables = _.pluck(window.metlife.variable_pool, 'name');

              if (_.intersection(observed_variables, target_variables).length === 2) {

                      // set the target variable to hidden

                      _.each(target_variables, function(value, key) {

                              log.info('set variable "' + value + '" display state to hidden - window.metlife.set_user_ref_fields_hidden');

                              g_form.setDisplay(value, false);

                      });

              }

      }

};

1 Comment
kevinanderson
Giga Guru

I made a small adjustment to the delivery of the variable pool data, found here