
- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 01-27-2020 06:02 PM
This is quite advanced but very cool as it allows you to use the flexibility of the service portal in the back-end of ServiceNow without the need for any Jelly XML. You will find a working version of this code in the attached update set
In essence, you can use a service catalog record producer as a custom form in a modal dialog box. That gives you a lot of flexibility as the record producer can be easily updated/changed, etc, and you escape the ugliness of Jelly XML.
Here's some screenshots of how it looks/works...
Add a button to the regular form
Launch a record producer within the service portal in a modal dialog box
Here's how you can do it...
Generic Portal
The first step is to set up a generic portal. The reason for this is you don't want users to be able to navigate away to other portal pages, so the generic portal is simply blank. There are no themes or navigation. I've added a "blank" page as the home page simply so if anyone accidentally reaches this portal they see a white page.
Once you've got a generic portal in place, simply create the record producer you want to use. Note, if this is only accessible to back-end process workers you might want to set up the user-criteria to reflect that so your new record producer isn't visible in the regular service portal.
Record Producer
In order to get information from our service portal record producer back onto the g_form, we're going to use session data. This is where things get interest. The first thing is we need to define what we're going to pass back to our g_form and put that in the script that runs once the record producer has created a new record.
var newUser = {displayValue: producer.first_name.toString() + ' ' + producer.last_name.toString(), value: current.sys_id.toString()};
gs.getSession().putClientData('newUser', JSON.stringify(newUser));
The important thing to note here is the NAME of the session variable we're passing to the client. In this particular case, it is newUser and it is an object with two attributes to capture the display value and sys_id (value). This is the glue that binds everything together. The process is going to be...
- User launches record producer in a modal dialog box
- User creates new record but how do we get that back to our g_form? Pop it in the session data
- Glide Modal Dialog closes and grabs that new value from session data and puts it on the form
Add button(s) and modal dialog box(es) to your g_form
Now, on our regular ServiceNow glide form we're going to add a button that launches our record producer in the generic portal. To do this, we're going to add an On Load client script (with Isolate Script turned off)
The following script has a few details you'll need to tweak for your instance.
- whichButton should be the field you want to attach this button to (it'll be tableName.fieldName)
- whichItem should be the sys_id of your record producer
- whichTitle is whatever you want to appear at the top of the modal dialog
- recordProdVariable MUST be the same as the variable name you passed to the session object (above)
//Add as many buttons as you need on your form as objects to this array
var buttons = [
{
whichButton:'element.incident.caller_id',
whichItem : 'cfc13a06db812300407f78eebf9619a6',
whichTitle:'Add New User',
recordProdVariable:'newUser'
}
];
var modalWidth = 1050; //default is 800 x 800
var modalHeight = 800;
////////////////////////////////////////////////////
//add a button after this field
function onLoad() {
buttons.forEach(function(button){
var baseElement = document.getElementById(button.whichButton);
var thisElement = jQuery(baseElement).find('.form-field-addons');
thisElement.css('width','25%');
thisElement.append('<input type="button" value="'+button.whichTitle+'" class="btn btn-default" style="width:160px;" onclick="openModal(\''+button.whichTitle+'\',\''+button.whichItem+'\', \''+button.recordProdVariable+'\', \''+button.whichButton+'\')">');
});
}
////////////////////////////////////////////////////
//open a record producer in a modal window
function openModal (whichTitle, whichItem, recordProdVariable, whichButton) {
var whichAddress = '/generic?id=sc_cat_item&sys_id='+whichItem+'&sysparm_variables=%7B\"type\":\"'+recordProdVariable+'\"%7D';
var angularModal;
angularModal = new GlideModal();
angularModal.setTitle(whichTitle);
angularModal.setWidth(modalWidth);
angularModal.setPreference('id','jModal');
angularModal.renderWithContent("<iframe id='iframe_modal' name='iframe_modal_name' width='100%' height='"+(modalHeight-80)+"px' frameBorder='0' src='"+whichAddress+"' onload='iframeAngularOnLoad(\""+recordProdVariable+"\", \""+whichButton+"\")'></iframe>");
}
////////////////////////////////////////////////////
//When the iframe loads, prepare to pass info back to the g_form
function iframeAngularOnLoad(recordProdVariable, whichButton){
var thisFrame = jQuery('iframe#iframe_modal');
jQuery('.modal-content').css("width", modalWidth);
jQuery('.modal-content').css("height", modalHeight);
var requestSuccess = jQuery('div.text-center.text-success' ,thisFrame[0].contentWindow.document.body);
var ticketOpened = jQuery("span:contains('Your request has been submitted')",thisFrame[0].contentWindow.document.body);
var notifications = jQuery('div#uiNotificationContainer' ,thisFrame[0].contentWindow.document.body);
//If either the request was successful or a new record was added close this modal window automagically
if(requestSuccess.length!=0 || ticketOpened.length!=0){
setTimeout(function(){
updateDetails(recordProdVariable, whichButton);
}, 1000);
}
//Sometimes new record only return an ajax notification to the user, so we will watch for these
notifications.bind('DOMSubtreeModified', function(thisElement) {
var messageType = "";
//Examine the change in angular notifications
for(var thisTarget in thisElement.target){
if(thisElement.target[thisTarget] !== null && thisElement.target[thisTarget] !== undefined && thisElement.target[thisTarget].hasOwnProperty('$spNotificationsController')){
thisElement.target[thisTarget]['$spNotificationsController'].notifications.forEach(function(notification){
if(notification.hasOwnProperty('type')){messageType = notification.type;}
//examine the property 'message' if more precise information is needed than just the 'type'
});
}
}
if(messageType=="info"){//If we got an information message about a successful entry, close the modal and update the form
setTimeout(function(){
updateDetails(recordProdVariable, whichButton);
}, 1000);
}
});
var thisJavascriptContext = window.frames[window.frames.length -1];
thisJavascriptContext.isAngularReady = function(){
if(thisJavascriptContext.angular.element('main').scope().page.title=='Loading...') {
window.setTimeout(function(){ thisJavascriptContext.isAngularReady(); },100);
}else{
console.log('%cThis is the "'+thisJavascriptContext.angular.element('main').scope().page.title + '" page', 'color:red');
console.log('%cThe angular scope for this page is...','color:red');
console.log(thisJavascriptContext.angular.element('main').scope());
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//Close the dialog if the page isn't in the service catalog
//(ie, the catalog item has been saved and the user has been redirected to a summary page outside the catalog)
if(thisJavascriptContext.angular.element('main').scope().page.static_title != 'Catalog Item'){updateDetails(recordProdVariable, whichButton);console.log('Update details successful');}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//Sometimes new record only return an ajax notification to the user, so we will watch for these
var main = jQuery('main',thisFrame[0].contentWindow.document.body);
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var myObserver = new MutationObserver (mutationHandler);
main.each ( function () {
myObserver.observe (this, { childList: false, characterData: false, attributes: true, subtree: false });
});
function mutationHandler (mutationRecords) {
mutationRecords.forEach ( function (mutation) {
if(mutation.target.nodeName=='MAIN' && mutation.attributeName=='data-page-id'){
console.log('!!page refreshed!! ');
updateDetails(recordProdVariable, whichButton);
}
} );
}
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////
};
thisJavascriptContext.isAngularReady();
}
function updateDetails(recordProdVariable, whichButton){
jQuery(window.document).find('button[data-dismiss=GlideModal]').trigger("click");
var ga = new GlideAjax('getSessionData');
ga.addParam('sysparm_name', 'fetch');
ga.addParam('sysparm_key', recordProdVariable);
ga.getXML(updateForm);
function updateForm(response) {
var answer = response.responseXML.documentElement.getAttribute("answer");
var newUser = JSON.parse(answer);
if(answer !== null && newUser.hasOwnProperty('displayValue')){
var buttonName = whichButton.split('.');
g_form.setValue(buttonName[2],newUser.value, newUser.displayValue);
}
}
}
And the glue that holds it all together...
This is where the magic happens... we now have a regular g_form with a modal dialog popup that brings up our record producer, but in order to get the information that was entered into the record producer back to our form we need the following script include.
var getSessionData = Class.create();
getSessionData.prototype = Object.extendsObject(AbstractAjaxProcessor, {
fetch: function(){
var thisKey = this.getParameter('sysparm_key');
var results = gs.getSession().getClientData(thisKey);
gs.getSession().clearClientData(thisKey);
return results;
},
type: 'getSessionData'
});
...and there you have it...
You should now have a regular ServiceNow form with a button after one of the fields. Pressing that button will launch a record producer in a generic portal. Any record created by that record producer then populates your ServiceNow form.
Here's an example where there are seven buttons (but only two record producers, one for people, the other for organisations). Clicking each button allows you to create a new user/organisation that is immediately populated on the g_form.
If you find this doesn't work, go back and make sure you've got all the components in place. The attached update set should help.
Have fun
- 8,667 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks for this write up... I've managed to get 80% of this working for my use case. The only missing part is the dialog closure and I'm wondering if you have any advice...
My scenario is a bit different. I'm firing from a UI Action on a related list. I used your example and have a headerless portal with a custom page and widget (cloned and modified from the OOB sc_cat_item page and widget). I successfully load the record producer in the modal but can't get it to close.
I'll admit as an initial prototype of this activity I skipped the session bit as it seemed you were doing more than I needed. I've been trying to simply replicate the click of the dismiss button as you did in your updateDetails function. In my case, I already needed to get some data back from the widget client script and into the record producer which works fine so I was hoping I could just add the modal close to the submit function that's already working.
Thoughts?

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi, it's always difficult to know the specifics of how someone is adapting code, but I hope this helps...
jQuery(window.document).find('button[data-dismiss=GlideModal]').trigger("click");
This is the line of code that closes the dialog box. It's just a question of WHEN you trigger it. Look to see which button the user last clicks or page refreshes, etc, to know when you should close the dialog box. You could potentially add an event to an existing button, or you could look for the page within the dialog refreshing with new content (which is the approach I used)
From the code above... whenever the iframe loads (which is when it first opens AND again after the form is submitted), the following variables are set to look for content on the page.
var requestSuccess = jQuery('div.text-center.text-success' ,thisFrame[0].contentWindow.document.body);
var ticketOpened = jQuery("span:contains('Your request has been submitted')",thisFrame[0].contentWindow.document.body);
var notifications = jQuery('div#uiNotificationContainer' ,thisFrame[0].contentWindow.document.body);
If the page contains these HTML elements then the code goes to updateDetails, and the first thing that happens in that section is the single line of code used to close the dialog.
//If either the request was successful or a new record was added close this modal window automagically
if(requestSuccess.length!=0 || ticketOpened.length!=0){
setTimeout(function(){
updateDetails(recordProdVariable, whichButton);
}, 1000);
}
So it's all about understanding WHERE you can intercept the normal process and take action, like closing the dialog.
I hope that helps
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
It looks like Chrome 127 broke some of this functionality, specifically the event DOMSubtreeModified has been removed. I have been able to use the replacement MutationOberserver to detect the notification, but haven't been able to figure out how to get the message or message type out of the mutation.
Had you noticed this change and come up with a solution?