Please use docs.servicenow.com for the latest documentation.

This site is for reference purposes only and may not be accurate for the latest ServiceNow version

Client Script Best Practices

From Wiki Archive
Jump to: navigation, search


This article applies to Fuji. For more current information, see Client Scripting Best Practices at https://developer.servicenow.com

The ServiceNow Wiki is no longer being updated. Please refer to the Developer Portal for the latest information.

Overview

A client script is a piece of JavaScript code that runs on the client, rather than the server. Well-designed client scripts can reduce the amount of time it takes to complete a form. However, improperly implemented client scripts can significantly slow down form load times. With the exception of the CellEdit client script, client scripts apply to forms only

Follow these best practices to ensure that client scripts work efficiently.

Restrict List Editing

With the exception of onCellEdit client scripts, UI policies and client scripts apply to forms only. If you create UI policies or client scripts for fields on a form, you must use another method to ensure that data in those fields is similarly controlled in a list. You can:

See Setting Field Values in Forms and Lists for a comparison of client scripts and business rules.

Minimize Server Lookups

Client scripting uses either data available on the client or data retrieved from the server. Use client data as much as possible to eliminate the need for time-consuming server lookups. The top ways to get information from the server are g_scratchpad, and asynchronous GlideAjax lookup.

The primary difference between these methods is that g_scratchpad is sent once when a form is loaded (information is pushed from the server to the client), whereas GlideAjax is dynamically triggered when the client requests information from the server.

Other methods, GlideRecord and g_form.getReference() are also available for retrieving server information. However, these methods are no longer recommended due to their performance impact. Both methods retrieve all fields in the requested GlideRecord when most cases only require one field.

Example: g_scratchpad

The g_scratchpad object passes information from the server to the client, such as when the client requires information not available on the form. For example, if you have a client script that needs to access the field u_retrieve, and the field is not on the form, the data is not available to the client script. A typical solution to this situation is to place the field on the form and then always hide it with a client script or UI policy. While this solution may be faster to configure, it is slower to execute.

If you know what information the client needs from the server before the form is loaded, a display business rule can create g_scratchpad properties to hold this information. The g_scratchpad is sent to the client when the form is requested, making it available to all client-side scripting methods. This is a very efficient means of sending information from the server to the client. However, you can only load data this way when the form is loaded. The business rule cannot be triggered dynamically. In those cases, use an asynchronous GlideAjax call.

For example, assume you open an incident and need to pass this information to the client:

  • The value of the system property css.base.color
  • Whether or not the current record has attachments
  • The name of the caller's manager

A display business rule sends this information to the client using the following script:

<source lang="javascript"> g_scratchpad.css = gs.getProperty('css.base.color'); g_scratchpad.hasAttachments = current.hasAttachments(); g_scratchpad.managerName = current.caller_id.manager.getDisplayValue(); </source>

To access scratchpad data using a client script:

<source lang="javascript"> // Check if the form has attachments if (g_scratchpad.hasAttachments)

   // do something interesting here

else

   alert('You need to attach a form signed by ' + g_scratchpad.managerName);

</source>

Example: Asynchronous GlideAjax

This script compares the support group of the CI and the assignment group of the incident by name:

<source lang="javascript">//Alert if the assignment groups name matches the support group function onChange(control, oldValue, newValue, isLoading) {

   if (isLoading)
       return;
   var ga = new GlideAjax('ciCheck');
   ga.addParam('sysparm_name', 'getCiSupportGroup');
   ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
   ga.addParam('sysparm_ag', g_form.getValue('assignment_group'));
   ga.getXML(doAlert); // Always try to use asynchronous (getXML) calls rather than synchronous (getXMLWait)

}

// Callback function to process the response returned from the server function doAlert(response) {

   var answer = response.responseXML.documentElement.getAttribute("answer");
   alert(answer);

} </source>

This script relies on an accompanying script include, such as:

<source lang="javascript"> var ciCheck = Class.create();

ciCheck.prototype = Object.extendsObject(AbstractAjaxProcessor, {

   getCiSupportGroup: function() {
       
       var retVal = ; // Return value
       var ciID   = this.getParameter('sysparm_ci');
       var agID   = this.getParameter('sysparm_ag');		
       var ciRec  = new GlideRecord('cmdb_ci');
       
       // If we can read the record, check if the sys_ids match
       if (ciRec.get(ciID)) {
           if (ciRec.getValue('support_group') == agID)
               retVal = 'CI support group and assignment group match';
           else
               retVal = 'CI support group and assignment group do not match';
           
           // Can't read the CI, then they don't match
       } else {
           retVal = 'CI support group and assignment group do not match';
       }
       
       return retVal;
   }
   

}); </source>

Use setValue()'s displayValue Parameter with Reference Fields

When using setValue() on a reference field, be sure to include the displayValue with the value (sys_id). If you set the value without the displayValue, the system does a synchronous Ajax call to retrieve the displayValue for the record you specified. This extra round trip to the server can leave you at risk of performance issues.

Example:

Incorrect:

<source lang="javascript"> var id = '5137153cc611227c000bbd1bd8cd2005';

g_form.setValue('assigned_to', id); // Client needs to go back to the server to

                                   // fetch the name that goes with this ID

</source>

Correct: <source lang="javascript"> var id = '5137153cc611227c000bbd1bd8cd2005'; var name = 'Fred Luddy';

g_form.setValue('assigned_to', id, name); // No server call required </source>

Use UI Policies Instead of Client Scripts

When possible, consider using a UI policy instead of a client script for these reasons:

  • UI policies have an Order field to allow full control over the order in which client-side operations take place.
  • UI policies do not require scripting to make a field mandatory, read-only, or visible.
Note
Note: UI policies execute after client scripts.


Use Client Scripts to Validate Data

An excellent use for client scripts is validating input from the user. This validation improves the user experience because the user finds out if there are data issues before submitting the information. An example of validation is to verify that the Impact field value is valid with the Priority field value. In this example, Low impact is not allowed with High priority.

<source lang="javascript"> if (g_form.getValue('impact') == '3' && g_form.getValue('priority') == '1')

  g_form.showFieldMsg('impact', gs.getMessage('Low impact now allowed with High priority'), 'error');

</source>

Avoid Global Client Scripts

A global client script is any client script where the selected Table is Global. Global client scripts have no table restrictions, therefore they will load on every page in the system introducing browser load delay in the process. There is no benefit to loading this kind of scripts on every page.

As an alternative, and for a more modular and scalable approach, consider moving client scripts to a base table (such as Task[task] or Configuration Item[cmdb_ci]) that can be inherited for all the child/extending tables. This eliminates the system loading the scripts on every form in the UI - such as home pages or Service Catalog where they are rarely (if ever) needed.

Avoid DOM Manipulation

Avoid Document Object Model (DOM) manipulation if possible. It can cause a maintainability issue when browsers are updated.

Instead, use the GlideForm API or consider a different approach for the solution. In general, when using DOM manipulation methods, you have to reference an element in the DOM by id or using a CSS selector. When referencing out-of-box DOM elements, there is a risk that the element ID or placement within the DOM could change thus causing the code to stop working and/or generate errors. While we are not saying this should never be done, it needs to be done with forethought, caution, and a full understanding of the risk you are incurring. It is recommended to review these objects and reduce the use of DOM manipulation methods as much as possible.

Enclose Code in Functions

Enclose the code in a client script inside a function. Client scripts without a function cause issues with variable scope. When code is not enclosed in a function, variables and other objects are available and shared to all other client-side scripts. So if you're using the same variable names, it's possible that they could collide. This can lead to unexpected consequences that are difficult to troubleshoot. Consider this example:

<source lang="javascript">

var state = "6";

function onSubmit() {

  if(g_form.getValue('incident_state') == state) {
  		alert("This incident is Resolved");
  }

} </source>

Because the state variable is not enclosed in a function, all client-side scripts, have access to it. Other scripts may also use the common variable name state. The duplicate names can conflict and lead to unexpected results. These issues are very difficult to isolate and resolve. To avoid this issue, ensure all you code is wrapped in a function:

<source lang="javascript"> function onSubmit() {

  var state = "6";
  if(g_form.getValue('incident_state') == state) {
  		alert("This incident is Resolved");
  }

} </source>

This solution is much safer because the scope of the variable state is limited to the onSubmit() function. Therefore, the state variable does not conflict with state variables in other client-side scripts.

Run Only Necessary Scripts

Remember that client scripts have no Condition field. This means that onLoad() and onChange() scripts run in their entirety every time the appropriate form is loaded. To avoid running time-consuming scripts unnecessarily, make sure that client scripts perform only necessary tasks.

This example is an inefficient onChange() client script set to run when the Configuration item field changes.

<source lang="javascript"> //Set Assignment Group to CI's support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading) {

   var ciSupportGroup = g_form.getReference('cmdb_ci').support_group;
   if (ciSupportGroup !=  && g_form.getValue('assignment_group) != )
       g_form.setValue('assignment_group', ciRec.support_group.sys_id);

} </source>

Note
Note: Ensure the Script field is not empty. This sends unnecessary code to the browser to process.


The following steps provide examples showing how this example can be improved to prevent the client script from running unnecessary code.

General Cleanup

Look for performance optimizations. In the previous example, the getReference(), or GlideRecord lookup can be replaced with an asynchronous GlideAjax call.

<source lang="javascript">//Set Assignment Group to support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading) {

   var ga = new GlideAjax('ciCheck');
   ga.addParam('sysparm_name', 'getSupportGroup');
   ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
   ga.getXML(setAssignmentGroup);

}

function setAssignmentGroup(response) {

   var answer = response.responseXML.documentElement.getAttribute("answer");
   g_form.setValue('assignment_group', answer);

} </source>

Add the isLoading Check (onChange Scripts Only)

The isLoading flag is the simplest way to prevent unnecessary code from taking up browser time. The isLoading flag should be used at the beginning of any script that is not required to run when the form is loading. There is no need to run this script on a form load because the logic would have already run when the field was last changed. Adding the isLoading check to the script prevents it from doing a cmdb_ci lookup on every form load.

<source lang="javascript">//Set Assignment Group to CI's support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading, isTemplate) {

   if (isLoading)
       return;
   var ga = new GlideAjax('ciCheck');
   ga.addParam('sysparm_name', 'getSupportGroup');
   ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
   ga.getXML(setAssignmentGroup);

}

function setAssignmentGroup(response) {

   var answer = response.responseXML.documentElement.getAttribute("answer");
   g_form.setValue('assignment_group', answer);

} </source>

If the onChange script should run during loading, use the following convention:

<source lang="javascript"> function onChange(control, oldValue, newValue, isLoading, isTemplate) {

   if (isLoading) {}; // run during loading

   // rest of script here

} </source>

Note
Note: The isLoading condition statement is part of the default template. It is illustrated here for clarity.


Add the newValue Check

The newValue check tells this script to continue only if there is a valid value in the relevant field. This prevents the script from running when the field value is removed or blanked out. This also ensures that there will always be a valid value available when the rest of the script runs.

<source lang="javascript">//Set Assignment Group to CI's support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading, isTemplate) {

   if (isLoading)
       return;
   if (newValue) {
       var ga = new GlideAjax('ciCheck');
       ga.addParam('sysparm_name', 'getSupportGroup');
       ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
       ga.getXML(setAssignmentGroup);
   }

}

function setAssignmentGroup(response) {

  var answer = response.responseXML.documentElement.getAttribute("answer");
  g_form.setValue('assignment_group', answer);

} </source>

Note
Note: The newValue condition statement is part of the default template. It was illustrated here for clarity.


Add the newValue != oldValue check

To have the script react to a value that changes after the form loads, use the newValue != oldValue check.

<source lang="javascript">//Set Assignment Group to CI's support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading, isTemplate) {

   if (isLoading)
       return;
   if (newValue) {
       if (newValue != oldValue) {
           var ga = new GlideAjax('ciCheck');
           ga.addParam('sysparm_name', 'getSupportGroup');
           ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
           ga.getXML(setAssignmentGroup);
       }
   }

}

function setAssignmentGroup(response) {

  var answer = response.responseXML.documentElement.getAttribute("answer");
  g_form.setValue('assignment_group', answer);

} </source>

Bury the GlideAjax Call

In this example, the GlideAjax call is buried one level deeper by rearranging the script to check as many things available to the client as possible before running the server calls. The script checks the assignment before executing the GlideAjax call. This prevents the server lookup when the assignment_group field is already set.

<source lang="javascript">//Set Assignment Group to CI's support group if assignment group is empty function onChange(control, oldValue, newValue, isLoading, isTemplate) {

   if (isLoading)
      return;
   if (newValue) {
       if (newValue != oldValue) {
           if (g_form.getValue('assignment_group') == ) {
               var ga = new GlideAjax('ciCheck');
               ga.addParam('sysparm_name', 'getSupportGroup');
               ga.addParam('sysparm_ci', g_form.getValue('cmdb_ci'));
               ga.getXML(setAssignmentGroup);
           }
       }
   }

}

function setAssignmentGroup(response) {

  var answer = response.responseXML.documentElement.getAttribute("answer");
  g_form.setValue('assignment_group', answer);

} </source>