Note: Want to download an update set containing a packaged version of this functionality? Skip to the bottom! But don't forget to at least check out the catalog client script for a usage example.
Unless you're somehow still rocking UI15 and do most of your development on a Commodore 64, there's a good chance that by now you're at least vaguely familiar with ServiceNow's new(-ish) Angularized end-user front-end feature: the Service Portal.
If you've tried to implement your Service catalog in the Service Portal, there's also a good chance that you've wept openly over your keyboard, trying to find an effective and non-hackey way to replicate functionality that was readily available using out-of-box APIs in the old Jelly-based CMS and catalog form. Stuff you'd think would be really basic, especially two full major releases later, it just not possible in the Service Portal. g_form APIs like getControl() and getElement() (and gel()) don't exist in Service Portal. Neither does any synchronous GlideRecord query or GlideAjax call, meaning that if you want to check something on the server in an onSubmit() script, you're out of luck.
One of the biggest annoyances resulting from this, is that there is no good way to check if a catalog item has attachments as the user submits the form. This means that you cannot make attachments required on a specific catalog item.
Oh sure, there are sort of ways to do it, such as modifying the catalog item widget just for that catalog item, so that the client controller and server script work together to check for attachments. Or you could embed the whole CMS or ITIL UI catalog item form in an iframe. Unfortunately, both of those have major downsides.
I've made this solution, in order to restore some basic functionality, with a mind toward the specific problem of checking for attachments in the Service Portal.
Here is a basic feature-list of the below functionality:
Require attachments on submission, using sp_form.getAttachments()
Require attachments of a specific type
Require a specific number of attachments
Require a specific number of attachments of various types
Access DOM element of variable field input box and div element, using sp_form.getElement() and sp_form.getControl()
Access variable names, sys_ids, and question text, using sp_form.variables or sp_form.getVariables().
Iterate over each variable on the page, or otherwise retrieve variable sys_IDs, names, and question text without having prior knowledge of each within your catalog client script.
In order to enable some of the functionality we're used to, we have to make use of a method which you may be familiar with from my previous article on enabling DOM access in the service portal; but first, let's create a Script Include that we can make use of to enable a query we're going to need to run later.
Script Include
Name: CatItemVariables
Client Callable: True
Script: See below:
var CatItemVariables = Class.create(); CatItemVariables.prototype = Object.extendsObject(AbstractAjaxProcessor, { getSysIdsForQuery: function(itemSid, fieldName) { if (!fieldName) { fieldName = 'sys_id'; } var a = []; var item = GlideappCatalogItem.get(itemSid); var variables = item.getVariables(); while (variables.next()) { a.push(variables[fieldName].toString()); } return a.join(','); }, type: 'CatItemVariables' });
In this client-callable Script Include, we accept two arguments: a catalog item, and (optionally) a field name to retrieve for each one. We then create an array, and populate it with that field from each catalog item variable associated with the indicated catalog item. We're going to use this to build a query string later on.
Next, we need to build a UI Script that's going to be included as part of a JS Include, which we'll associate with the Theme that's included on the Portal on which our Service Catalog will be used. This UI Script will primarily be responsible for constructing the sp_form object, and its' various API methods. Here's how to construct that UI Script:
UI Script
Name: sp_form
Global: False
Description: I recommend writing a reasonable description of this script, as with all useful scripts, so future developers will know what it's for.
Script: See below:
/*======== UI SCRIPT FOR JS INCLUDE ========*/ var sp_form = { /** * Get an array of all the attachments associated with the displayed catalog item. * FOR USE IN SERVICE PORTAL ONLY. * MUST BE ATTACHED TO SERVICE PORTAL THEME, AS A JS INCLUDE. * @returns {_CatalogAttachment} */ getAttachments: function() { var i, attachmentElement; //init vars for the loop var catalogAttachments = []; //This will store the CatalogAttachment records, constructed from the loop over each attachment // identified from the angular call var attachmentElements = angular.element("#sc_cat_item").scope().attachments; //Use some JS Include scope magic with a nod to // angular, to get the list of attachments. for (i = 0; i < attachmentElements.length; i++) { //For each attachment document element returned from the angular call attachmentElement = attachmentElements[i]; //Grab a single element for each loop //Push a constructed CatalogAttachment object into the array, with properties corresponding to the relevant attachment // properties. catalogAttachments.push( //Construct a new CatalogAttachment object, for inclusion in the returned array of attachment data. new this._CatalogAttachment( attachmentElement.file_name, attachmentElement.ext, attachmentElement.sys_id, attachmentElement.size ) ); } return catalogAttachments; }, /** * Constructs a custom CatalogAttachment object * @param file_name {string} The name of the file, INCLUDING the extension. e.g.: 'test_file.csv'. * @param file_extension {string} The file extension, WITHOUT the dot. e.g.: 'xls'. * @param sysID {string} the sys_id of the attachment record (not to be confused with the table_sys_id) * @param file_size {string} the size of the attachment, in KB. e.g.: "13.3 KB". * @constructor */ _CatalogAttachment: function(file_name, file_extension, sysID, file_size) { this.file_name = file_name; this.file_extension = file_extension; this.sysID = sysID; this.file_size = file_size; } }; getVariables(); /** * This function extends the sp_form object, and adds the 'variables' object, as well as the getElement() and getControl() methods. */ function getVariables() { var i, varz, vSid, vName, vLabel, catalogItemSid; var itemVariables = {}; var hostLocation = window.location.host + ''; var sidBegin = window.location.search.indexOf('sys_id=') + 7; var sidEnd = window.location.search.indexOf('&', sidBegin); if (sidEnd >= 0) { catalogItemSid = window.location.search.slice(sidBegin, sidEnd); } else { catalogItemSid = window.location.search.slice(sidBegin); } var requestBody = ""; var client = new XMLHttpRequest(); //Updated to also get variables from variable sets on the client.open("get", "https://" + hostLocation + "/api/now/table/item_option_new?sysparm_query=sys_idINjavascript%3Anew%20CatItemVariables().getSysIdsForQuery('" + catalogItemSid + "', 'sys_id')" + "%5Eactive%3Dtrue&sysparm_fields=sys_id%2Cname%2Cquestion_text&sysparm_limit=100"); client.setRequestHeader('Accept', 'application/json'); client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('X-UserToken', g_ck); client.onreadystatechange = function() { if (this.readyState == this.DONE) { if (this.status == 200 || this.status == 201) { varz = JSON.parse(this.response).result; for (i = 0; i < varz.length; i++) { vSid = varz[i].sys_id; vName = varz[i].name; vLabel = varz[i].question_text; itemVariables[vName] = new CatalogItemVariable( varz[i].sys_id, varz[i].name, varz[i].question_text ); } sp_form.variables = itemVariables; sp_form.getElement = function(varName) { return document.getElementsByName('IO:' + sp_form.variables[varName].sid)[0]; }; sp_form.getControl = function(varName) { return document.getElementById(sp_form.variables[varName].sid); }; } else { console.error('Some kind of REST error happened. Error: ' + this.status); } } sp_form.getVariables = function() { return sp_form.variables; } }; client.send(requestBody); } /** * @description Constructor function for building CatalogItemVariables * @param variableName {string} The name (not to be confused with the label/question) of the variable. * @param variableSysID {string} The catalog variable record's sys_id. * @param variableQuestion {string} The question/label of the variable itself. May contain spaces. * @constructor */ function CatalogItemVariable(variableName, variableSysID, variableQuestion) { this.name = variableName; this.sys_id = variableSysID; this.question = variableQuestion; }
This UI Script is a little complicated, but here's a basic run-down of what it's doing:
First, we construct an object called sp_form. We give it a method called getAttachments(), and a private constructor method for building custom CatalogAttachment objects, which we use in the getAttachments() method. This constructor just builds an object with a known set of properties associated with Catalog Attachments, so we don't have to keep declaring new generic objects for each one.
After initializing the sp_form object, we then want to build on it to include a list of variables and the methods for accessing them as well as for accessing elements based on variable names. This is accomplished by the getVariables() function. Since DOM elements in the service portal are given IDs and names based on the variable sys_IDs rather than names, we need to get the sys_ID for each variable as well. That is also handled by the getVariables().
The getVariables() function in this script is responsible for making a REST call using vanilla JavaScript (since GlideAjax isn't available to JS Includes). This REST call will retrieve the list of variables associated with the specified Catalog Item, using a query that utilizes the Script Include we defined first in this article.
Once we've created the necessary UI Script, we'll need to create a JS Include as a wrapper, which we'll then associate with the Theme for our Service Portal. Follow the steps below, to set this up.
JS Include
Navigate to the sp_js_include table, by entering "sp_js_include.list" into the Application Navigator's filter bar, and pressing Enter. Click New to create a new JS Include record.
Enter sp_form in the Display name field.
Make sure Source is set to UI Script, then select the sp_form UI Script by entering it into the Source field.
Save the JS Include record.
Next, associate the JS Include with a given Theme.
Navigate to Service Portal > Portals (the sp_portal table).
Select the Service Portal you'd like to make your Service Catalog available in.
On the record you select, you'll see a Theme field. Click on the reference icon to navigate to this Theme record.
Note that you may need to click the reference icon, then click Open record on the sys_popup view.
Once on the Theme record, scroll down and select the JS Includes related list. If you don't see this list, add it.
Click on the Edit button, and move the sp_form JS Include from the left side, to the right; then click Save.
That's it! Now your JS Include (And thus, your sp_form UI Script) will load on every page on that Service Portal. This JS Include is relatively small, will be optimized by ServiceNow, and uses an asynchronous REST request, so the performance impact will be negligible.
Now, all that's left is to create a Catalog Client Script that makes use of this new sp_form API. Of course, this will vary greatly based on what you want to accomplish with the UI Script, but I'll provide an example below. Note that for the Catalog Client Script to run on the Service Portal, the UI Type field must be either Mobile / Service Portal, or All. If you select All, your script should work on both the Service Portal, and CMS view. Desktop Catalog Client Scripts will not run on the Service Portal. This is a bit of a misnomer, because of course the Service Portal can run on "desktops" as well.
Catalog Client Script
Name: Require CSV Attachment
UI Type: All
Type: onSubmit
Applies on Catalog Item view: True
Script: See below:
/*======== EXAMPLE CLIENT SCRIPT: REQUIRE CSV ATTACHMENT ON SUBMIT ========*/ function onSubmit() { if (document && gel) { //If we're in the CMS view var cat_id = gel('sysparm_item_guid').value; //Not idea, but there's no better way at present. var gr = new GlideRecord("sys_attachment"); gr.addQuery("table_name", "sc_cart_item"); //unnecessary, but improves query efficiency gr.addQuery("table_sys_id", cat_id); gr.addQuery('file_name', 'ENDSWITH', '.csv'); //Optional: require a specific attachment type. gr.query(); //synchronous for non-service portal CMS. if (!gr.hasNext()) { g_form.addErrorMessage("You must attach a file to submit."); return false; } } else if (sp_form.getAttachments) { //If we're in a service portal page, and we can find the sp_form object with the getAttachments() // method, which should be included via a JS Include on the service portal we're rendering this in var i; //iterator for the below for-loop var hasCorrectAttachments = false; //initialized to false, but will be set to true if the loop identifies a valid attachment var scAttachments = sp_form.getAttachments(); //This function MUST be exposed via a non-global UI Script included in a JS Include //constants for required attachment type and quantity. var REQUIREDATTACHMENTCOUNT = 1; //optional: the number of required attachments var REQUIREDATTACHMENTTYPE = 'csv'; //optional: the required attachment file extension (no 'dot') attached to the service portal's // theme, for this to work in service portal. if (scAttachments.length >= REQUIREDATTACHMENTCOUNT) { //If we have at least a sufficient number of attachments for (i = 0; i < scAttachments.length; i++) { //Iterate over each returned attachment if (scAttachments[i].file_extension.toLowerCase() === REQUIREDATTACHMENTTYPE.toLowerCase()) { //Make sure at least one attachment has the required file-type hasCorrectAttachments = true; //If we have a correct file-type attachment, prevent the error break; //and break out of the loop for efficiency } } } if (!hasCorrectAttachments) { //If there are not enough attachments, or if there is no attachment of the required type g_form.addErrorMessage('Please attach at least ' + REQUIREDATTACHMENTCOUNT + ' ' + REQUIREDATTACHMENTTYPE + ' attachment(s)'); return false; } } else { console.error( 'Unable to find either gel, document, or getSCAttachments method. \n' + 'If you are viewing this in the Service Portal, be sure to add the ClientCatalogUtils UI Script JS Include to the Service Portal\'s theme.' + 'If you are in the CMS, then an error has occurred and will require troubleshooting.' ); return false; } }
The first thing we're doing in this Catalog Client Script, is checking if we have access to document and gel(). If we have access to those, then we're not in the Service Portal, so we use the normal method of checking for attachments.
If we do not locate those objects however, then we assume that we are in the Service Portal, and we check to make sure that sp_form and the getAttachments() API are both present (line 14). If not, we throw an error to the console but we don't raise one to the level that the user would see it. You can change this behavior by adding throw new Error(); to a new line below line 40.
If our sp_form API is present, then we use it to get the list of attachments on the current catalog item. Since this example is looking for at least one CSV attachment, we then iterate over each attachment and check its' extension. If the extension is "CSV", then we break out of the loop and allow the form to be submitted. However, if we do not find any such attachment, or if the required number of attachments are not present (though in this case, only one is required), we return false; thus halting submission. We also display an error to the user, alerting them to the attachment requirement.
Download Update Set
Don't feel like copy/pasting all that code? No problemo! Simply download the update set below, and follow these instructions to install it into your ServiceNow instance.
Download latest version
- 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