Asynchronous onSubmit Catalog/Client Scripts in ServiceNow
I often get questions that go something like this:
When a user submits a Catalog Item, I need to be able to do some validation that requires client-server communication (either a GlideAjax script, a GlideRecord query, or a getRefRecord() query). How can I make this happen?
As anyone who’s read my article on requiring attachments in the Service Portal knows, ServiceNow has been (for better or worse) making some changes to how you are supposed to, and able to, do certain things within the platform. What’s relevant to this question, is that ServiceNow does not allow synchronous client-server communication in the portal; which means that your catalog client scripts should not use synchronous versions of GlideAjax, GlideRecord, or getRefRecord().
This is fine, and generally good advice anyway. Synchronous client-server operations lock up the user’s browser, and typically make for a poor user-experience. However - onSubmit Client and Catalog Client Scripts need to run synchronously, because if they don’t return false, the form will submit, reload, and stop any scripts that are currently running (or waiting to run; such as your callback function).
This is a silly example that you probably wouldn’t ever want to use in real life, but consider the following code as an example of this issue.
function onSubmit() { var grUser = new GlideRecord('sys_user'); grUser.addQuery('sys_id', g_user.getUserID()); grUser.setLimit(1); grUser.query(validateUser);
} function validateUser(grUser) { if (grUser.getValue('title').indexOf('director') >= 0) { return true; } else { return false;
}
}
You might be inclined to think that this will work, but consider how it executes…
First, the variable grUser is declared and instantiated to a GlideRecord object. Then we add a query to it, set a limit, and send it on its way.
Once that query is complete, the callback function (which we passed into the .query() method) will be called. However, the onSubmit script is done. The next line is the close-brace that ends the onSubmit function, which essentially tells it to return; in this case, returning undefined.
The callback function is passed into the .query() method, but the script is finished running before the callback function is ever invoked, because once the onSubmit script is done running, the form submits and the page reloads! Even if the validateUser() function were to run and return true or false, it isn’t the same thing as the onSubmit script returning true or false, so it would have no bearing on whether the form can submit or not.
To put it simply: A callback function or other asynchronous operation cannot stop the form from submitting once the process has begun.
So how do we get around this problem, if we can’t use synchronous queries?
Well, one popular option is to add a hidden field on the catalog item, set the value of that field based on a separate onChange script, then have your onSubmit script read that field to determine if the form can be submitted. I don’t personally like this option, because there are a lot of moving parts, and it requires an additional unnecessary field which is always hidden.
A better option might be to use client-data, and a self-submitting callback function!
EDIT: ServiceNow has recently made a change which causes the original solution here, to no longer work on the Service Portal. This is because the g_user.setClientData() function is no longer available there… for some reason.
I’ve made some tweaks to the scripts below so that they should work with the Service Portal as well as the “classic view” no matter your version of ServiceNow.
The new functions, which you can implement in your client script to work in both CMS and portal UIs, are setClientData(), and getClientData(). The rest of the example has also been updated to work correctly in both UIs.
function onSubmit() { g_form.clearMessages(); if (getClientData('user_validated')) { return true; } var grUser = new GlideRecord('sys_user'); grUser.addQuery('sys_id', g_user.getUserID()); grUser.setLimit(1); grUser.query(validateUser); return false;
} function validateUser(grUser) { if (grUser.getValue('title').indexOf('director') >= 0) { setClientData('user_validated', true); g_form.orderNow(); } else { setClientData('user_validated', false); g_form.addErrorMessage('Some message about not being a valid user'); }
} function setClientData(key, val) { if (typeof g_user.setClientData != 'undefined') { g_user.setClientData(key, val); } else { if (typeof this.client_data == 'undefined') { this.client_data = {}; } this.client_data[key] = val; }
} function getClientData(key) { if (typeof g_user.getClientData != 'undefined') { return g_user.getClientData(key); } else { try { return (typeof this.client_data[key] == 'undefined' ? '' : this.client_data[key]); } catch(ex) { console.error('Error retrieving client data value ' + key + ': ' + ex.message); } } return '';
}
The code comments walk you through exactly what’s happening, but here’s the gist of what the script above is doing.
- The user clicks the save/submit button. The onSubmit script is triggered.
- Clear any existing messages at the top of the form (line 3).
- Check if there is a “client data” element that indicates that we have already performed our validation checks, and that it is set to true, indicating that the validation passed (ln 5).
- If the validation has already passed, simply return true and allow the form to submit (ln 6).
Otherwise, continue to the next step. - Query the sys_user table for the current user record. Pass in our callback function to the .query() method (ln 12)
- Return false, thus preventing submission (ln 14).
Note that this step comes before any of the code in the callback function runs. This is because the callback function executes asynchronously. - Once the query is complete, our callback function (validateUser()) executes.
- Perform the validation in the callback function (ln 19).
This specific validation is not something you’d do in real life, it’s just an example - If the validation passes, set the client data user_validated property to true (ln 21) and re-submit the form (ln 23). This returns you to step 2 in this list.
- Otherwise if the validation failed, set user_validated to false (ln 26) and add an error message to the top of the form (ln 28).
Do not re-submit the form if the validation fails.
- Otherwise if the validation failed, set user_validated to false (ln 26) and add an error message to the top of the form (ln 28).
Use this method; it’s way cooler.
Huzzah; now I have a link that I can send people when they ask me this question. :-D
- March 2024
- February 2024
- Feb 12, 2024 5 Lessons About Programming From Richard Feynman
- July 2023
- May 2023
- April 2023
- December 2022
- Dec 13, 2022 ServiceNow Developers: BE THE GUIDE!
- October 2022
- August 2022
- March 2022
- February 2022
- May 2021
- April 2021
- February 2021
- November 2020
- Nov 17, 2020 SN Guys is now part of Jahnel Group!
- September 2020
- July 2020
- January 2020
- Jan 20, 2020 Getting Help from the ServiceNow Community
- December 2019
- November 2019
- April 2019
- March 2019
- February 2019
- Feb 27, 2019 Making Update Sets Smarter - Free Tool
- November 2018
- October 2018
- September 2018
- July 2018
- Jul 23, 2018 Admin Duty Separation with a Single Account
- June 2018
- May 2018
- May 29, 2018 Learning ServiceNow: Second Edition!
- April 2018
- March 2018
- February 2018
- Feb 11, 2018 We have a new book!
- November 2017
- September 2017
- Sep 12, 2017 Handling TimeZones in ServiceNow (TimeZoneUtil)
- July 2017
- June 2017
- May 2017
- April 2017
- March 2017
- Mar 12, 2017 reCAPTCHA in ServiceNow CMS/Service Portal
- December 2016
- November 2016
- Nov 10, 2016 Chrome Extension: Load in ServiceNow Frame
- September 2016
- July 2016
- May 2016
- May 17, 2016 What's New in Helsinki?
- April 2016
- March 2016
- February 2016
- January 2016
- December 2015
- October 2015
- August 2015
- Aug 27, 2015 Easily Clone One User's Access to Another User
https://snprotips.com/blog/2018/10/19/synchronous-lite-onsubmit-catalogclient-scripts