Using GlideAjax in Catalog Items - pushing a version of the variable_pool to the client
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.
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('<\/script>');</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._ === 'undefined') {</p> <p> throw "underscore.js is undefined";</p> <p> }</p> <p> try {</p> <p> // set the user reference related variables to hidden</p> <p> if (_.has(window.metlife, 'variable_pool')) {</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("variable_pool::received", 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('Error: ' + e.message + ' - UI Policy hide recipient fields');</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('Hide Device Recipient fields - onCondition::Catalog UI Policy');</p> <p> var target_variables = ['recipient_email', 'recipient_employee_id'];</p> <p> // verify the expected variables exist on the form before processing</p> <p> if (_.isArray(window.metlife.variable_pool) && window.metlife.variable_pool.length) {</p> <p> var observed_variables = _.pluck(window.metlife.variable_pool, 'name');</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('set variable "' + value + '" display state to hidden - window.metlife.set_user_ref_fields_hidden');</p> <p> g_form.setDisplay(value, false);</p> <p> });</p> <p> }</p> <p> }</p> <p>};</p>
https://www.servicenow.com/community/south-carolina-snug/using-glideajax-in-catalog-items-pushing-a-version-of-the/ba-p/2280998