
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
Extension Points
A couple of years ago I worked on a project to enable plugin-like possibilities for Scoped Apps. The goal was to allow application authors to define points in their applications where third-party developers could register to have their UI, server-side or client-side scripted logic be executed. The result of this project was three different kind of Extension Points in the platform:
- Scripted extension points
- UI extension points
- Client extension points
We already have data models in the platform to define server-side script logic, client-side script logic, and UI. The extension point framework allows an application author to provide a reference implementation of one or all of the above, and to write their applications to call registered implementations.
A full implementation of an Extension Point, regardless of type, consists of four things:
- A definition of the extension point, which provides the interface everyone must adhere to when the implement the extension.
- A concrete implementation of the interface which actually does some work.
- A registration record which links the concrete implementation to the interface.
- Some code which executes the extension points.
In my use-case, I had an application that used Google Maps and surfaced points-of-interest on a map of Chicago. I implemented a couple of types of reference points by pulling open data from Chicago's Data Portal and rendering them on the map. I wanted any third-party developer to be able to add their own points of interest to the map, so I needed all three kinds of extension points. I wanted developers to be able to write their own Script Includes to be able to fetch geographic points of interest, UI Macros to include server-generated HTML into the page, and UI Scripts to manipulate the map on in the user's browser.
NOTE: All of the snippets below were written from memory and aren't meant to be directly copy-and-pasted. There are likely bugs. This blog post is not designed to be an end-to-end tutorial.
Scripted extension points
In my application, I defined a scripted extension point for fetching and formatting the points of the map. The reference implementation, which is defined in the sys_extension_point
table, looked somewhat like this:
var MapPoints = Class.create();
MapPoints.prototype = {
process: function(/*object*/ bounding_box, /*array*/ data) {
/* bounding_box is an object that contains the four corners of the map
you can use these to fetch the relevant attraction points from your data
source and format each point as an object with these properties:
* latitude : A string representation of the latitude in floating-point format
* longitude : A string representation of the longitude in floating-point format
* name : The title of the attraction
* description : A short description that will appear in the hover-box
* type : the classification of the data point (monument, building, park, kiosk)
* icon : the path of a custom icon to display on the map (optional)
*/
var obj = {
"name : "Adler Planetarium",
"description" : "A planetarium with a cool laser show",
"type" : "museum"
"icon" : "/images/repository.png"
}
data.push(obj);
},
type: 'MapPoints'
};
Then I made a concrete implementation by writing an actual Script Include that implemented this interface- it has a process
method that accepts a bounding-box object and an array, fetches data from an endpoint, builds the objects, and pushes them into the array. That script looked something like this:
var ChicagoPoints = Class.create();
ChicagoPoints.prototype = {
process: function(bb, /*array*/ data) {
var cc = new CoordinateCalculator();
cc.setMaxLong(bb.longitude_max);
cc.setMinLong(bb.longitude_min)
cc.setMaxLat(bb.latitude_max);
cc.setMinLat(bb.latitude_min);
var centralPoint = cc.calcCenter();
var fetcher = sn_ws.RESTMessageV2("ChicagoDataFetcher","get");
fetcher.setAuthenticationProfile("basic","<a_sys_id>");
fetcher.setStringParameter("latitude", centralPoint.latitude);
fetcher.setStringParameter("longitude", centralPoint.longitude);
var response = fetcher.execute();
var body = JSON.parse(response.getBody());
for (var key in body) {
var POI = body[key];
var formattedPOI = {
"name" : POI.attractionName,
"description" : POI.attractionDescription,
"type" : POI.attractionCategory,
"icon" : "/images/chicago-flag-pin.png"
};
data.push(formattedPOI);
}
},
type: 'ChicagoPoints'
};
This isn't working code, but you can see here how I defined an interface in the first script, then made an actual implementation of it in the second one. To make sure my ChicagoPoints
script is called when I want it to be, I did two more thing:
- I registered ChicagoPoints as an implementation of ExamplePoint by adding a record to the
sys_extension_instance
table. There is a Related Link on the Extension Point table that makes this very easy to do. - I wrote another script include which is what I call when I want to get all points for my map. This script include is where I execute all implementations of the extension point.
My second Script Include looks like this:
var PointsOfInterest = Class.create();
PointsOfInterest.prototype = {
initialize: function() {
},
get: function(max_lat, max_long, min_lat, min_long) {
var points = [];
var bb = {
"longitude_max" : max_long,
"longitude_min" : min_long,
"latitude_max" : max_lat,
"latitude_min" : min_lat
};
var sep = new GlideScriptedExtensionPoint().getExtensions("MapPoints");
for (var i = 0; i < sep.length; i++) {
var ep = sep[i];
try {
ep.process(bb, points);
} catch (e) {
gs.error("Error processing pont {0}", ep.type);
}
}
},
type: 'PointsOfInterest'
};
This is the script that I actually call in any of my code where I want to return points for a map. This script include's get
method will call the server-side GlideScriptedExtensionPoint
class to fetch the registered extensions, and then call the process method on each of them to aggregate data before returning the aggregated data to the caller.
UI extension points
UI extension points are for generating HTML server-side. Like Scripted Extension Points, they are implemented with an existing platform data model - in this case, with UI Macros. UI Macros are jelly code that can be executed on the server to generate HTML. A lot of the classic interface of ServiceNow is built with UI Macros- some of them exposed in the database for customers to modify, and others are hard-coded on the server.
UI Extension Points use the ability of UI Macros to programmatically call other UI Macros. Like Scripted Extension Points, there is an interface for implementers to follow, a way for implementers to register their macro to be called, and an API that application authors can call in their UI's to output the HTML of all registered implementations.
There are three APIs that can be used with UI Extension Points:
- Direct calling in a UI Macro executed as part of some server-side form
- A REST API that returns HTML rendered on the server to the caller (usually a script running in the user's browser)
- Scripted access that returns HTML strings to a server-side script
For my application, I had a UI Page with a menu in it. The menu was just an unordered list with some Javascript that was supposed to run when a user clicked on each entry. Much like the Scripted Extension Point above, the first step is to create the Extension Point definition. UI Extension Points are defined in the sys_ui_extension_point
table. For my very simple interface, I wanted the implementation to return an <li />
HTML element.
My UI Extension Point definition was named "MenuItems" and looked something like this:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<li onclick="x_my_app.MenuRunner.itemClicked('Custom item')">$[jvar_prefix] $[gs.getMessage("Custom item")]</li>
</j:jelly>
Then I created a few implementations of this extension point by adding several UI Macros that each defined a single menu item, adding an entry in the sys_ui_extension_instance
for each implementation. The implementations were all just variations on the above definition, with different strings passed to gs.getMessage
. My menu items were ordered by the Order field for each row in the sys_ui_extension_instance
table for the MenuItems extension point.
Next I added a UI Macro that calls my UI Extension Point, which I then used in the UI Page with the map in it. The UI Macro that called my extension point was named MapMenu and looked like this:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<menu id="mapmenu">
<g2:call_extension extension="x_my_app.MenuItems" prefix="**"/>
</menu>
</j:jelly>
Then in my UI Page, I called my MapMenu UI Macro at the appropriate place: <g:x_my_app_MapMenu />
This called my Menu UI Action which invoked my extension point which rendered all of my implementations, and my page contained a <menu>
item with multiple entries in it.
If I wanted to, I could write an Ajax call on the client side to fetch the rendered HTML, and then use DOM-manipulation to dynamically modify my page as well. UI Extension Points are exposed via the REST endpoint /api/now/uiextension
. This API takes the name of the UI Extension Point on the path, and two parameters in the query: sysparm_scope
, which is the scope that owns the UI Extension Point definition, and sysparm_variables
, which is a JSON-string of name:value
pairs to use as variables (like I used "prefix" above).
I could make a GET request to
/api/now/uiextension/MenuItems?sysparm_scope=x_my_app&sysparm_variables={"prefix":"**"}
and the response will be an HTML string that I can embed in my page. The Extension Point definition must opt-in to being runnable in this way. You should not expose sensitive information in an implementation, and your macros should do access-checking if you will be returning any information from records in the platform.
Alternatively, I could get the HTML string in script on the server-side, and then use it in places where UI Macros aren't generally called, but server-side HTML generation is wanted. To do that, in a server script I would call:
var HTML = new GlideUIExtensionPoint().getExtensions("MenuItems", {"prefix" : "**"});
My HTML variable now contains my <menu>
with all of the list items, and I can insert that somewhere that HTML is being rendered.
Client Extension Points
The last type of Extension Point is for client-side consumption. This type of extension is how you can embed multiple UI Scripts into a page. There are two way you may want to do this: by getting the URL for each implementing script and adding <script>
tags for each one, or by concatenating the contents of every script that implements the point into a single string and embedding that in a <script>
tag. You might want to do this before the page loads either via script or by calling a jelly tag; alternatively, you may want to do this from the client itself, using Ajax calls and possibly the ScriptLoader.
There are lots of ways that you may want to design a Client Extension Point interface, depending on what you want to do client-side, and how you want to import the scripts. In my example, I used a global object defined in a UI Script to define a list handlers that my item items would end up needing, and then built the Client Extension Points around that usage. I started with a UI Script which looked like this:
var x_my_app = sn_extension_point || {};
x_my_app.MenuRunner = (function() {
"use strict";
var handlers = {};
function hasHandler(title) {
return handlers.hasOwnProperty(title);
}
function addItemHandler(title, handleFunc) {
if (!hasHandler(title)) {
handlers[title] = handleFunc;
return true;
}
return false;
}
function executeHandler(title) {
if (!hasHandler(title))
return false;
return handlers[title].call(null, title);
}
return {
addHandler: function(title, handleFunc) {
return private_function();
},
itemClicked: function(title) {
return executeHandler(title);
},
type: "MenuRunner"
};
})();
In this script, a global object named "x_my_app" will be created in the browser's script environment. This acts like my namespace so I don't accidentally override other globals. If the namespace has already been created (such as because I have multiple UI Scripts on this page) then we will simply be adding stuff to it. It'll add a MenuRunner
object in the "x_my_app" namespace, and expose 2 methods on it: addHandler
and itemClicked
. The itemClicked method is what our <li>
items call when clicked. The addHandler
method is what our Extension Point implementations will want to call.
Once I knew how I wanted to expose access to my UI script code, I settled on an interface that my Client Extension Points needed to implement. Each Client Extension Point implementation will need to call my addHandler method when it loaded in the page. They will need to pass in the title of the menu item they care about, as well as the function to execute when it is clicked on.
Like the other extension points, I needed to define the 4 essential parts of the Extension Point: the definition, the implementation, the registration, and the caller.
Starting with the definition, I added the interface the UI Scripts should follow to the sys_client_extension_point
table. My ExtensionPoint was named "MenuItemScripts" and the interface looked something like this:
(function() {
function alertTitle(title) {
alert("You clicked on " + title);
}
if (typeof x_my_app != undefined && x_my_app.hasOwnProperty("MenuRunner"))
x_my_app.MenuRunner.addHandler("Title 1", alertTitle);
})();
For each title I expected in my menu (for which I created a UI Extension Point), I added a Client Extension Point to handle what happens when it was clicked on. Each one was a UI Script in the sys_ui_script
table, and I added a registrations linking the UI Script to the Extension Point in the sys_client_extension_instance
table. Each script looked like the above, though with different titles (one for each custom menu item).
To get the UI Scripts into my page, I decided to use the UI Macro mechanism to just output the script tags individually in my UI Page. I created a UI Macro named "MenuHandlers" which looked like this:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:client_extension name="MenuItemScripts" inline="false" />
</j:jelly>
Then, like the UI Extension Point, I called this macro in my page: <g:x_my_app_MenuHandlers />
This resulted in 4 <script>
tags being added to my page, each one with a src
attribute pointing to the UI Script path in my instance (scripts/SomeUiScript.jsdbx
).
Instead of outputting individual <script>
tags with URLs, I could have changed my macro to call it inline:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:client_extension name="MenuItemScripts" inline="true" />
</j:jelly>
This would output a single <script>
tag in the page, with the content of each UI Script embedded directly inside it. I can also get this content in one String via a scripted API (similar to the "inline" option when using the UI Macro above), or from an AJAX call to a REST endpoint;
The scripted way is the same as with the other Extension Points. There is a class called GlideClientExtensionPoint
with a getExtensions
method that can return the data for us. I could then output that into a <script>
tag somewhere that outputs HTML, but where UI Macros aren't available.
Just like with UI Extensions, I could write an Ajax call on the client side to fetch the UI Script content for all of the scripts which implements my Extension Point. Client Extension Points are exposed via the REST endpoint /api/now/clientextension
. This API takes the name of the Client Extension Point on the path, and one parameter in the query: sysparm_scope
, which is the scope that owns the Client Extension Point definition.
I could make a GET request to
/api/now/clientextension/MenuItemScripts?sysparm_scope=x_my_app
and the response will be an HTML string that I can embed in my page. The Extension Point definition must opt-in to being runnable in this way. You should not expose sensitive information in an implementation, and your macros should do access-checking if you will be returning any information from records in the platform.
Depending on the context of where you want to use these scripts, consider your options for getting the scripts onto the page:
- On a classic platform Form, create a Formatter and UI Macro and add it to the form layout.
- The
sys_client_extension_instance
table is readable by all scopes, so you could always make your own REST endpoint or client-callable Script Include, query the table for your endpoint, and return the script names, and then use the ScriptLoader to load them. - On non-classic pages, such as in the Service Portal, use server-side script to get the content of the UI Scripts, and output it into a script tag using the appropriate Angular/Portal conventions.
Wrap up
I hope this brief overview of the available types of Extension Points was helpful. If you take nothing else away from this, I hope you at least know that you need 4 things to successfully utilize an Extension Point:
- A definition of the extension point, which provides the interface everyone must adhere to when the implement the extension.
- A concrete implementation of the interface which actually does some work.
- A registration record which links the concrete implementation to the interface.
- Some code which executes the extension point.
You have a couple of options for getting the output of an Extension Point into a rendered page. You can also mix-and-match because there is scriptable access to extension points with the three classes:
- GlideScriptedExtensionPoint
- GlideUIExtensionPoint
- GlideClientExtensionPoint
All are called on the server and have a method named getExtensions
which takes the name of the extension point and returns something.
You may need to utilize other features of the platform to do something useful with an extension point depending on your use-case. Don't forget that you can write client-callable Script Includes, you can utilize the REST endpoints I described, you can directly inject scripts using the scriptable classes in appropriate places for the UI you are presenting.
- 1,558 Views
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.