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).
The problem
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); } //Callback function function validateUser(grUser) { //This function could be one line but // I've written it this way for clarity if (grUser.getValue('title').indexOf('director') >= 0) { //Allow submission return true; } else { //disallow submission 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.
The solution
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() { //Hide any existing messages from previous validation attempts g_form.clearMessages(); //If the user was validated, stop here and submit the form. if (getClientData('user_validated')) { //getClientData implemented below return true; } //If the user has not yet been validated, do so var grUser = new GlideRecord('sys_user'); grUser.addQuery('sys_id', g_user.getUserID()); grUser.setLimit(1); //Query and pass in the callback function grUser.query(validateUser); //then return false to halt submission (for now) return false; } function validateUser(grUser) { //Perform validation if (grUser.getValue('title').indexOf('director') >= 0) { //If the user is valid, set client data user_validated to true setClientData('user_validated', true); //then re-submit g_form.orderNow(); //(This will trigger line 6 above, and submit the form!) } else { //If the validation failed... //set the client data user_validated element to false setClientData('user_validated', false); //and warn the user, but do not re-submit the form. g_form.addErrorMessage('Some message about not being a valid user'); } } /** * Sets a client data variable. Works on both classic UI (using legacy g_user.setClientData() method) and portal UI (using this.client_data). * @param {string} key - The key to store the data in. Use this with getClientData() to retrieve the value stored here. * @param {string} val - The value to store in the specified key. */ 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; } } /** * Gets a client data variable, stored using setClientData(). Works on both classic UI (using legacy g_user.getClientData() method) and portal UI (using this.client_data). * @param {string} key - The key to the value you'd like to retrieve. * @returns {string} */ 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 exampleIf 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.
Conclusion
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
- Aug 23, 2022 Using .addJoinQuery() & How to Query Records with Attachments in ServiceNow
- Aug 18, 2022 Free, Simple URL Shortener for ServiceNow Nerds (snc.guru)
- Aug 16, 2022 How to Get and Parse ServiceNow Journal Entries as Strings/HTML
- Aug 14, 2022 New tool: Get Latest Version of ServiceNow Docs Page
- 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
- Apr 21, 2019 Understanding Attachments in ServiceNow
- Apr 10, 2019 Using Custom Search Engines in Chrome to Quickly Navigate ServiceNow
- Apr 4, 2019 Set Catalog Variables from URL Params (Free tool)
- Apr 1, 2019 Outlook for Android Breaks Email Approvals (+Solution)
- March 2019
-
February 2019
- Feb 27, 2019 Making Update Sets Smarter - Free Tool
-
November 2018
- Nov 29, 2018 How to Learn ServiceNow
- Nov 6, 2018 ServiceNow & ITSM as a Career?
- October 2018
- September 2018
-
July 2018
- Jul 23, 2018 Admin Duty Separation with a Single Account
-
June 2018
- Jun 19, 2018 Improving Performance on Older Instances with Table Rotation
- Jun 4, 2018 New Free Tool: Login Link Generator
-
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
- Jun 25, 2017 What's New in ServiceNow: Jakarta (Pt. 1)
- Jun 4, 2017 Powerful Scripted Text Search in ServiceNow
- May 2017
- April 2017
-
March 2017
- Mar 12, 2017 reCAPTCHA in ServiceNow CMS/Service Portal
-
December 2016
- Dec 20, 2016 Pro Tip: Use updateMultiple() for Maximum Efficiency!
- Dec 2, 2016 We're Writing a Book!
-
November 2016
- Nov 10, 2016 Chrome Extension: Load in ServiceNow Frame
- September 2016
-
July 2016
- Jul 17, 2016 Granting Temporary Roles/Groups in ServiceNow
- Jul 15, 2016 Scripted REST APIs & Retrieving RITM Variables via SRAPI
-
May 2016
- May 17, 2016 What's New in Helsinki?
-
April 2016
- Apr 27, 2016 Customizing UI16 Through CSS and System Properties
- Apr 5, 2016 ServiceNow Versions: Express Vs. Enterprise
-
March 2016
- Mar 28, 2016 Update Set Collision Avoidance Tool: V2
- Mar 18, 2016 ServiceNow: What's New in Geneva & UI16 (Pt. 2)
-
February 2016
- Feb 22, 2016 Reference Field Auto-Complete Attributes
- Feb 6, 2016 GlideRecord & GlideAjax: Client-Side Vs. Server-Side
- Feb 1, 2016 Make Your Log Entries Easier to Find
-
January 2016
- Jan 29, 2016 A Better, One-Click Approval
- Jan 25, 2016 Quickly Move Changes Between Update Sets
- Jan 20, 2016 Customize the Reference Icon Pop-up
- Jan 7, 2016 ServiceNow: Geneva & UI16 - What's new
- Jan 4, 2016 Detect/Prevent Update Set Conflicts Before They Happen
-
December 2015
- Dec 28, 2015 SN101: Boolean logic and ServiceNow's Condition Builder
- Dec 17, 2015 Locate any record in any table, by sys_id in ServiceNow
- Dec 16, 2015 Detecting Duplicate Records with GlideAggregate
- Dec 11, 2015 Array.indexOf() not working in ServiceNow - Solution!
- Dec 2, 2015 Understanding Dynamic Filters & Checking a Record Against a Filter Using GlideFilter
- October 2015
-
August 2015
- Aug 27, 2015 Easily Clone One User's Access to Another User