Reusing client-callable Script Includes: Wrapper and Proxy
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
yesterday - last edited yesterday
Problem
You want to reuse a function from client-callable Script Include.
Unfortunately the function accepts zero arguments and instead calls "this.getParameter" a few times, to receive values from a client-side call.
You're trying to avoid changing or copying the original code and creating more server-side objects.
I'd like to offer two solutions:
- A wrapper fill-in
- The standard JavaScript Proxy object (newly supported in ServiceNow since Zurich)
Prerequisites
Assume we have a client-callable Script Include that looks something like this:
var MyAppClientUtils = Class.create();
MyAppClientUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
doStuff: function doStuff() {
let result = {
success: false,
value: ''
};
let someVariable = this.getParameter('sysparm_some_variable');
let xyz = this.getParameter('sysparm_xyz');
result.value = someVariable + ' - ' + xyz;
if (result.value) {
result.success = true;
}
return JSON.stringify(result);
},
type: 'MyAppClientUtils'
});
And we also have a purely server-side script, a Business Rule in this case, which attempts to use the "doStuff" function directly:
(function executeRule(current, previous /*null when async*/) {
let number = current.getValue('number');
let createdOn = current.getValue('sys_created_on');
let clientUtils = new MyAppClientUtils();
let result = clientUtils.doStuff(number, createdOn);
current.some_field = JSON.parse(result).value;
})(current, previous);
This will not work in the way intended, because "doStuff" does not accept any arguments.
The result will be some undefined value.
We need to somehow replace or intercept the "this.getParameter" function to feed our own values to the "doStuff" function.
There are several ways to do this, two of which I'll show next.
Solution 1: Wrapper fill-in
We can construct a wrapper object, which contains its own "getParameter" function and "sysparm_<variableName>" properties, so the Business Rule looks like this:
(function executeRule(current, previous /*null when async*/ ) {
let number = current.getValue('number');
let createdOn = current.getValue('sys_created_on');
let clientUtils = new MyAppClientUtils();
let wrapperObject = {
// borrow target function
clientUtilFunction: clientUtils.doStuff,
// substitute the getParameter function
getParameter: function(parameterName) {
return this[parameterName];
},
// define "sysparm"s with function's parameters
sysparm_some_variable: number,
sysparm_xyz: createdOn
};
let result = wrapperObject.clientUtilFunction();
current.some_field = JSON.parse(result).value;
})(current, previous);
The wrapper object borrows the target client-side function as its own property as a reference.
Weaving the magic:
By calling the client-callable function via the borrowed property like follows, the "this" keyword inside the actual function will point to our wrapperObject:
wrapperObject.clientUtilFunction();This is standard JavaScript functionality.
So to connect the dots, any call inside "doStuff" to "this.getParameter" will actually call "wrapperObject.getParameter" and thus will retrieve both "wrapperObject.sysparm_some_variable" and "wrapperObject.sysparm_xyz" with the values from our server-side variables.
Solution 2: Proxy
ServiceNow gradually integrates support for more and more modern JavaScript functionality.
Among this functionality is the Proxy object, which as the name says is a proxy or wrapper object for other objects.
It's available since the Zurich version, prior versions disallow its usage.
The Proxy object is used to intercept and modify property access, function calls, etc., see MDN documentation for more info.
Using Proxy our Business Rule will look like this:
(function executeRule(current, previous /*null when async*/ ) {
let proxyHandler = {
// "apply": Special property for function interception.
apply: function(target, thisArg, argArray) {
thisArg.getParameter = function(sysparmName) {
if (sysparmName == 'sysparm_some_variable') {
// The first argument form the "proxy(...)" call below.
return argArray[0];
}
if (sysparmName == 'sysparm_xyz') {
// The second argument from the "proxy(...)" call below.
return argArray[1];
}
};
// Call target, which will be "doStuff".
return target();
}
};
let number = current.getValue('number');
let createdOn = current.getValue('sys_created_on');
let clientUtils = new MyAppClientUtils();
// Create the proxy for our client-callable function.
let proxy = new Proxy(clientUtils.doStuff, proxyHandler);
// Call our proxy, which calls the handler, which in turn calls "doStuff".
let result = proxy(number, createdOn);
current.some_field = JSON.parse(result).value;
})(current, previous);
We first define a handler object for our proxy. This is what defines what happens when our client-callable function is called.
The "apply" property is a special property name that we need to use. It tells the Proxy object, that we're trying to intercept a function call, instead of other things like property access.
The function that "apply" holds is the function that will be executed before calling our actual client-callable function.
The parameters for the apply functions are as follows:
- target: The target function "doStuff" that is being wrapped/proxied. We're calling it in the end of the apply-function.
- thisArg: The value "this" inside the "doStuff" function will be. We're redefining the "getParameter" function as a function property of "thisArg", so "doStuff" gets actual values.
- argArray: Passed-through arguments from the actual proxied function call. This is where the "number" and "createdOn" values will come from.
So when we call "proxy(number, createdOn)", the two arguments are passed to the handler function "apply".
"apply" redefines what "doStuff" knows as "this" and then "apply" calls "doStuff".
Result
Both the wrapper fill-in and the Proxy approach produce the same result for the "some_field" field:
Why and when
With all versions beginning with Zurich the Proxy object can be freely used, as its standard JavaScript functionality that is now supported.
If you need pre-Zurich support or if Proxy syntax is too complicated use the wrapper fill-in approach.
It's probably a very specific use case.
If you're able to design the client-callable Script Include before anyone might need to reuse it in the future, you could apply what Earl Duque has layed out in his article. That way it's accessible both from client and server side in the same way.
-
Hope this is interesting and helpful to some of you.