開発情報・ナレッジ

投稿者: ShiningStar株式会社 2025年4月2日 (水)

カスタムAPIを使った動的社員検索プルダウンを作成するサンプルプログラム

カスタムAPIを利用してマスタDBを検索しプルダウンに反映するサンプルプログラムを紹介します。
今回は社員検索フォームを題材にサンプルの実装方法を解説します。

本記事では、社員マスタデータベースから階層的に本部、部署、課、社員を選択し、
社員IDをhiddenフィールドに格納するフォームの作成方法を紹介します。
本記事に記載の内容は、カスタムAPIでSPIRAL APIを効果的に扱うライブラリのライブラリを使用していて、
セットアップはカスタムAPIでSPIRAL APIを効果的に扱うライブラリを参考にして済ませておく必要があります。

目次
  1. 実装の概要
  2. 設置するソースコード
  3. 注意事項
  4. 注意点
  5. 社員マスタDBの構成
  6. HTMLの実装
  7. JavaScriptの実装
  8. APIレスポンスの構造
  9. 実装のポイント
  10. まとめ

実装の概要

今回の社員検索フォームは、以下の特徴を持っています。

1. 階層的な選択(本部→部署→課→社員)
2. APIクライアントを使用したデータ取得
3. 選択された社員IDのhiddenフィールドへの格納

設置するソースコード

社員検索フォームを実装するために、以下のソースコードを設置してください

BODY: 社員検索フォームのHTML(フォームブロックのbodyタブに設置してください)
JavaScript: 社員検索フォームのJavaScript(フォームブロックのJSタブに設置してください)

注意事項

社員検索フォームブロック及びページは、カスタムAPIに認証設定をした認証エリアに設置してください。

注意点

api-client.jsが正しく読み込まれていることを確認してください。
APIレスポンスの構造が変更になる場合は、データ取得部分のコードを修正する必要があります。
大量のデータを扱う場合は、APIリクエスト回数などを考慮してページネーションを実装することを検討してください。
社員データが200件以上ある場合は、繰り返し処理等が必要になります。
数万人の社員がいる場合は、プルダウンで段階的に絞り込む必要があります。その場合、選択されるたびにAPIを呼び出すため、API呼び出しの回数制限に注意してください。

社員マスタDBの構成

本部 headquarters テキスト
部署 division テキスト
section テキスト
名前 staffName テキスト

以上の通りでテキストで所属を管理している想定です。

HTMLの実装

まず、社員検索フォームのHTMLを実装します。
フォームブロックのbodyタブを編集してください。
フォームには、本部、部署、課、社員を選択するためのドロップダウンメニューと、
選択された社員IDを格納するためのhiddenフィールドが含まれます。

コピー
<div class="sp-form-container">
    <div class="sp-form-item sp-form-html" th:inline="none"><p><span style="font-size: 18pt;">社員メモ追加</span></p></div>
    <!--/* 社員ID(staffId) */-->
    <sp:input-field name="f01"></sp:input-field>
    <input type="hidden" id="staffId" name="f01" th:value="${inputs['f01']}" />

    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${'本部'}">
          本部
        </th:block>
      </div>
      <div class="sp-form-data">
        <div class="sp-form-dropdown">
          <select id="division" name="division" class="sp-form-control">
            <option value="">選択してください</option>
          </select>
          <span class="sp-form-dropdown-icon"></span>
        </div>
      </div>
    </div>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${'部署'}">
          部署
        </th:block>
      </div>
      <div class="sp-form-data">
        <div class="sp-form-dropdown">
          <select id="department" name="department" class="sp-form-control" disabled>
            <option value="">選択してください</option>
          </select>
          <span class="sp-form-dropdown-icon"></span>
        </div>
      </div>
    </div>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${'課'}">
          課
        </th:block>
      </div>
      <div class="sp-form-data">
        <div class="sp-form-dropdown">
          <select id="section" name="section" class="sp-form-control" disabled>
            <option value="">選択してください</option>
          </select>
          <span class="sp-form-dropdown-icon"></span>
        </div>
      </div>
    </div>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${'社員'}">
          社員
        </th:block>
      </div>
      <div class="sp-form-data">
        <div class="sp-form-dropdown">
          <select id="staff" name="staff" class="sp-form-control" disabled>
            <option value="">選択してください</option>
          </select>
          <span class="sp-form-dropdown-icon"></span>
        </div>
      </div>
    </div>

    <!--/* メモ(memo) */-->
    <sp:input-field name="f02"></sp:input-field>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${fields['f02'].label}">
          Label
        </th:block>
        <span class="sp-form-required" th:if="${fields['f02'].required}" th:text="${fields['f02'].requiredIndicator}">*</span>
      </div>
      <div class="sp-form-data">
        <input type="text" class="sp-form-control" th:name="${fields['f02'].name}" th:placeholder="${fields['f02'].placeholder}" th:value="${inputs['f02']}" th:if="${fields['f02'].control == 'text'}">
        <!--ZipCode Option-->
        <div class="sp-form-zip-code" th:if="${fields['f02'].control == 'zipCode'}">
          <input type="text" class="sp-form-control" th:name="${fields['f02'].name}" th:placeholder="${fields['f02'].placeholder}" th:value="${inputs['f02']}">
          <button class="sp-form-zip-code-button" th:data-zipcode="|zipCodeSearch${fields['f02'].name}|" th:if="${fields['f02'].addressByZipCode != null}" th:text="${fields['f02'].zipCodeButtonLabel}">住所検索</button>
        </div>
        <span class="sp-form-noted" th:if="${fields['f02'].help != null}" th:text="${fields['f02'].help}">Help text</span>
        <span class="sp-form-error" th:data-zipcode="|zipCodeError${fields['f02'].name}|" th:text="${errors['f02']?.message}">Error message</span>
      </div>
    </div>
    <div class="sp-form-item sp-form-interaction">
      <button class="sp-form-prev-button" type="submit" name="action" value="previous" th:if="!${step.isFirst}" th:text="${step.prevButtonLabel}">Prev</button>
      <button class="sp-form-next-button" type="submit" name="action" value="next" th:text="${step.nextButtonLabel}">Next</button>
    </div>
    <div id="loading">
      <div class="spinner"></div>
      <p>データ取得中...</p>
    </div>
</div>
  <script src="/_media/restAPItest/api-client.js"></script>
            

JavaScriptの実装

次に、APIクライアントを使用してデータを取得し、ドロップダウンメニューを操作するためのJavaScriptを実装します。
フォームブロックのJSタブを編集してください。
主な機能は以下の通りです

1. APIクライアントの初期化
2. 本部データの取得と表示
3. 選択に基づく部署、課、社員データの取得と表示
4. 社員選択時のIDの格納
5. フォーム送信時のバリデーション
コピー
    // APIクライアントのインスタンスを作成
    let api;
    let staffData = [];
    
    // DOM要素の参照
    let divisionSelect;
    let departmentSelect;
    let sectionSelect;
    let staffSelect;
    let staffIdInput;
    let loadingIndicator;
    
    // api-client.jsが読み込まれているか確認する関数
    function checkApiClientLoaded() {
      if (typeof ApiClient === 'undefined') {
        console.error('ApiClientが読み込まれていません。api-client.jsが正しく読み込まれているか確認してください。');
        // 500ミリ秒後に再試行
        setTimeout(checkApiClientLoaded, 500);
        return false;
      }
      
      console.log('ApiClientが正常に読み込まれました');
      // ApiClientが利用可能になったらインスタンスを作成
      api = new ApiClient();
      
      // DOM要素の参照を取得し、初期化
      initializeApp();
      return true;
    }
    
    /**
     * アプリケーションの初期化
     */
    function initializeApp() {
      // DOM要素の参照を取得
      divisionSelect = document.getElementById('division');
      departmentSelect = document.getElementById('department');
      sectionSelect = document.getElementById('section');
      staffSelect = document.getElementById('staff');
      staffIdInput = document.getElementById('staffId');
      loadingIndicator = document.getElementById('loading');
      
      // イベントリスナーを設定
      setupEventListeners();
      
      // 初期データの読み込み
      loadDivisions();
    }
    
    /**
     * イベントリスナーを設定
     */
    function setupEventListeners() {
      // 本部選択時のイベント
      divisionSelect.addEventListener('change', function() {
        const divisionId = this.value;
        if (divisionId) {
          loadDepartments(divisionId);
        } else {
          resetSelect(departmentSelect);
          resetSelect(sectionSelect);
          resetSelect(staffSelect);
          staffIdInput.value = '';
        }
      });
      
      // 部署選択時のイベント
      departmentSelect.addEventListener('change', function() {
        const departmentId = this.value;
        if (departmentId) {
          loadSections(departmentId);
        } else {
          resetSelect(sectionSelect);
          resetSelect(staffSelect);
          staffIdInput.value = '';
        }
      });
      
      // 課選択時のイベント
      sectionSelect.addEventListener('change', function() {
        const sectionId = this.value;
        if (sectionId) {
          loadStaff(sectionId);
        } else {
          resetSelect(staffSelect);
          staffIdInput.value = '';
        }
      });
      
      // 社員選択時のイベント
      staffSelect.addEventListener('change', function() {
        const staffId = this.value;
        if (staffId) {
          // 選択された社員のIDをhiddenフィールドに設定
          staffIdInput.value = staffId;
        } else {
          staffIdInput.value = '';
        }
      });
      
      // フォーム送信時のイベント
      const formElement = document.querySelector('.sp-form-container');
      if (formElement) {
        // フォームの親要素にイベントリスナーを追加
        formElement.closest('form')?.addEventListener('submit', function(event) {
          // フォームのバリデーション
          if (!staffIdInput.value) {
            event.preventDefault();
            alert('社員を選択してください');
          }
        });
      }
    }
    
    /**
     * 本部一覧を取得して表示
     */
    async function loadDivisions() {
      toggleLoading(true);
      
      try {
        // 社員マスタDBからデータを取得
        const result = await api.getDatabaseRecords({
          limit: 200,
          offset: 0,
          // 必要に応じてフィルタリングパラメータを追加
        });
        if (result.status === "success" && result.data && result.data.data && result.data.data.result.items) {
          // 正しいデータ構造からitemsを取得
          staffData = result.data.data.result.items;
          
          // 本部の一覧を抽出(重複を排除)
          const divisions = [...new Set(staffData.map(staff => staff.headquarters))].filter(Boolean);
          
          // 本部のセレクトボックスにオプションを追加
          divisions.sort().forEach(division => {
            const option = document.createElement('option');
            option.value = division;
            option.textContent = division;
            divisionSelect.appendChild(option);
          });
          
          // 本部セレクトボックスを有効化
          divisionSelect.disabled = false;
        } else {
          console.error('社員データの取得に失敗しました', result);
        }
      } catch (error) {
        console.error('社員データの取得中にエラーが発生しました', error);
      }
      
      toggleLoading(false);
    }
    
    /**
     * 選択された本部に基づいて部署一覧を表示
     * @param {string} divisionId - 選択された本部ID
     */
    function loadDepartments(divisionId) {
      // 部署セレクトをリセット
      resetSelect(departmentSelect);
      resetSelect(sectionSelect);
      resetSelect(staffSelect);
      
      // 選択された本部に属する部署を抽出(重複を排除)
      const departments = [...new Set(
        staffData
          .filter(staff => staff.headquarters === divisionId)
          .map(staff => staff.division)
      )].filter(Boolean);
      
      // 部署のセレクトボックスにオプションを追加
      departments.sort().forEach(department => {
        const option = document.createElement('option');
        option.value = department;
        option.textContent = department;
        departmentSelect.appendChild(option);
      });
      
      // 部署セレクトボックスを有効化
      departmentSelect.disabled = false;
    }
    
    /**
     * 選択された部署に基づいて課一覧を表示
     * @param {string} departmentId - 選択された部署ID
     */
    function loadSections(departmentId) {
      // 課セレクトをリセット
      resetSelect(sectionSelect);
      resetSelect(staffSelect);
      
      // 現在選択されている本部を取得
      const divisionId = divisionSelect.value;
      
      // 選択された本部と部署に属する課を抽出(重複を排除)
      const sections = [...new Set(
        staffData
          .filter(staff => staff.headquarters === divisionId && staff.division === departmentId)
          .map(staff => staff.section)
      )].filter(Boolean);
      
      // 課のセレクトボックスにオプションを追加
      sections.sort().forEach(section => {
        const option = document.createElement('option');
        option.value = section;
        option.textContent = section;
        sectionSelect.appendChild(option);
      });
      
      // 課セレクトボックスを有効化
      sectionSelect.disabled = false;
    }
    
    /**
     * 選択された課に基づいて社員一覧を表示
     * @param {string} sectionId - 選択された課ID
     */
    function loadStaff(sectionId) {
      // 社員セレクトをリセット
      resetSelect(staffSelect);
      
      // 現在選択されている本部と部署を取得
      const divisionId = divisionSelect.value;
      const departmentId = departmentSelect.value;
      
      // 選択された本部、部署、課に属する社員を抽出
      const staffMembers = staffData
        .filter(staff => 
          staff.headquarters === divisionId && 
          staff.division === departmentId && 
          staff.section === sectionId
        );
      
      // 社員のセレクトボックスにオプションを追加
      staffMembers.forEach(staff => {
        const option = document.createElement('option');
        option.value = staff._id; // 社員IDを値として設定
        option.textContent = staff.staffName; // 社員名を表示テキストとして設定
        staffSelect.appendChild(option);
      });
      
      // 社員セレクトボックスを有効化
      staffSelect.disabled = false;
    }
    
    /**
     * セレクトボックスをリセット
     * @param {HTMLSelectElement} selectElement - リセットするセレクトボックス
     */
    function resetSelect(selectElement) {
      // 最初のオプション(「選択してください」)以外を削除
      while (selectElement.options.length > 1) {
        selectElement.remove(1);
      }
      
      // 最初のオプションを選択
      selectElement.selectedIndex = 0;
      
      // セレクトボックスを無効化
      selectElement.disabled = true;
    }
    
    /**
     * ローディング表示の切り替え
     * @param {boolean} isLoading - ローディング中かどうか
     */
    function toggleLoading(isLoading) {
      loadingIndicator.style.display = isLoading ? 'block' : 'none';
    }
    
    // DOMが読み込まれたら実行
    document.addEventListener('DOMContentLoaded', () => {
      // api-client.jsが読み込まれているか確認
      checkApiClientLoaded();
    });
            

APIレスポンスの構造

社員マスタデータベースからのAPIレスポンスは、以下のような構造になっています

コピー
{
    "status": "success",
    "data": {
        "status": "success",
        "data": {
            "success": true,
            "data": {
                "result": {
                    "items": [
                        {
                            "division": "総務部",
                            "_updatedBy": {
                                "from": "ui",
                                "type": "user",
                                "userId": "958"
                            },
                            "_unauthorized": false,
                            "headquarters": "東京本社",
                            "_revision": "1",
                            "_createdAt": "2025-03-26T05:45:30Z",
                            "staffName": "森下絵美",
                            "section": "経理課",
                            "_id": "50",
                            "_createdBy": {
                                "from": "ui",
                                "type": "user",
                                "userId": "958"
                            },
                            "_updatedAt": "2025-03-26T05:45:30Z"
                        },
                        // 他の社員データ...
                    ]
                }
            }
        }
    }
}
            

実装のポイント

1. データ構造の理解: APIレスポンスのネスト構造を正しく処理
2. 階層的な選択: 本部→部署→課→社員の順に選択肢を絞り込む
3. 重複排除: Set オブジェクトを使用して一意の選択肢を表示
4. エラーハンドリング: API呼び出しのエラーを適切に処理
5. ユーザーエクスペリエンス: ローディングインジケータやバリデーションの実装

まとめ

本記事では、カスタムAPIクライアントを使用して社員マスタデータベースから階層的に社員を検索するフォームの実装方法を紹介しました。
この実装方法は、大規模な組織での社員検索に非常に有効であり、ユーザーが直感的に操作できるインターフェースを提供します。
また、同様の階層型検索フォームは、他の用途(例:商品カテゴリ検索、地域検索など)にも応用できます。

解決しない場合はこちら コンテンツに関しての
要望はこちら