カスタムAPIを利用してマスタDBを検索しプルダウンに反映するサンプルプログラムを紹介します。
今回は社員検索フォームを題材にサンプルの実装方法を解説します。
本記事では、社員マスタデータベースから階層的に本部、部署、課、社員を選択し、
社員IDをhiddenフィールドに格納するフォームの作成方法を紹介します。
本記事に記載の内容は、カスタムAPIでSPIRAL APIを効果的に扱うライブラリのライブラリを使用していて、
セットアップはカスタムAPIでSPIRAL APIを効果的に扱うライブラリを参考にして済ませておく必要があります。
実装の概要
今回の社員検索フォームは、以下の特徴を持っています。
2. APIクライアントを使用したデータ取得
3. 選択された社員IDのhiddenフィールドへの格納
設置するソースコード
社員検索フォームを実装するために、以下のソースコードを設置してください
JavaScript: 社員検索フォームのJavaScript(フォームブロックのJSタブに設置してください)
注意事項
社員検索フォームブロック及びページは、カスタムAPIに認証設定をした認証エリアに設置してください。
注意点
・ 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タブを編集してください。
主な機能は以下の通りです
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" }, // 他の社員データ... ] } } } } }
実装のポイント
2. 階層的な選択: 本部→部署→課→社員の順に選択肢を絞り込む
3. 重複排除: Set オブジェクトを使用して一意の選択肢を表示
4. エラーハンドリング: API呼び出しのエラーを適切に処理
5. ユーザーエクスペリエンス: ローディングインジケータやバリデーションの実装
まとめ
本記事では、カスタムAPIクライアントを使用して社員マスタデータベースから階層的に社員を検索するフォームの実装方法を紹介しました。
この実装方法は、大規模な組織での社員検索に非常に有効であり、ユーザーが直感的に操作できるインターフェースを提供します。
また、同様の階層型検索フォームは、他の用途(例:商品カテゴリ検索、地域検索など)にも応用できます。