- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
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.
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);
}
};
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.