logo

NJP

Using GlideAjax in Catalog Items - pushing a version of the variable_pool to the client

Import · Aug 27, 2015 · article

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.

image

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%5FObjects/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('&lt;\/script&gt;&#39;);</p> <p>/**</p> <p>    perform ajax request back to db to retrieve the list of variables on this catalog</p> <p>    @method onload </p> <p>*/</p> <p>function onLoad() {</p> <p>      var lcvp = new LoadCatalogItemVariablePool();</p> <p>      lcvp.getVariablePool();</p> <p>};</p> <p>Catalog UI Policy — hide_device_recipient_variables_onload</p> <ul> <li>Very basic example client script that shows how the variable pool can be used</li> </ul> <p>function onCondition() {</p> <p>      /**</p> <p>        when the recipient reference field is empty, hide the email and employee id fields</p> <p>        note:</p> <p>        Due to the fact the variable data is delivered via an ajax request, we may have to</p> <p>        create an event handler to listen for the variable data to arrive from the server</p> <p>        condition - user reference field is empty (default when page loads)</p> <p>        @method onCondition</p> <p>      */</p> <p>      if (window._ === &#39;undefined&#39;) {</p> <p>              throw &quot;underscore.js is undefined&quot;;</p> <p>      }</p> <p>      try {</p> <p>              // set the user reference related variables to hidden</p> <p>              if (_.has(window.metlife, &#39;variable_pool&#39;)) {</p> <p>                      window.metlife.set_user_ref_fields_hidden();</p> <p>              } else {</p> <p>                      // execute via callback after variable data loads from ajax call</p> <p>                      // listen for the event notification that the variable data exists on window.metlife.variable_pool</p> <p>                      // uses prototype custom events</p> <p>                      document.observe(&quot;variable_pool::received&quot;, function() {</p> <p>                              window.metlife.set_user_ref_fields_hidden()</p> <p>                      });</p> <p>              }</p> <p>      } catch (e) {</p> <p>              // ie8 protection</p> <p>              if (!window.console) {</p> <p>                      console = {</p> <p>                              log: function() {}</p> <p>                      }</p> <p>              };</p> <p>              // incase the loglevel module gets disabled</p> <p>              if (!window.log) {</p> <p>                      log = {</p> <p>                              error: console.log</p> <p>                      };</p> <p>              }</p> <p>              log.error(&#39;Error: &#39; + e.message + &#39; - UI Policy hide recipient fields&#39;);</p> <p>      }</p> <p>};</p> <p>/**</p> <p>  namespace functions added for processing various form   data</p> <p>  @property window.metlife</p> <p>*/</p> <p>if (!window.metlife) {</p> <p>      window.metlife = {};</p> <p>}</p> <p>/**</p> <p>  process the variable list and set the recipient user fields hidden</p> <p>  @method window.metlife.set_user_ref_fields_hidden</p> <p>*/</p> <p>window.metlife.set_user_ref_fields_hidden = function() {</p> <p>      log.info(&#39;Hide Device Recipient fields - onCondition::Catalog UI Policy&#39;);</p> <p>      var target_variables = [&#39;recipient_email&#39;, &#39;recipient_employee_id&#39;];</p> <p>      // verify the expected variables exist on the form before processing</p> <p>      if (_.isArray(window.metlife.variable_pool) &amp;&amp; window.metlife.variable_pool.length) {</p> <p>              var observed_variables = _.pluck(window.metlife.variable_pool, &#39;name&#39;);</p> <p>              if (_.intersection(observed_variables, target_variables).length === 2) {</p> <p>                      // set the target variable to hidden</p> <p>                      _.each(target_variables, function(value, key) {</p> <p>                              log.info(&#39;set variable &quot;&#39; + value + &#39;&quot; display state to hidden - window.metlife.set_user_ref_fields_hidden&#39;);</p> <p>                              g_form.setDisplay(value, false);</p> <p>                      });</p> <p>              }</p> <p>      }</p> <p>};</p>

View original source

https://www.servicenow.com/community/south-carolina-snug/using-glideajax-in-catalog-items-pushing-a-version-of-the/ba-p/2280998