Help
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
The SN Nerd
Mega Sage
Mega Sage

How to Write Smart GlideAjax Quickly

Contents

Part 1 - The Approach

Part 2 - Reusability

Part 3 - Extending Functionality

Part 4 - Implementing GlideAjax in 1 LOC

Implementing GlideAjax in 1 LOC

In Part 3 of this serious, we looked at how to use Client Scripts and Script Includes to make server side calls for data. We solved the Too Much Information and Round Trip problems present with the existing g_form.getReference() and created a re-usable Ajax Script include we can re-use for any given table and fields. Our code is looking good, but there is still room for improvement. We are still having to write the same Client Scripts for each time we want to get data:

var ga = new GlideAjax('WhyStopAtOneAjax'); 
ga.addParam('sysparm_name','ajaxClientDataHandler'); 
ga.addParam('sysparm_tablename',table); 
ga.addParam('sysparm_sysid',newValue); 
ga.addParam('sysparm_fieldnames',fieldNames); 
ga.getXML(userCallback);

function userCallback(response) {
	var answer = response.responseXML.documentElement.getAttribute("answer");
	answer = answer.evalJSON();
	doSomething(answer);
}

Surely this can be made to be more simple and re-usable like g_form.getReference().

1 LOC like getReference()

Part of the reason we liked getReference() so much was that it made it easy to return data from the server with one line of code! If only it didn't suffer from the Too Much Information and Round Trip problem. 

In this tutorial, we will take you through how to make your own have getReference() function that uses a similar callback but returns display values and only the data you need. You'll be able to go back to your existing g_form.getReference() code and do your server side calls with one line of code.

Firstly, let's make some changes to our existing Script Include "WhyStopAtOneAjax" from Part 3. In this example, we will create a new Script Include called SmartAjaxDataLookup. We will modify our getPairValuesDisplays() to store our values and displays in separate structures.

var SmartAjaxDataLookup = Class.create();

SmartAjaxDataLookup.prototype = Object.extendsObject(AbstractAjaxProcessor, {
	
	ajaxClientDataHandler: function() {
		//Get data from the form
		var tableName = this.getParameter('sysparm_tablename');
		var sysId = this.getParameter('sysparm_sysid');
		var commaSeperatedFields = this.getParameter('sysparm_fieldnames');
		var fieldNames = commaSeperatedFields.split(",");
		//Setup data to return to form
		var answer={};

		//Do server side stuff
		answer = this.getPairValuesDisplays(tableName, sysId, fieldNames);
		//Encode data to send back to the form
		return new JSON().encode(answer);

	},
		
	getPairValuesDisplays: function(table, sysId, fieldNames) {
		var fieldsPairValues = {};
		fieldsPairValues._displayValues = {}; // Store display values separately
		var gr = new GlideRecordSecure(table);
		if (gr.get(sysId)) {
			for(var f in fieldNames) {
				var fieldName = fieldNames[f];
				var value =  gr.getValue(fieldName);
				if (value != null) {
					fieldsPairValues._displayValues[fieldName] = gr.getDisplayValue(fieldName);
					fieldsPairValues[fieldName] = value;
				}
			}
		} 
		
		return fieldsPairValues;
	},
	
	type: 'SmartAjaxDataLookup'
			
});

Create UI Script

Just like our Server-side GlideAjax Script, Client Scripts can be made re-usable too via UI Scripts. 

Let's add the common Client Script code to a Globally Scoped UI Script. We will want this object shared amongst all our scripts, so we only need one instance and will implement as a Singleton class.

We will also separate our GlideAjax and callback functions into functions.

processResponse() will also return a .getDisplayValue() function that we will use to get our display values.

var s_ajax = (function () {
	"use strict";
	
	return {
		// Public Functions
		getReference: function(fieldName, tableName, callbackFunc, fieldsToGet) {
				
			var fieldValue = g_form.getValue(fieldName);
			
			var ga = new GlideAjax('AjaxSmartDataLookup'); //Name of the Ajax Script Inclide
			ga.addParam('sysparm_name','ajaxClientDataHandler'); //Method to call
			ga.addParam('sysparm_tablename',tableName); //Table name
			ga.addParam('sysparm_sysid', fieldValue); //newValue
			ga.addParam('sysparm_fieldnames',fieldsToGet.join(",")); //Field names we want to retrieve
			ga.getXML(
				function(response) {
					processResponse(response,callbackFunc);
				}
			);
		},
		
		type: 'SmartAjax'
	};
	
	// Private Functions
	function processResponse(response,callbackFunc) {
		var answer = response.responseXML.documentElement.getAttribute("answer");
		answer = JSON.parse(answer);
		console.log(answer);
		answer.getDisplayValue = function(fieldName) {
			if (this._displayValues.hasOwnProperty(fieldName))
				return this._displayValues[fieldName];
			else
				throw("Error - Provided field '" + fieldName + "' is an invalid field and has no Display Value!");
		};
		callbackFunc(answer);
	}
	
})();

Developer Note

While making changes to UI Scripts, ensure you clear your browsers cache.
Refreshing the browser with cntrl-F5 or running cache.do usually does the trick.

Global UI Script

Unlike Server-Side scripts, Global UI Scripts are loaded on to every page (with the exclusion of Service Portal, which we will address later). If you don't plan on using this script across your entire implementation, you might want to deselect the 'Gobal' flag. This will stop the script from loading in every page. You'll have to add the code below to get access to SmartAjax Script Include.

function onLoad() {
        //Type appropriate comment here, and begin script below
        ScriptLoader.getScripts('SmartAjax.jsdbx', function(){});
}

Limitations

Using a non-global UI Script, you can not reliably use the SmartAjax functionality in an onLoad script. Sometimes the UI Script will load in time, sometimes it won't. If you need to run any SmartAjax code onLoad, you will have to place it in the second parameter function.
Make sure all your onChange scripts that call SmartAjax have an isLoading check before any code runs.

if (isLoading) {
	return;
}

If you need to do any logic on load, it is generally best to minimise server lookups and find alternatives to GlideAjax, such as Scratchpad and Business Rules.

 

Smarter coding

Good function should have as few parameters as possible. Our getReference() function is starting to get a little messy, with four parameters being passed in.

getReference: function(fieldName, tableName, callbackFunc, fieldsToGet) {

We can derive the Table name from the field being passed in using the g_from API.
To access it from this script, we need to reference it from window.

getReference: function(fieldName, callbackFunc, fieldsToGet) {
		
	var tableName = window.g_form.getGlideUIElement(fieldName).reference;
	var fieldValue = window.g_form.getValue(fieldName);
	
	var ga = new GlideAjax('SmartAjaxDataLookup'); //Name of the Ajax Script Inclide
	ga.addParam('sysparm_name','ajaxClientDataHandler'); //Method to call
	ga.addParam('sysparm_tablename',tableName); //Table name
	ga.addParam('sysparm_sysid', fieldValue); //newValue
	ga.addParam('sysparm_fieldnames',fieldsToGet.join(",")); //Field names we want to retrieve
	ga.getXML(
		function(response) {
			processResponse(response,callbackFunc);
		}
	);
	return fieldValue;
},

Client Script

Lets take a look at my favorite OOTB example from previous entries in the series, Client Script "(BP) Set Location to User"

onChange(control, oldValue, newValue, isLoading) {
   if (isLoading || (g_form.isLiveUpdating && g_form.isLiveUpdating()))
      return;

   if (newValue == '') {
      g_form.setValue('location', '');
      return;
   }
   if (!g_form.hasField('location'))
      return;
   var caller = g_form.getReference('caller_id', setLocation);
}

function setLocation(caller) {
   if (caller)
       g_form.setValue('location', caller.location);
}

We can improve the performance of this Script easily now using g_ajax.

  • Substitute g_form.getReference() call with s_ajax.getReference()
  • Add third parameter array of fields to retrieve
  • Add third parameter to g_form.setValue() caller.getDisplayValue('location')

 

function onChange(control, oldValue, newValue, isLoading) {
   if (isLoading)
      return;
	
	if (newValue == '') {
      g_form.setValue('location', '');
      return;
   }
	
   if (!g_form.hasField('location'))
      return;
	
   var caller = s_ajax.getReference('caller_id',setLocation,['location']);
}

function setLocation(caller) { 
	if (caller) {
		g_form.setValue('location', caller.location, caller.getDisplayValue('location')); //set value to avoid round-trip
	}
}

We have refactored our AJAX code down to 1 LOC!

But our journey isn't over yet.

There is a bit of work involved with getting this working in Service Portal.

Working with Service Portal 

To get this Script working in Service Portal, we will need to fix the following problems:

  • UI Scripts do not have access to g_form in Service Portal
  • ServicePortal GlideForm does not have getGlideUIElement to get the table name

Where did GlideForm go?

UI Scripts in Service Portal do not have access to window.g_form.
Not only that, they do not have access to g_form at all. There is currently no way (that  I am aware of) to call g_form API directly from a UI Script in Service Portal. This isn't a deal breaker though - we will just need to pass it in.

First, create a global variable called formHandler and replace all existing window.g_form with the new variable.

	"use strict";
	
	//Class variables
	var formHandler;

Create a public function to set the formHandler:

//Service Portal does not have access to g_form, so need to pass it in for SP use
//use s_ajax.setFormHandler(g_form) in onLoad Client Script of order 1, SP only
setFormHandler: function(newFormHandler) {
	formHandler = newFormHandler;
},

Create a private function to default the formHandler when window.g_form is accessible:

function setDefaultFormHandlerWhenUndefined() {
	if (formHandler == undefined)
		formHandler = window.g_form;
}

Replace all existing window.g_form with the new variable:

setDefaultFormHandlerWhenUndefined();	
var tableName = formHandler.getGlideUIElement(fieldName).reference;
var fieldValue = formHandler.getValue(fieldName);

Your code should now look like this

var s_ajax = (function () {
	"use strict";
	
	//Class variables
	var formHandler;
	
	return {
		
		/* Public Functions */
		
		//Service Portal does not have access to g_form, so need to pass it in for SP use
		//use s_ajax.setFormHandler(g_form) in onLoad Client Script of order 1, SP only
		setFormHandler: function(newFormHandler) {
			formHandler = newFormHandler;
		},
		
		getReference: function(fieldName, callbackFunc, fieldsToGet) {
			setDefaultFormHandlerWhenUndefined();	
			var tableName = formHandler.getGlideUIElement(fieldName).reference;
			var fieldValue = formHandler.getValue(fieldName);
			
			var ga = new GlideAjax('SmartAjaxDataLookup'); //Name of the Ajax Script Inclide
			ga.addParam('sysparm_name','ajaxClientDataHandler'); //Method to call
			ga.addParam('sysparm_tablename',tableName); //Table name
			ga.addParam('sysparm_sysid', fieldValue); //newValue
			ga.addParam('sysparm_fieldnames',fieldsToGet.join(",")); //Field names we want to retrieve
			ga.getXML(
				function(response) {
					processResponse(response,callbackFunc);
				}
			);
			return fieldValue;
		},
		
		type: 'SmartAjax'
	};
	
	//Private Functions
	function setDefaultFormHandlerWhenUndefined() {
		if (formHandler == undefined)
			formHandler = window.g_form;
	}
	
	function processResponse(response,callbackFunc) {
		var answer = response.responseXML.documentElement.getAttribute("answer");
		answer = JSON.parse(answer);
		console.log(answer);
		answer.getDisplayValue = function(fieldName) {
			if (this._displayValues.hasOwnProperty(fieldName))
				return this._displayValues[fieldName];
			else
				throw("Error - Provided field '" + fieldName + "' is an invalid field and has no Display Value!");
		};
		callbackFunc(answer);
	}
	
})();

Alternative to getGlideUIElement 

GlideForm has 3 different implementations depending on which interface you are using (Service Portal, Catalogue Items and Forms).
You can find a list of supported client side APIs for Service Portal on the Product Documentation web site, where you will see g_form.getGlideUIElement() is not supported. I have tested it just in case and can confirm that the function does not work. We will need to find another function that will give us an alternative.

After some experimentation, I found that we can use use g_form.getField() and then the .refTable property to get the Table name anyway.
Please note that this function is not supported or documented, but I was not able to find any alternative. Please let me know in the comments below if you have any alternative solutions.

Carrying on, lets make a private function to handle the different interfaces:

function getTableName(fieldName) {
	var tableName='';
	try {
		tableName = formHandler.getGlideUIElement(fieldName).reference;
	} catch (e) {
		tableName = formHandler.getField(fieldName).refTable; //SP does not support getGlideUIElement
	}
	return tableName;
}

And substitute it with our existing code

var tableName = getTableName(fieldName);

Our getReference() function should look like this 

getReference: function(fieldName, callbackFunc, fieldsToGet) {
	setDefaultFormHandlerWhenUndefined();
	
	var tableName = getTableName(fieldName);
	var fieldValue = formHandler.getValue(fieldName);
	
	var ga = new GlideAjax('OneLOCAjax'); //Name of the Ajax Script Inclide
	ga.addParam('sysparm_name','ajaxClientDataHandler'); //Method to call
	ga.addParam('sysparm_tablename',tableName); //Table name
	ga.addParam('sysparm_sysid', fieldValue); //newValue
	ga.addParam('sysparm_fieldnames',fieldsToGet); //Field names we want to retrieve
	ga.getXML(
		function(response) {
			processResponse(response,callbackFunc);
		}
	);
},

Passing in the form handler

One last step to get this working in Service Portal.
For any Catalog Item or Table you need this script, create a new onLoad Client Script.

In this example, we will create a Catalog Client Script.
Be sure to set the 'UI Type' to "Mobile / Service Portal" so it only runs for Service Portal.
Ensure your Client Script loads after the ScriptLoader call (if not using global UI Script) but before any other Client Script that calls s_ajax.

This is needed for your other Client Scripts that use s_ajax to work in Service Portal, without needing to create separate scripts. You can simply use your existing code.

 

Adding the UI Script to your Service Portal Theme

Now that all our code is setup, we need to add the UI Script to our Service Portal theme - even if you have set the 'Global' flag.

Navigate to "Service Portal > Themes" and select the theme being used by your Service Portal.
Scroll to the bottom of the Theme form to the Related Lists.
Select 'JS Includes' and click New

With 'Source' set as "UI Script", select the UI Script that contains your GlideAjax code and select Save.

Your 1 LOC Ajax is now usable in Service Portal.

Final Code

Your final UI Script should look like this

var s_ajax = (function () {
	"use strict";
	
	//Class variables
	var formHandler;
	
	return {
		
		/* Public Functions */
		
		//Service Portal does not have access to g_form, so need to pass it in for SP use
		//use s_ajax.setFormHandler(g_form) in onLoad Client Script of order 1, SP only
		setFormHandler: function(newFormHandler) {
			formHandler = newFormHandler;
		},
		
		getReference: function(fieldName, callbackFunc, fieldsToGet) {
			setDefaultFormHandlerWhenUndefined();	
			var tableName = getTableName(fieldName);
			var fieldValue = formHandler.getValue(fieldName);
			
			var ga = new GlideAjax('SmartAjaxDataLookup'); //Name of the Ajax Script Inclide
			ga.addParam('sysparm_name','ajaxClientDataHandler'); //Method to call
			ga.addParam('sysparm_tablename',tableName); //Table name
			ga.addParam('sysparm_sysid', fieldValue); //newValue
			ga.addParam('sysparm_fieldnames',fieldsToGet.join(",")); //Field names we want to retrieve
			ga.getXML(
				function(response) {
					processResponse(response,callbackFunc);
				}
			);
			return fieldValue;
		},
		
		type: 'SmartAjax'
	};
	
	//Private Functions
	function setDefaultFormHandlerWhenUndefined() {
		if (formHandler == undefined)
			formHandler = window.g_form;
	}
	
	function getTableName(fieldName) {
		var tableName='';
		try {
			tableName = formHandler.getGlideUIElement(fieldName).reference;
		} catch (e) {
			tableName = formHandler.getField(fieldName).refTable; //SP does not support getGlideUIElement
		}
		return tableName;
	}
	
	function processResponse(response,callbackFunc) {
		var answer = response.responseXML.documentElement.getAttribute("answer");
		answer = JSON.parse(answer);
		console.log(answer);
		answer.getDisplayValue = function(fieldName) {
			if (this._displayValues.hasOwnProperty(fieldName))
				return this._displayValues[fieldName];
			else
				throw("Error - Provided field '" + fieldName + "' is an invalid field and has no Display Value!");
		};
		callbackFunc(answer);
	}
	
})();


Conclusion

You now have a API to use GlideAjax to retrieve data from the server with 1 LOC.!

var fieldValue = s_ajax.getReference(referenceField,callbackFunction, arrayOfFieldsToGet);

In a Client Script:

function onChange(control, oldValue, newValue, isLoading) {
   if (isLoading)
      return;
	
	if (newValue == '') {
      g_form.setValue('location', '');
      return;
   }
	
   if (!g_form.hasField('location'))
      return;
	
   var caller = s_ajax.getReference('caller_id',setLocation,['location']);
}

function setLocation(caller) { 
	if (caller) {
		g_form.setValue('location', caller.location, caller.getDisplayValue('location')); //set value to avoid round-trip
	}
}

The final code can be downloaded via the Share Project page SmartAjax - Write GlideAjax Data Lookups in 1 L.O.C.

If you found this content useful, please bookmark and rate the Share App 🙂 🙂 🙂

Thanks! 

22 Comments