mcpField
C
client-scripts

client-scripts

github.com/happy-technologies-llc開発4143 回インストール4 日前に更新
リポジトリ

client-scripts について

client-scriptsは、ServiceNowの開発者向けスキルで、ブラウザー上のフォーム動作、検証、動的な操作を実装します。スクリプトの種類、フォームAPI、GlideAjax、性能、モバイル対応、デバッグを扱います。

主な機能

  1. 01onLoad、onChange、onSubmit、onCellEditスクリプトの作成
  2. 02g_formによるフォームフィールドの操作
  3. 03g_userとg_scratchpadによるセッション情報の取得
  4. 04GlideAjaxによるサーバーロジックの非同期呼び出し
  5. 05スクリプトとサーバー呼び出しの最適化
  6. 06ブラウザーツールでのスクリプトのデバッグ

活用シーン

  1. 01送信前のフォームデータ検証
  2. 02連動するフィールド動作の実装
  3. 03同期呼び出しを避けたサーバーデータの取得
  4. 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-Recordsys_script_client
クライアントスクリプトの更新SN-Update-Recordsys_script_client
クライアントスクリプトのクエリSN-Query-Tablesys_script_client
Script Include の作成SN-Create-Recordsys_script_include
UI スクリプトの作成SN-Create-Recordsys_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 で反映します

トラブルシューティング

スクリプトが実行されない

症状: コンソール出力がなく、フォームに変化がありません 原因:

  1. スクリプトが無効になっている
  2. 条件に一致していない
  3. ui_type が一致していない(デスクトップとモバイルの違い)
  4. 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 が実行されない

症状: フィールド値を変更してもスクリプトが実行されません 原因:

  1. field_name が実際のフィールド名と一致していない
  2. isLoading/isTemplate が処理されていない
  3. ロジックに到達する前にスクリプトエラーが発生している

解決策:

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 です 原因:

  1. Script Include が client_callable ではない
  2. メソッド名のスペルが間違っている
  3. Script Include 内でサーバー側エラーが発生している

解決策:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_include
  query: name=MyAjaxUtil
  fields: name,client_callable,active

client_callable=true を確認します。サーバーエラーがないか、システムログを確認します。

フォームの読み込みが遅い

症状: フォームが操作可能になるまでに時間がかかる 原因:

  1. 同期的な getReference 呼び出し
  2. onLoad 内での複数の GlideAjax 呼び出し
  3. 負荷の高い DOM 操作

解決策:

  1. getReference を GlideAjax に置き換える
  2. 複数の Ajax 呼び出しを 1 つにまとめる
  3. サーバーデータには g_scratchpad を使用する
  4. ブラウザーの DevTools の Performance タブでプロファイリングする

関連スキル

  • catalog/ui-policies - コード不要で表示/非表示/必須を設定
  • admin/script-execution - サーバーサイドスクリプトのパターン
  • development/business-rules - サーバーサイドのフォーム処理
  • development/script-includes - 再利用可能なサーバー関数
  • admin/script-sync - ローカル開発ワークフロー

参考資料

コメント