kevinanderson
Giga Guru

Our team had an issue arise recently that was an interesting problem to correct.   We have quite a few Catalog Item forms built on top of the tables sc_task and sc_req_item.   A request was made a few development cycles ago to apply a read only state to all form variables when the user is viewing a submitted form as a part of a RITM.

The original solution that was developed, introduced a severe client-side performance hit that was resulting in load times that were as long almost 1 minute for the form to fully render client side.     The performance issue was due to the manner that the client side script was setting the variable fields to the read only state.   The original code leveraged client side GlideRecord queries that used synchronous database requests for each field.   Some of our forms could have 40   or more variables on them, and with each variable field related query taking on average 0.3 seconds, we began to get complaints from users experiencing painfully long page load times.


Original client side code

//Client script - Set Variables Read-Only

                            // if (g_user.hasRole('admin'))

                                      //                                 return;

                                     

                                      var map = gel('variable_map');

                                      var cat_form = new ServiceCatalogForm('ni', true, true);

                                    var items = map.getElementsByTagName("item");

                                      for (var i = 0; i < items.length; i++) {

                                                                              var item = items.item(i);

                                                                              var id = item.id;

                                                                              var name = item.getAttribute('qname');

                                                                              var optionId = getItemOptionID(id);

                                                                              var nm = new NameMapEntry(name, "ni.VE" + optionId);

                                                                              cat_form.addNameMapEntry(nm);

                                                                              if(g_form.getValue(name)=='' || g_form.getValue(name)=='false')

                                                                                          {

                                                                                                                      cat_form.setDisplay(name,false);

                                                                                          }

                                                                            else

                                                                                              {

                                                                                                                      cat_form.setReadonly(name,true);

                                                                                                                      if(name=='comments') //Hide Comments container if comments field is blank

                                                                                                                      {

                                                                                                                                                              if(g_form.getValue('comments') == '')

                                                                                                                                                              {

                                                                                                                                                                                                      g_form.setDisplay('commContainerStart',false);

                                                                                                                                                              }

                                                                                                                      }

                                     

                                                                                                                      // Added this to Hide the fields without any value

                                                                             

                                                                             

                                                                              }

}

function getItemOptionID(id) {

                                      var item_option = new GlideRecord('sc_item_option_mtom');

                                      item_option.addQuery('request_item', g_form.getUniqueValue());

                                      item_option.addQuery('sc_item_option.item_option_new', id);

                                      item_option.query();

                                      if(item_option.next())

                                                                              return item_option.sc_item_option;

}

After doing a thorough analysis of the situation, and feedback from an ACE report from ServiceNow, I set about to remove the synchronous ajax calls from the process.   My first task was to group all the variable data collection into a javascript object, and then move the "getItemOptionID"   function to a script include class.   The data flow I decided on was to collect for form variable information on page load,   call a server side function via an asynchronous ajax call that would   submit ALL the variable data .   The script include class would provide an endpoint to receive all the form data, perform the necessary   database queries on each variable, and send all the results back as a single javascript object.   On the client, the DB response would be received, and the javascript object contained in the response would be de-serialized (from json), and looped over to apply the hide/read-only state to each variable.


The solution works very well, reducing client side render time for these forms by 30-60%.   The only downside is that the fields are open for editing for a second or so as the page completes the asynchronous transaction to get all of the variable information from the database.

ajax_flow.png

Final Scripts:

Client Script: Set Variables Read-Only (attached to the target tables,   sc_task and sc_req_item )

/**

  call the UI scripts that collect all the form variables and submit back to server to retrieve the variable element ids

  then process each variable and set to read only or hidden

  @method onLoad

*/

// set variables read only on table sc_req_item

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

function onLoad() {

  var rfv = new ReadOnlyFormVariables();

  rfv.collectFormVariableData("sc_req_item");

  rfv.getVariableDataGlideAjax();

}

//-----------------------------------------

// Set Variables Read Only on Task

/**

  call the UI scripts that collect all the form variables and submit back to server to retrieve the variable element ids

  then process each variable and set to read only or hidden

  @method onLoad

*/

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

function onLoad() {

  var rfv = new ReadOnlyFormVariables();

  rfv.collectFormVariableData("sc_task");

  rfv.getVariableDataGlideAjax();

}

Script Include: GetFormVariableIds

var GetFormVariableIds = Class.create();

GetFormVariableIds.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /**

        process the array of variable information and fetch the id for each variable

        @method GetFormVariableIds

    */

    GetFormVariableIds: function() {

          var json = new JSON();

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

          result = [];

    if (data && data.length){

                  var obj = {};

    for (var i=0, len = data.length; i < len; i++ ){

  try {

  if (this._has(data[i], 'id') && this._has(data[i], 'request_item')){

  obj = data[i];

  obj['option_id'] = this._getItemOptionID(data[i].id, data[i].request_item);

  }

  else {

  var key = "error_" +i;

  obj[key] =   "Error: the required properties \"id\" and \"request_item\" were not found - GetFormVariableIds";

  }

  }

  catch(e){

  var key = "error_" +i;

  obj[key] =   "Error: "+e.message+" - GetFormVariableIds";

  }

  result.push(obj);

    }

          }

    else{

    result.push({'error': 'data parameter is undefined'});

    }

          return json.encode(result);

    },

  /**

  provate method to test if an object contaisna   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]';

  },

  /**

    lookup the

    @method _getItemOptionID

    @param p_id

  */

  _getItemOptionID : function(p_id, p_request_item) {

  if (this._isString(p_id) && this._isString(p_request_item)){

  //Variable Ownership sc_item_option_mtom

  var item_option = new GlideRecord('sc_item_option_mtom');

  item_option.addQuery('request_item', p_request_item);

  item_option.addQuery('sc_item_option.item_option_new', p_id);

  item_option.query();

  if(item_option.next()){

  return item_option.sc_item_option + '';

  }

  else{

  return "no db match for params: "+p_id+" - "+p_request_item;

  }

  }

  else{

  return "not string params"

  }

  },

});

});

UI Script: ReadOnlyFormVariables

/**

  UI script

  collect all the form variables and submit back to server to retrieve the variable element ids

  then process each variable and set to read only or hidden

  @method onLoad

*/

/*

//usage example

function onLoad() {

  var rfv = new ReadOnlyFormVariables();

  rfv.collectFormVariableData("sc_req_item" );

}

*/

/**

  collect variables off the ServiceNow form,

  and query the DB via glidejax to determine which variables to hide / set read-only

  @class ReadOnlyFormVariables

*/

function ReadOnlyFormVariables (obj) {

  this.variables = [];

}

ReadOnlyFormVariables.prototype = {

      constructor : ReadOnlyFormVariables,

  /**

    read the variables off the form and create an array of objects

    containing the   properties "id","request_item","name"

    @method collectFormVariableData

    @param {string} p_srctable - current expects either "sc_task" or "sc_req_item"

  */

  collectFormVariableData : function(p_srctable){

  var context = this;

  if (this._isString(p_srctable)){

  var map = gel('variable_map');

  if(map) {

  var cat_form = new ServiceCatalogForm('ni', true, true);

  var items = map.getElementsByTagName("item");

  /**

    for the sc_task table there is a db request to be made for a field sysid.

    this callback allows that db request to complete before further processing the field values

    @method callback

    @param the request item id (sysid or unique for id)

  */

  var callback = function(caller){

  for (var i = 0; i < items.length; i++) {

  var item = items.item(i);

  if (caller && context._has(caller, 'sys_id') && caller.sys_id.length){

  var target_el = {

  /*item : items.item(i),*/

  id: item.id,

  name : item.getAttribute('qname'),

  request_item: (caller.sys_id + '')

  }

  context.variables.push(target_el);

  }

  else{

  context._log("Error: invalid callback parameter \"caller\" - ReadOnlyFormVariables::collectFormVariableData");

  }

  }

  // query the database to get data necessary to hide the form variables

  context.getVariableDataGlideAjax();

  }

  switch (p_srctable.toLowerCase()) {

  case 'sc_task':

  g_form.getReference('request_item', callback);

  break;

  case 'sc_req_item':

  var req_item = {};

  req_item['sys_id'] = g_form.getUniqueValue() + '';

  callback(req_item);

  break;

  }

  }

  else{

  context._log("Error: GEL variable map is null. Process halted - ReadOnlyFormVariables::collectFormVariableData");

  }

  }

  else{

  }

  },

  /**

    perform the ajax request for the variable ids that occur on the form based on their glide properties

    @method getVariableDataGlideAjax

  */

  getVariableDataGlideAjax : function (){

  var context = this;

  if (this.variables && this.variables.hasOwnProperty('length') && this.variables.length){

  // next line is dependant on prototype - ie8 may not have the json encode method

  var json_data = Object.toJSON(this.variables);

  //context._log(json_data);

  var ga = new GlideAjax('GetFormVariableIds');

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

  ga.addParam('sysparm_data',json_data);

  /**

    process the response from the ajax request

    @method anonymous callback function

    @param {object} response - xml server response

  */

  ga.getXML(function (response) {

  var data, answer = response.responseXML.documentElement.getAttribute("answer");

  if (context._isString(answer)){

  // prototype dependcy -   json parser

  data = answer.evalJSON(true)

  context._log('DATA RECIEVED');

  if (data && data.hasOwnProperty('length') && data.length){

  for (var i = 0; i < data.length; i++) {

  context._log(data[i]);

  context.setVariableReadOnly(data[i]);

  }

  }

  else{

  context._log("Error: ajax response payload is not of type \"array\". Process halted - ReadOnlyFormVariables::getVariableDataGlideAjax")

  }

  }

  else{

  context._log("Error: invalid data type for the ajax response payload. Process halted - ReadOnlyFormVariables::getVariableDataGlideAjax")

  }

  });

  }

  else{

  context._log("Error: the array of form variable data is empty or invalid. Process halted - ReadOnlyFormVariables::getVariableDataGlideAjax");

  }

  },

  /**

    process the results from the DB ajax request on the variable list and set the identified variables as read-only

    @method setVariablesReadOnly

    @param {object} p_obj contains the properties "id","request_item","name", and "option_id" used to identify a variable on the form

  */

  setVariableReadOnly : function(p_obj){

  var context = this;

  if (this._isObject(p_obj) && this._has(p_obj, 'name') && this._has(p_obj, 'option_id')){

  var nm = new NameMapEntry(p_obj.name, "ni.VE" + p_obj.option_id);

  var cat_form = new ServiceCatalogForm('ni', true, true);

  cat_form.addNameMapEntry(nm);

  if(g_form.getValue(p_obj.name) === '' || g_form.getValue(p_obj.name) === 'false'){

  cat_form.setDisplay(p_obj.name,false);

  }

  else{

  cat_form.setReadonly(p_obj.name,true);

  }

  if(p_obj.name === 'comments'){ //Hide Comments container if comments field is blank

  if(g_form.getValue('comments') === ''){

  g_form.setDisplay('commContainerStart',false);

  }

  }

  }

  else{

  context._log("Error: missing keys from the variable data object - setVariableReadOnly::getVariableDataGlideAjax")

  }

  },

  /**

  private method to test if an object contains a   property

  @method _has

  @param {object} obj

  @param {string} prop

  */

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

  var result = false;

  if (this._isObject(obj) && this._isString(prop) && obj && prop.length){

  if (obj.hasOwnProperty(prop)){

  result = true;

  }

  }

  return result;

  },

  /**

    Is a given variable an object?

    @method isObject

    @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

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

  */

  _isString : function(str) {

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

  },

  /**

    prevent console.log errors in ie8

    @method _log

  */

  _log : function(str){

  if (typeof console === 'undefined'){

  /**

    ie8 console.log shim

    @link http://stackoverflow.com/questions/690251/what-happened-to-console-log-in-ie8

  */

  !window.console && (window.console = { log: function () { } });

  }

  console.log(str);

  }

};


2 Comments
Shane J
Tera Guru

Kevin, I'm getting errors when trying to save the ScriptInclude (I already cleaned up some piddly errors with it):



WARNING at line 8: Do not use JSON as a constructor.



WARNING at line 19: ['option_id'] is better written in dot notation.



WARNING at line 28: 'key' is already defined.



Can you assist?


kevinanderson
Giga Guru

jued0001


line 28 - move the "var key" line to above the for loop,   line 14 would be a good place.   The on line 28 just write "key = "error_" +i;"



line 19 you can rewrite obj['option_id']   as obj.option_id



line 8 :   do you have the JSON class in your script includes?   that is a dependency.