client-scripts について
client-scriptsは、ServiceNowの開発者向けスキルで、ブラウザー上のフォーム動作、検証、動的な操作を実装します。スクリプトの種類、フォームAPI、GlideAjax、性能、モバイル対応、デバッグを扱います。
主な機能
- 01onLoad、onChange、onSubmit、onCellEditスクリプトの作成
- 02g_formによるフォームフィールドの操作
- 03g_userとg_scratchpadによるセッション情報の取得
- 04GlideAjaxによるサーバーロジックの非同期呼び出し
- 05スクリプトとサーバー呼び出しの最適化
- 06ブラウザーツールでのスクリプトのデバッグ
活用シーン
- 01送信前のフォームデータ検証
- 02連動するフィールド動作の実装
- 03同期呼び出しを避けたサーバーデータの取得
- 04モバイルとService Portal向けのフォームスクリプト調整
MCP 対応
SN-Create-RecordやSN-Query-TableなどのMCPツールで、ServiceNowのクライアントスクリプトやUIスクリプトのレコードを作成・確認する方法を示します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill client-scriptsインストール後、Claude Code で/client-scriptsを入力して読み込まれたか確認します。
詳細
クライアントスクリプト
概要
このスキルでは、ServiceNow におけるクライアントスクリプトの包括的な開発方法を扱います。
- クライアントスクリプトの種類:onLoad、onChange、onSubmit、onCellEdit
- フォーム操作のための g_form API
- セッションコンテキストを扱う g_user オブジェクトと g_scratchpad オブジェクト
- 非同期サーバー呼び出しのための GlideAjax
- パフォーマンスの最適化とベストプラクティス
- モバイル/Service Portal に関する考慮事項
- デバッグ手法と一般的なパターン
使用する場面: ブラウザーで JavaScript を実行して、フォームの動作を操作したり、データを検証したり、動的なユーザー操作を提供したりする必要がある場合です。
対象ユーザー: カスタムフォームの動作、検証、ユーザーエクスペリエンスの改善機能を構築する開発者です。
前提条件
- ロール:
client_script_adminまたはadmin - アクセス: sys_script_client、sys_ui_script テーブル
- 知識: JavaScript の基礎、ServiceNow のフォームアーキテクチャ
- 関連スキル: より簡単な表示/非表示/必須のロジックについては
catalog/ui-policies
クライアントスクリプトについて
スクリプトタイプの比較
| タイプ | トリガー | ユースケース | パフォーマンスへの影響 |
|---|---|---|---|
| onLoad | フォームの読み込み | フィールドの初期化、デフォルト値の設定 | 中 |
| onChange | フィールド値の変更 | フィールド間の依存関係、連動ロジック | 低~中 |
| onSubmit | フォームの送信 | 検証、確認 | 低 |
| onCellEdit | リストセルの編集 | リスト編集の検証 | 低 |
クライアントスクリプトと UI ポリシー
┌─────────────────────────────────────────────────────────────┐
│ Decision Matrix │
├─────────────────────────────────────────────────────────────┤
│ Need to show/hide/mandatory fields? │
│ YES → Use UI Policy (no code, easier maintenance) │
│ │
│ Need to set field values or complex logic? │
│ YES → Use Client Script │
│ │
│ Need server-side data? │
│ YES → Use Client Script with GlideAjax │
│ │
│ Need to prevent form submission? │
│ YES → Use Client Script (onSubmit) │
│ │
│ Simple field validation? │
│ Dictionary validation → UI Policy → Client Script │
└─────────────────────────────────────────────────────────────┘
実行順序
Form Load Sequence:
1. UI Policies (on load = true) evaluate
2. onLoad Client Scripts execute (by order)
3. Default values applied
4. Field-level ACLs applied
Field Change Sequence:
1. onChange Client Script for field executes
2. UI Policies with that field in condition re-evaluate
3. Related onchange handlers fire
Form Submit Sequence:
1. onSubmit Client Scripts execute (by order)
2. If all return true, form submits
3. Server-side business rules fire
手順
フェーズ 1:クライアントスクリプトの作成
ステップ 1.1:onLoad スクリプトを作成する
基本的な onLoad の構造:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Initialize Request Form"
table: incident
type: onLoad
script: |
function onLoad() {
// Set default values
g_form.setValue('contact_type', 'email');
// Hide fields for new records
if (g_form.isNewRecord()) {
g_form.setDisplay('resolution_notes', false);
g_form.setDisplay('resolved_by', false);
}
// Show informational message
g_form.addInfoMessage('Please provide detailed information for faster resolution.');
}
active: true
order: 100
ui_type: 0
ui_type の値:
| 値 | 意味 |
|---|---|
| 0 | デスクトップ |
| 1 | モバイル/Service Portal |
| 10 | デスクトップとモバイルの両方 |
ステップ 1.2:onChange スクリプトを作成する
フィールドの依存関係を持つ onChange:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Category Sets Subcategory Options"
table: incident
type: onChange
field_name: category
script: |
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
// Skip if loading form or using template
if (isLoading || isTemplate) {
return;
}
// Clear dependent field when parent changes
g_form.clearValue('subcategory');
// Set subcategory based on category
if (newValue == 'hardware') {
g_form.setValue('assignment_group', 'Hardware Support');
} else if (newValue == 'software') {
g_form.setValue('assignment_group', 'Software Support');
}
}
active: true
order: 100
ui_type: 0
onChange のパラメーターの説明:
| パラメーター | 説明 |
|---|---|
| control | フォーム要素(ほとんど使用されません) |
| oldValue | 変更前のフィールド値 |
| newValue | 新しいフィールド値(現在の値) |
| isLoading | フォームの読み込み中の場合は true |
| isTemplate | テンプレートを使用している場合は true |
ステップ 1.3:onSubmit スクリプトを作成する
onSubmit での検証:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Validate Required Fields Before Submit"
table: incident
type: onSubmit
script: |
function onSubmit() {
// Get field values
var shortDesc = g_form.getValue('short_description');
var category = g_form.getValue('category');
var priority = g_form.getValue('priority');
// Validate short description length
if (shortDesc.length < 10) {
g_form.addErrorMessage('Short description must be at least 10 characters.');
g_form.flash('short_description', '#FF0000', 0);
return false; // Prevent submission
}
// Validate P1 requires assignment group
if (priority == '1' && !g_form.getValue('assignment_group')) {
g_form.addErrorMessage('P1 incidents require an assignment group.');
return false;
}
// Confirm high priority submission
if (priority == '1' || priority == '2') {
var confirmed = confirm('You are submitting a high priority incident. Continue?');
if (!confirmed) {
return false;
}
}
return true; // Allow submission
}
active: true
order: 100
ui_type: 0
ステップ 1.4:onCellEdit スクリプトを作成する
リスト編集の検証:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Validate Priority Change in List"
table: incident
type: onCellEdit
script: |
function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
// sysIDs: array of selected record sys_ids
// oldValues: array of previous values
// newValue: the new value being set
// callback: must be called to complete the edit
var priority = g_form.getValue('priority');
// Prevent bulk P1 changes
if (newValue == '1' && sysIDs.length > 1) {
g_form.addErrorMessage('Cannot bulk change to P1. Edit records individually.');
callback(false); // Cancel edit
return;
}
// Confirm P1 assignment
if (newValue == '1') {
var confirmed = confirm('Setting priority to Critical. This will escalate the incident. Continue?');
callback(confirmed);
return;
}
callback(true); // Allow edit
}
active: true
order: 100
フェーズ 2:g_form API
ステップ 2.1:値の取得と設定
基本的な g_form メソッド:
// Get field values
var value = g_form.getValue('field_name'); // Internal value
var display = g_form.getDisplayValue('field_name'); // Display value
var reference = g_form.getReference('assigned_to'); // Reference object (deprecated - use GlideAjax)
// Set field values
g_form.setValue('field_name', 'value'); // Set value
g_form.setValue('assigned_to', sysId, displayValue); // Set reference with display
g_form.clearValue('field_name'); // Clear to empty
// Check field states
var isEmpty = g_form.getValue('field_name') == ''; // Check empty
var isNewRecord = g_form.isNewRecord(); // New vs existing
参照フィールドの処理:
// DEPRECATED - Makes synchronous server call (performance issue!)
var user = g_form.getReference('assigned_to');
var email = user.email;
// BETTER - Use callback (still not ideal)
g_form.getReference('assigned_to', function(ref) {
var email = ref.email;
// Continue processing
});
// BEST - Use GlideAjax (see Phase 4)
ステップ 2.2:表示状態と状態の制御
// Visibility
g_form.setDisplay('field_name', true); // Show field (affects row)
g_form.setVisible('field_name', true); // Show field (preserves space)
g_form.hideFieldMsg('field_name'); // Hide field message
// State control
g_form.setMandatory('field_name', true); // Make required
g_form.setReadOnly('field_name', true); // Make read-only
g_form.setDisabled('field_name', true); // Disable (grayed out)
// Labels
g_form.setLabelOf('field_name', 'New Label'); // Change label text
// Options (choice fields)
g_form.clearOptions('priority'); // Remove all options
g_form.addOption('priority', '1', 'Critical', 0); // Add option (value, label, index)
g_form.removeOption('priority', '5'); // Remove specific option
ステップ 2.3:メッセージと強調表示
// Form-level messages
g_form.addInfoMessage('Information message');
g_form.addWarningMessage('Warning message');
g_form.addErrorMessage('Error message');
g_form.clearMessages(); // Clear all messages
// Field-level messages
g_form.showFieldMsg('field_name', 'Message text', 'info'); // info, warning, error
g_form.hideFieldMsg('field_name'); // Clear field message
g_form.hideAllFieldMsgs(); // Clear all field messages
// Visual highlighting
g_form.flash('field_name', '#FF0000', 0); // Flash red (color, count; 0=once)
ステップ 2.4:セクションと関連リストの制御
// Sections (tabs)
g_form.setSectionDisplay('section_name', true); // Show/hide section
g_form.isSectionVisible('section_name'); // Check visibility
g_form.activateTab('section_name'); // Switch to tab
// Related lists (limited support)
// Use UI Actions or GlideAjax for related list operations
フェーズ 3:g_user オブジェクトと g_scratchpad オブジェクト
ステップ 3.1:g_user オブジェクト
g_user オブジェクトは、現在ログインしているユーザーに関する情報を提供します。
// User identification
var userSysId = g_user.userID; // User sys_id
var userName = g_user.userName; // Username (login name)
var firstName = g_user.firstName; // First name
var lastName = g_user.lastName; // Last name
var fullName = g_user.getFullName(); // Full display name
// Role checks
var isAdmin = g_user.hasRole('admin'); // Check single role
var isItil = g_user.hasRoleExactly('itil'); // Exact role match
var hasAnyRole = g_user.hasRoles(); // Has any role
// Client data (set in business rules)
var customData = g_user.getClientData('custom_key');
// Preferences
var pref = g_user.getPreference('preference_name');
サーバーからクライアントデータを設定する(ビジネスルール):
// Server-side (business rule, before query/display)
gs.getSession().putClientData('manager_email', current.caller_id.manager.email);
// Client-side (client script)
var managerEmail = g_user.getClientData('manager_email');
ステップ 3.2:g_scratchpad オブジェクト
g_scratchpad は、フォームの読み込み中にサーバーからクライアントへデータを渡します。
サーバー側(表示ビジネスルール):
// Type: display, When: before
// Set scratchpad values for client access
g_scratchpad.isVip = current.caller_id.vip == true;
g_scratchpad.callerCompany = current.caller_id.company.name.toString();
g_scratchpad.maxPriority = gs.getProperty('incident.max_priority', '3');
クライアント側(onLoad スクリプト):
function onLoad() {
// Access scratchpad data (no server call needed!)
if (g_scratchpad.isVip) {
g_form.addInfoMessage('VIP Caller - Handle with priority');
g_form.setValue('priority', '2');
}
// Use server-side property value
var maxPriority = g_scratchpad.maxPriority;
// Remove low priority options for VIP
if (g_scratchpad.isVip) {
g_form.removeOption('priority', '5');
g_form.removeOption('priority', '4');
}
}
フェーズ 4:サーバー呼び出しに GlideAjax を使用する
ステップ 4.1:スクリプトインクルードを作成する
まず、クライアントから呼び出し可能なスクリプトインクルードを作成します。
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: "IncidentAjaxUtils"
api_name: IncidentAjaxUtils
client_callable: true
script: |
var IncidentAjaxUtils = Class.create();
IncidentAjaxUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
// Get user details by sys_id
getUserDetails: function() {
var userId = this.getParameter('sysparm_user_id');
var result = {};
var user = new GlideRecord('sys_user');
if (user.get(userId)) {
result.name = user.name.toString();
result.email = user.email.toString();
result.phone = user.phone.toString();
result.department = user.department.getDisplayValue();
result.manager = user.manager.getDisplayValue();
result.vip = user.vip == true;
}
return JSON.stringify(result);
},
// Validate assignment group can handle priority
validateAssignment: function() {
var groupId = this.getParameter('sysparm_group_id');
var priority = this.getParameter('sysparm_priority');
var result = { valid: true, message: '' };
var group = new GlideRecord('sys_user_group');
if (group.get(groupId)) {
// Check if group handles this priority
var canHandleP1 = group.u_handles_critical == true;
if (priority == '1' && !canHandleP1) {
result.valid = false;
result.message = group.name + ' does not handle Critical incidents. Please select a Critical-capable group.';
}
}
return JSON.stringify(result);
},
// Get related incidents count
getRelatedIncidentCount: function() {
var ciId = this.getParameter('sysparm_ci_id');
var count = 0;
if (ciId) {
var ga = new GlideAggregate('incident');
ga.addQuery('cmdb_ci', ciId);
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
count = ga.getAggregate('COUNT');
}
}
return count.toString();
},
type: 'IncidentAjaxUtils'
});
access: public
active: true
ステップ 4.2:クライアントスクリプトから GlideAjax を呼び出す
基本的な GlideAjax パターン:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate || !newValue) {
return;
}
// Create GlideAjax call
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'getUserDetails'); // Method name
ga.addParam('sysparm_user_id', newValue); // Custom parameter
// Make asynchronous call
ga.getXMLAnswer(function(response) {
// Parse JSON response
var user = JSON.parse(response);
if (user.name) {
// Update form with retrieved data
g_form.setValue('u_caller_email', user.email);
g_form.setValue('u_caller_phone', user.phone);
// VIP handling
if (user.vip) {
g_form.addInfoMessage('VIP Caller: ' + user.name);
g_form.setValue('priority', '2');
}
}
});
}
GlideAjax による検証:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate || !newValue) {
return;
}
var priority = g_form.getValue('priority');
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'validateAssignment');
ga.addParam('sysparm_group_id', newValue);
ga.addParam('sysparm_priority', priority);
ga.getXMLAnswer(function(response) {
var result = JSON.parse(response);
if (!result.valid) {
g_form.showFieldMsg('assignment_group', result.message, 'error');
g_form.setValue('assignment_group', '');
} else {
g_form.hideFieldMsg('assignment_group');
}
});
}
ステップ 4.3:getXML を使用した GlideAjax(完全な応答)
応答をより細かく制御するには、次のようにします。
function onLoad() {
var ciId = g_form.getValue('cmdb_ci');
if (!ciId) return;
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'getRelatedIncidentCount');
ga.addParam('sysparm_ci_id', ciId);
ga.getXML(function(response) {
// Get the answer element
var answer = response.responseXML.documentElement.getAttribute('answer');
var count = parseInt(answer) || 0;
if (count > 5) {
g_form.addWarningMessage('This CI has ' + count + ' active incidents. Consider checking for related problems.');
}
});
}
フェーズ 5:パフォーマンスに関するベストプラクティス
ステップ 5.1:サーバー呼び出しを最小限にする
// BAD - Multiple synchronous calls
function onLoad() {
var caller = g_form.getReference('caller_id'); // Server call 1
var assignee = g_form.getReference('assigned_to'); // Server call 2
var group = g_form.getReference('assignment_group'); // Server call 3
// Form takes 3+ seconds to load!
}
// BETTER - Use g_scratchpad (set in Display Business Rule)
function onLoad() {
// Data already available - no server calls!
var callerVip = g_scratchpad.callerVip;
var assigneeEmail = g_scratchpad.assigneeEmail;
}
// BEST - Single GlideAjax call returning all needed data
function onLoad() {
var ga = new GlideAjax('FormDataUtils');
ga.addParam('sysparm_name', 'getFormContext');
ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
ga.getXMLAnswer(function(response) {
var data = JSON.parse(response);
// All data in one call
processFormData(data);
});
}
ステップ 5.2:条件を適切に使用する
// BAD - Script runs for every incident
function onLoad() {
if (g_form.getValue('priority') == '1') {
// Logic only needed for P1
}
}
// BETTER - Use script condition in client script record
// Condition: priority=1
function onLoad() {
// Only runs when priority is 1
// No condition check needed in script
}
クライアントスクリプトのレコードで条件を設定する:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "P1 Special Handling"
table: incident
type: onLoad
condition: "priority=1"
script: |
function onLoad() {
// Only executes when condition matches
g_form.addInfoMessage('Critical incident - expedited handling required');
}
active: true
ステップ 5.3:連続する変更をデバウンスする
// BAD - Ajax call on every keystroke
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) return;
// Fires on every character typed!
var ga = new GlideAjax('SearchUtils');
ga.addParam('sysparm_name', 'search');
ga.addParam('sysparm_query', newValue);
ga.getXMLAnswer(handleResults);
}
// BETTER - Debounce rapid changes
var searchTimeout;
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) return;
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Wait 500ms after last change before searching
searchTimeout = setTimeout(function() {
var ga = new GlideAjax('SearchUtils');
ga.addParam('sysparm_name', 'search');
ga.addParam('sysparm_query', newValue);
ga.getXMLAnswer(handleResults);
}, 500);
}
ステップ 5.4:参照データをキャッシュする
// Store lookup results to avoid repeated calls
var categoryAssignments = {};
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || !newValue) return;
// Check cache first
if (categoryAssignments[newValue]) {
g_form.setValue('assignment_group', categoryAssignments[newValue]);
return;
}
// Fetch and cache
var ga = new GlideAjax('CategoryUtils');
ga.addParam('sysparm_name', 'getAssignmentGroup');
ga.addParam('sysparm_category', newValue);
ga.getXMLAnswer(function(response) {
var groupId = response;
categoryAssignments[newValue] = groupId; // Cache for next time
g_form.setValue('assignment_group', groupId);
});
}
フェーズ 6:モバイルと Service Portal に関する考慮事項
ステップ 6.1:モバイル対応スクリプト
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Mobile-Compatible Validation"
table: incident
type: onSubmit
ui_type: 10
script: |
function onSubmit() {
// These g_form methods work on mobile
var shortDesc = g_form.getValue('short_description');
if (!shortDesc || shortDesc.length < 5) {
// Use g_form messages (works on mobile)
g_form.addErrorMessage('Please provide a description');
return false;
}
// AVOID: alert(), confirm(), prompt() - don't work on mobile
// AVOID: document.getElementById() - DOM may differ
// AVOID: jQuery selectors - not available
return true;
}
active: true
モバイル対応の g_form メソッド:
| メソッド | デスクトップ | モバイル | Service Portal |
|---|---|---|---|
| getValue() | はい | はい | はい |
| setValue() | はい | はい | はい |
| setMandatory() | はい | はい | はい |
| setDisplay() | はい | はい | はい |
| setReadOnly() | はい | はい | はい |
| addErrorMessage() | はい | はい | はい |
| showFieldMsg() | はい | はい | はい |
| getReference() | はい | いいえ | 制限あり |
| flash() | はい | いいえ | いいえ |
| activateTab() | はい | いいえ | いいえ |
ステップ 6.2:Service Portal UI スクリプト
Service Portal 固有のクライアントロジックには、UI スクリプトを使用します。
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_script
data:
name: "SP Incident Form Handler"
script: |
// Service Portal UI Script
// Runs in the Service Portal context
function spIncidentFormInit() {
// Use $scope for Angular integration
// Access widget data
console.log('SP Incident form initialized');
}
// Self-executing for immediate availability
if (typeof angular !== 'undefined') {
angular.element(document).ready(function() {
spIncidentFormInit();
});
}
active: true
global: false
ui_type: 10
フェーズ 7:クライアントスクリプトのデバッグ
ステップ 7.1:コンソールログ
function onLoad() {
// Development logging (remove in production)
console.log('=== Client Script: Initialize Form ===');
console.log('Record sys_id:', g_form.getUniqueValue());
console.log('Is new record:', g_form.isNewRecord());
console.log('Current user:', g_user.userName);
// Log scratchpad contents
console.log('Scratchpad:', JSON.stringify(g_scratchpad));
// Performance timing
console.time('formSetup');
// ... form setup logic ...
console.timeEnd('formSetup');
}
ステップ 7.2:JavaScript デバッガー
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
// Add breakpoint for debugging
debugger; // Browser will pause here when DevTools open
if (isLoading) return;
// Step through logic in browser DevTools
var priority = g_form.getValue('priority');
var category = g_form.getValue('category');
// Inspect variables in DevTools console
}
ステップ 7.3:エラー処理
function onLoad() {
try {
// Main logic
initializeForm();
setupFieldDependencies();
loadReferenceData();
} catch (error) {
// Log error details
console.error('Client Script Error:', error.message);
console.error('Stack:', error.stack);
// Notify user gracefully
g_form.addErrorMessage('Form initialization error. Please refresh the page.');
}
}
function initializeForm() {
// Potentially error-prone code
var data = JSON.parse(g_scratchpad.formData);
// ...
}
ステップ 7.4:デバッグ用のクライアントスクリプトのクエリ
テーブルのすべてのクライアントスクリプトを検索:
Tool: SN-Query-Table
Parameters:
table_name: sys_script_client
query: table=incident^active=true
fields: sys_id,name,type,field_name,order,condition,ui_type
limit: 50
スクリプトの実行順序を確認:
Tool: SN-Query-Table
Parameters:
table_name: sys_script_client
query: table=incident^active=true^type=onLoad
fields: name,order,condition
order_by: order
limit: 50
フェーズ 8:一般的なパターン
パターン 1:連動するフィールドの依存関係
// Category -> Subcategory -> Assignment Group cascade
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate) return;
// Clear downstream fields
g_form.clearValue('subcategory');
g_form.clearValue('assignment_group');
if (!newValue) return;
// Fetch valid subcategories for this category
var ga = new GlideAjax('CategoryUtils');
ga.addParam('sysparm_name', 'getSubcategories');
ga.addParam('sysparm_category', newValue);
ga.getXMLAnswer(function(response) {
var subcategories = JSON.parse(response);
// Clear and repopulate options
g_form.clearOptions('subcategory');
g_form.addOption('subcategory', '', '-- Select --');
subcategories.forEach(function(sub) {
g_form.addOption('subcategory', sub.value, sub.label);
});
});
}
パターン 2:条件付き必須フィールド
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) return;
// State = Resolved (6) requires resolution notes
var isResolved = (newValue == '6');
g_form.setMandatory('resolution_code', isResolved);
g_form.setMandatory('resolution_notes', isResolved);
g_form.setDisplay('resolution_code', isResolved);
g_form.setDisplay('resolution_notes', isResolved);
if (isResolved) {
g_form.addInfoMessage('Please provide resolution details.');
}
}
パターン 3:複数のチェックによるフォーム検証
function onSubmit() {
var errors = [];
// Check 1: Short description length
var shortDesc = g_form.getValue('short_description');
if (shortDesc.length < 10) {
errors.push('Short description must be at least 10 characters');
g_form.showFieldMsg('short_description', 'Too short', 'error');
}
// Check 2: High priority requires assignment
var priority = g_form.getValue('priority');
var assignee = g_form.getValue('assigned_to');
var group = g_form.getValue('assignment_group');
if ((priority == '1' || priority == '2') && !assignee && !group) {
errors.push('High priority incidents must be assigned');
}
// Check 3: Configuration item recommended
var ci = g_form.getValue('cmdb_ci');
if (!ci) {
// Warning only, don't block
var proceed = confirm('No Configuration Item specified. Continue anyway?');
if (!proceed) {
return false;
}
}
// Display all errors
if (errors.length > 0) {
g_form.addErrorMessage('Please fix the following issues:\n' + errors.join('\n'));
return false;
}
return true;
}
パターン 4:参照からの自動入力
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate || !newValue) {
return;
}
// Get caller details via GlideAjax
var ga = new GlideAjax('UserUtils');
ga.addParam('sysparm_name', 'getUserInfo');
ga.addParam('sysparm_user_id', newValue);
ga.getXMLAnswer(function(response) {
var user = JSON.parse(response);
// Auto-populate fields
g_form.setValue('location', user.location);
g_form.setValue('u_department', user.department);
g_form.setValue('u_phone', user.phone);
// VIP handling
if (user.vip) {
g_form.setValue('priority', '2');
g_form.addInfoMessage('VIP Caller - Priority elevated');
}
});
}
フェーズ 9:避けるべきアンチパターン
アンチパターン 1:同期サーバー呼び出し
// BAD - Blocks the UI
function onLoad() {
var user = g_form.getReference('caller_id'); // Synchronous!
var manager = g_form.getReference('assigned_to'); // More blocking!
// User experiences frozen form
}
// GOOD - Asynchronous with callback
function onLoad() {
var callerId = g_form.getValue('caller_id');
if (!callerId) return;
var ga = new GlideAjax('UserUtils');
ga.addParam('sysparm_name', 'getUserInfo');
ga.addParam('sysparm_user_id', callerId);
ga.getXMLAnswer(processUser); // Non-blocking
}
アンチパターン 2:DOM 操作
// BAD - Direct DOM access
function onLoad() {
document.getElementById('incident.short_description').style.backgroundColor = 'yellow';
jQuery('#incident\\.priority').hide();
}
// GOOD - Use g_form API
function onLoad() {
g_form.flash('short_description', '#FFFF00', 3);
g_form.setDisplay('priority', false);
}
アンチパターン 3:sys_id のハードコード
// BAD - Hardcoded values
function onChange(control, oldValue, newValue, isLoading) {
if (newValue == 'hardware') {
g_form.setValue('assignment_group', 'a715cd759f2002002920bde8132e7018'); // Magic string!
}
}
// GOOD - Use GlideAjax lookup or sys_properties
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || !newValue) return;
var ga = new GlideAjax('AssignmentUtils');
ga.addParam('sysparm_name', 'getGroupByCategory');
ga.addParam('sysparm_category', newValue);
ga.getXMLAnswer(function(groupId) {
g_form.setValue('assignment_group', groupId);
});
}
アンチパターン 4:onLoad での過剰な処理
// BAD - Too much in onLoad
function onLoad() {
// 5 GlideAjax calls
loadCallerInfo();
loadCIDetails();
loadRelatedIncidents();
loadSLAStatus();
loadApprovalHistory();
// Form takes forever to load!
}
// GOOD - Use g_scratchpad for essential data, lazy load the rest
function onLoad() {
// Essential data from g_scratchpad (set in display business rule)
if (g_scratchpad.callerVip) {
g_form.addInfoMessage('VIP Caller');
}
// Load additional data only when needed
// e.g., when user clicks a tab or expands a section
}
ツールの使用概要
| 操作 | MCP ツール | テーブル |
|---|---|---|
| クライアントスクリプトの作成 | SN-Create-Record | sys_script_client |
| クライアントスクリプトの更新 | SN-Update-Record | sys_script_client |
| クライアントスクリプトのクエリ | SN-Query-Table | sys_script_client |
| Script Include の作成 | SN-Create-Record | sys_script_include |
| UI スクリプトの作成 | SN-Create-Record | sys_ui_script |
| スキーマの取得 | SN-Get-Table-Schema | 任意のテーブル |
| ローカル編集サイクル | SN-Get-Record + SN-Update-Record | 取得、最新状態の確認、編集、反映 |
ベストプラクティス
- まず UI ポリシーを使用: 単純な表示/非表示/必須設定には、コードを使わないでください
- サーバー呼び出しを最小限にする: g_scratchpad を使い、GlideAjax をまとめて呼び出し、結果をキャッシュします
- isLoading を処理する: 初期化時の問題を避けるため、onChange では必ず確認します
- 戻り値: onSubmit は true/false を明示的に返す必要があります
- スクリプト条件を使用する: プラットフォームレベルで実行対象を絞り込みます
- DOM へのアクセスを避ける: クロスプラットフォーム互換性のため、g_form API を使用します
- エラー処理: リスクのあるコードを try-catch ブロックで囲みます
- コンソールログ: 開発中に使用し、本番環境では削除します
- モバイルテスト: 実機で ui_type=10 のスクリプトをテストします
- バージョン管理:
SN-Get-Recordでscript、sys_updated_on、sys_mod_countを取得し、ローカルで編集してから、再度最新状態を確認し、レビュー済みのフィールドをSN-Update-Recordで反映します
トラブルシューティング
スクリプトが実行されない
症状: コンソール出力がなく、フォームに変化がありません 原因:
- スクリプトが無効になっている
- 条件に一致していない
- ui_type が一致していない(デスクトップとモバイルの違い)
- JavaScript の構文エラー
解決策:
Tool: SN-Query-Table
Parameters:
table_name: sys_script_client
query: table=incident^name=*my script*
fields: name,active,condition,ui_type
active=true を確認し、条件を検証して、ui_type が環境と一致しているか確認します。
onChange が実行されない
症状: フィールド値を変更してもスクリプトが実行されません 原因:
- field_name が実際のフィールド名と一致していない
- isLoading/isTemplate が処理されていない
- ロジックに到達する前にスクリプトエラーが発生している
解決策:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
console.log('onChange fired:', {
field: 'field_name',
old: oldValue,
new: newValue,
loading: isLoading
});
if (isLoading || isTemplate) {
console.log('Skipping - form loading');
return;
}
// ...
}
GlideAjax が空を返す
症状: 応答が空、または undefined です 原因:
- Script Include が client_callable ではない
- メソッド名のスペルが間違っている
- Script Include 内でサーバー側エラーが発生している
解決策:
Tool: SN-Query-Table
Parameters:
table_name: sys_script_include
query: name=MyAjaxUtil
fields: name,client_callable,active
client_callable=true を確認します。サーバーエラーがないか、システムログを確認します。
フォームの読み込みが遅い
症状: フォームが操作可能になるまでに時間がかかる 原因:
- 同期的な getReference 呼び出し
- onLoad 内での複数の GlideAjax 呼び出し
- 負荷の高い DOM 操作
解決策:
- getReference を GlideAjax に置き換える
- 複数の Ajax 呼び出しを 1 つにまとめる
- サーバーデータには g_scratchpad を使用する
- ブラウザーの DevTools の Performance タブでプロファイリングする
関連スキル
catalog/ui-policies- コード不要で表示/非表示/必須を設定admin/script-execution- サーバーサイドスクリプトのパターンdevelopment/business-rules- サーバーサイドのフォーム処理development/script-includes- 再利用可能なサーバー関数admin/script-sync- ローカル開発ワークフロー