フォーム上で金融機関・支店を検索一覧から選べるようにし、選択結果を項目へ格納する構成を紹介します。
検索用の中継は、フォームとは別にSPIRAL内にもう1つページを作成し、そのPHPタブ・BODYタブに貼り付ける前提です。ページのURLがそのままブラウザから叩ける中継エンドポイントになります。入力フォーム側の BODYタブ には検索UIのマークアップ、JSタブ にはその動作JSを貼り付けます。
検索処理は BankcodeJP API を利用します。
注意点
・ APIキーは
・ 中継用ページは、PHPを貼り付けるためだけの空ページとして運用してください。
・ 無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
・
ページのPHP(サーバ側)に保持し、
JSコード(クライアント)には置きません。
・ 中継用ページは、PHPを貼り付けるためだけの空ページとして運用してください。
・ 無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
・
curlのタイムアウト(接続・全体)は短めに設定し、外部API遅延で画面が固まらないようにしてください。
実装の概要
事前準備. BankcodeJP で APIキー取得。SPIRAL内に「中継用ページ」を1つ作成し、その
本来の入力フォーム(銀行を選ばせたい画面)の
1. 入力フォーム側の
2. 中継ページの PHPタブ(
3. 中継ページの BODYタブ(
4. 呼び出し側の JS(
5. 候補を選んだら、既存のフォーム入力項目(
PHPタブに
PHPコード、
BODYタブに
HTMLコードを貼り付けます。
本来の入力フォーム(銀行を選ばせたい画面)の
BODYタブに
HTMLコード、
JSタブに
JSコードを貼り付けます。
1. 入力フォーム側の
JSコードが、ユーザー入力を base64 化して中継ページのURLへ
fetchする。
2. 中継ページの PHPタブ(
PHPコード)が base64 を復号して入力を検証し、
curlで BankcodeJP を呼び、結果を
$SPIRAL->setTHValue('bankcodejp_result', base64(json)) で Thymeleaf に渡す。3. 中継ページの BODYタブ(
HTMLコード)の Thymeleaf が、渡された値を
<div id="bankcodejp-data" th:attr="data-result=...">の data 属性に埋め込む。
4. 呼び出し側の JS(
JSコード)が、
DOMParserで受信HTMLから
#bankcodejp-dataの
data-result属性を取り出し、base64 復号 → JSON.parse して候補一覧を
HTMLコードのUI内に表示する。
5. 候補を選んだら、既存のフォーム入力項目(
f01〜f04)に値を書き込み、送信する。
事前準備
以下を用意します。識別名は環境に合わせて読み替えてください。
| 用途 | 識別名(例) | フィールドタイプ |
|---|---|---|
| 金融機関コード(4桁・数字のみ) | bank_code | テキスト(最大128字) |
| 金融機関名 | bank_name | テキスト(最大128字) |
| 支店コード(3桁・数字のみ) | branch_code | テキスト(最大128字) |
| 支店名 | branch_name | テキスト(最大128字) |
filterを参照してください。
設定方法
1. 中継ページのPHPタブ(curl で BankcodeJP を呼び、結果を Thymeleaf に渡す)
SPIRAL内に中継用のページを作成し、そのPHPタブ に以下を貼り付けます。curl_*を用いて外部HTTPを行い、結果は
$SPIRAL->setTHValue('bankcodejp_result', base64_encode(json_encode(...))) で Thymeleaf に渡します。
<?php
/**
* SPIRAL ver.2 で、検索用の中継「PHPタブ」に貼り付けるコード。
*
*/
$BANKCODEJP_API_KEY = '***REPLACE_WITH_YOUR_KEY***';
$BANKCODEJP_API_BASE = 'https://apis.bankcode-jp.com/v3';
$response = null;
try {
// SPIRAL ver.2 の PHPタブでは、$_GET は使えず $SPIRAL->getParam() で
// GET/POST パラメータを取得する必要がある。
$mode = (string)($SPIRAL->getParam('mode') ?? '');
// q はJS側で UTF-8 → base64 にエンコードして渡してもらう。
$qRaw = (string)($SPIRAL->getParam('q') ?? '');
$q = $qRaw === '' ? '' : (string)base64_decode($qRaw, true);
$q = trim($q);
$bank = preg_replace('/\D/', '', (string)($SPIRAL->getParam('bank') ?? ''));
if ($q === '' || mb_strlen($q) > 32) {
throw new RuntimeException('invalid q');
}
$safe = preg_replace('/[^\x{3041}-\x{3096}A-Za-z0-9\-]/u', '', $q);
if ($safe === null || $safe === '') {
throw new RuntimeException('invalid characters');
}
$filter = 'hiragana==' . $safe . '*';
$query = http_build_query([
'apikey' => $BANKCODEJP_API_KEY,
'limit' => 30,
'filter' => $filter,
'fields' => 'code,name,hiragana',
]);
if ($mode === 'bank') {
$url = $BANKCODEJP_API_BASE . '/banks?' . $query;
} elseif ($mode === 'branch' && strlen($bank) === 4) {
$url = $BANKCODEJP_API_BASE . '/banks/' . $bank . '/branches?' . $query;
} else {
throw new RuntimeException('invalid mode or bank');
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$body = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException('upstream unreachable: ' . $curlErr);
}
if ($httpCode >= 400) {
throw new RuntimeException('upstream status ' . $httpCode);
}
$decoded = json_decode((string)$body, true);
if (!is_array($decoded)) {
throw new RuntimeException('invalid upstream json');
}
$response = ['ok' => true, 'status' => 200, 'data' => $decoded];
} catch (Throwable $e) {
$response = ['ok' => false, 'status' => 400, 'error' => $e->getMessage()];
}
// Thymeleaf に base64(json) 文字列として渡す。
// HTMLタブ(code1.html)側で data 属性に埋め込まれ、呼び出し側 JS から取り出される。
$json = json_encode($response, JSON_UNESCAPED_UNICODE);
$b64 = base64_encode($json);
$SPIRAL->setTHValue('bankcodejp_result', $b64);
2. 中継ページのBODYタブ(Thymeleaf で data 属性に埋め込む)
同じページのBODYタブ に以下を貼り付けます。th:attr="data-result=${cp.result.value['bankcodejp_result']}" で、PHPタブから渡された base64 文字列を
<div id="bankcodejp-data">の
data-result属性に埋め込みます。
<div id="bankcodejp-data"
th:data-result="|${cp.result.value['bankcodejp_result']}|"></div>
<pre id="bankcodejp-text" style="display:none;"
th:text="${cp.result.value['bankcodejp_result']}"></pre>
3. 入力フォームのBODYタブ(検索UIマークアップ)
銀行を選ばせたい本来のフォームブロックのBODYタブ、デフォルトソースの <div class="sp-form-container">内に、以下の検索UIマークアップを貼り付けます。
<div class="sp-form-container">
<!-- 銀行検索UI -->
<div class="sp-form-item sp-form-html" th:inline="none">
<p><span style="font-size: 18pt;">銀行検索フォーム</span></p>
<div class="sp-form-field">
<div class="sp-form-label">金融機関検索(ひらがな前方一致)</div>
<div class="sp-form-data">
<input type="text" id="bank-q" placeholder="例: みずほ" maxlength="32">
<button type="button" id="bank-search-btn">検索</button>
<ul id="bank-list"></ul>
<p id="bank-selected" style="display:none;">
選択中: <strong id="bank-selected-name"></strong>(<span id="bank-selected-code"></span>)
<button type="button" id="bank-reset-btn">変更</button>
</p>
</div>
</div>
<div class="sp-form-field" id="branch-search-area" style="display:none;">
<div class="sp-form-label">支店検索(ひらがな前方一致)</div>
<div class="sp-form-data">
<input type="text" id="branch-q" placeholder="例: しんじゅく" maxlength="32">
<button type="button" id="branch-search-btn">検索</button>
<ul id="branch-list"></ul>
<p id="branch-selected" style="display:none;">
選択中: <strong id="branch-selected-name"></strong>(<span id="branch-selected-code"></span>)
</p>
</div>
</div>
<p class="sp-form-error" id="bank-search-msg"></p>
</div>
<!--/* 金融機関コード(4桁・数字のみ)(bank_code) */-->
<sp:input-field name="f01"></sp:input-field>
<input type="hidden" id="f-bank-code" th:name="${fields['f01'].name}" th:value="${inputs['f01']}">
<!--/* 金融機関名(bank_name) */-->
<sp:input-field name="f02"></sp:input-field>
<input type="hidden" id="f-bank-name" th:name="${fields['f02'].name}" th:value="${inputs['f02']}">
<!--/* 支店コード(3桁・数字のみ)(branch_code) */-->
<sp:input-field name="f03"></sp:input-field>
<input type="hidden" id="f-branch-code" th:name="${fields['f03'].name}" th:value="${inputs['f03']}">
<!--/* 支店名(branch_name) */-->
<sp:input-field name="f04"></sp:input-field>
<input type="hidden" id="f-branch-name" th:name="${fields['f04'].name}" th:value="${inputs['f04']}">
<!-- 送信ボタン(デフォルトソースのまま) -->
<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>
4. 入力フォームのJSタブ(fetch で中継ページを呼び、data 属性から結果を取り出す)
フォームブロックを設置したページのJSタブ に以下のJSを貼り付けます。fetchしたレスポンスHTMLを
DOMParserで解析し、
#bankcodejp-dataの
data-result属性から base64 文字列を取り出して復号します。
BANK_PROXY_URLは中継用ページのURLに差し替えてください。
※ JSタブ内では Thymeleaf インライン式(
[[${...}]])は展開されないため、本JSは DOMParser経由で BODYタブ側の data 属性を読む方式で書かれています。本サンプルは ステップ構成 (Step1 =
s01) の
f01〜f04を対象としており、入力項目は
[name="s01.f01"]優先・
[name="f01"]フォールバックで取得します。ステップIDやフィールドIDが異なる環境では JS 内の
STEP定数と
findFieldの引数を読み替えてください。
// PHPコードを貼り付けた中継ページのフルURL(環境に合わせて差し替え)
var BANK_PROXY_URL = 'https://example.com/path/to/bankcodejp_proxy_page';
// ---- base64(UTF-8対応)ユーティリティ ----
function b64EncodeUtf8(str) {
var bytes = new TextEncoder().encode(str);
var bin = '';
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function b64DecodeUtf8(b64) {
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder('utf-8').decode(bytes);
}
async function callBankProxy(params) {
// BANK_PROXY_URL に既に ? が含まれている場合は & で繋ぐ
var sep = BANK_PROXY_URL.indexOf('?') >= 0 ? '&' : '?';
var sendParams = Object.assign({}, params);
if (typeof sendParams.q === 'string') sendParams.q = b64EncodeUtf8(sendParams.q);
var url = BANK_PROXY_URL + sep + new URLSearchParams(sendParams).toString();
var res = await fetch(url, { credentials: 'same-origin' });
if (!res.ok) throw new Error('proxy http ' + res.status);
// 中継ページの BODYタブ (code2.html) が Thymeleaf で値を埋め込んだ HTML を返す前提。
// data属性方式 と th:text方式の両方を試して、どちらかで取得できればそれを使う。
var html = await res.text();
var doc = new DOMParser().parseFromString(html, 'text/html');
// 1) data-result 属性から取得(推奨・公式方法③)
var b64 = '';
var dataEl = doc.getElementById('bankcodejp-data');
if (dataEl) {
b64 = (dataEl.getAttribute('data-result') || '').trim();
}
// 2) フォールバック: th:text で書き出した <pre id="bankcodejp-text"> の textContent
if (!b64) {
var textEl = doc.getElementById('bankcodejp-text');
if (textEl) {
b64 = (textEl.textContent || '').trim();
}
}
if (!b64) {
// 切り分け用にレスポンス全体と、見つかった要素の状態を console に出す
console.log('[bankcodejp] raw response:', html);
console.log('[bankcodejp] #bankcodejp-data:', dataEl ? dataEl.outerHTML : '(not found)');
throw new Error('Thymeleaf の値が空です。PHPタブの $SPIRAL->setTHValue が効いているか、BODYタブのキー名 (bankcodejp_result) が一致しているか確認してください。');
}
var payload;
try {
payload = JSON.parse(b64DecodeUtf8(b64));
} catch (err) {
throw new Error('invalid base64/json in data-result');
}
if (!payload.ok) throw new Error(payload.error || 'proxy error');
return payload.data;
}
async function fetchBanks(hiraganaPrefix) {
var safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
if (!safe) throw new Error('invalid input');
return callBankProxy({ mode: 'bank', q: safe });
}
async function fetchBranches(bankCode4, hiraganaPrefix) {
var safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
if (!safe) throw new Error('invalid input');
if (!/^\d{4}$/.test(bankCode4)) throw new Error('invalid bank code');
return callBankProxy({ mode: 'branch', q: safe, bank: bankCode4 });
}
// ---- UI 配線(DOM ready 後)----
document.addEventListener('DOMContentLoaded', function () {
var $ = function (id) { return document.getElementById(id); };
var msgEl = $('bank-search-msg');
var msg = function (t) { if (msgEl) msgEl.textContent = t || ''; };
// BODYタブ (code3.html) で配置した hidden input を固定IDで取得する。
// th:name="${fields['fNN'].name}" により runtime の name 属性は
// SPIRAL 側の仕様(ステップ prefix の有無)に自動追従するため、
// JS 側はステップ名を意識する必要がない。
var bankCodeInput = $('f-bank-code');
var bankNameInput = $('f-bank-name');
var branchCodeInput = $('f-branch-code');
var branchNameInput = $('f-branch-name');
// 選択状態は JS 内部で保持
var selectedBankCode = '';
var selectedBankName = '';
function renderList(listEl, items, onPick) {
listEl.innerHTML = '';
if (!items || items.length === 0) {
listEl.innerHTML = '<li>該当なし</li>';
return;
}
items.forEach(function (it) {
var li = document.createElement('li');
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = it.name + '(' + it.code + ')';
btn.addEventListener('click', function () { onPick(it); });
li.appendChild(btn);
listEl.appendChild(li);
});
}
var bankSearchBtn = $('bank-search-btn');
if (bankSearchBtn) bankSearchBtn.addEventListener('click', async function () {
msg('');
try {
var data = await fetchBanks($('bank-q').value);
var items = data.banks || data.items || data || [];
renderList($('bank-list'), items, function (bank) {
selectedBankCode = String(bank.code || '');
selectedBankName = String(bank.name || '');
if (bankCodeInput) bankCodeInput.value = selectedBankCode;
if (bankNameInput) bankNameInput.value = selectedBankName;
$('bank-selected-name').textContent = selectedBankName;
$('bank-selected-code').textContent = selectedBankCode;
$('bank-selected').style.display = '';
$('branch-search-area').style.display = '';
$('bank-list').innerHTML = '';
});
} catch (e) { msg('検索に失敗しました: ' + e.message); }
});
var bankResetBtn = $('bank-reset-btn');
if (bankResetBtn) bankResetBtn.addEventListener('click', function () {
selectedBankCode = '';
selectedBankName = '';
if (bankCodeInput) bankCodeInput.value = '';
if (bankNameInput) bankNameInput.value = '';
if (branchCodeInput) branchCodeInput.value = '';
if (branchNameInput) branchNameInput.value = '';
$('bank-selected').style.display = 'none';
var brSel = $('branch-selected'); if (brSel) brSel.style.display = 'none';
$('branch-search-area').style.display = 'none';
$('branch-list').innerHTML = '';
});
var branchSearchBtn = $('branch-search-btn');
if (branchSearchBtn) branchSearchBtn.addEventListener('click', async function () {
msg('');
if (!/^\d{4}$/.test(selectedBankCode)) { msg('先に金融機関を選んでください'); return; }
try {
var data = await fetchBranches(selectedBankCode, $('branch-q').value);
var items = data.branches || data.items || data || [];
renderList($('branch-list'), items, function (br) {
var code = String(br.code || '');
var name = String(br.name || '');
if (branchCodeInput) branchCodeInput.value = code;
if (branchNameInput) branchNameInput.value = name;
$('branch-selected-name').textContent = name;
$('branch-selected-code').textContent = code;
$('branch-selected').style.display = '';
$('branch-list').innerHTML = '';
});
} catch (e) { msg('検索に失敗しました: ' + e.message); }
});
});
エラーハンドリング(失敗時の動き)
・ BankcodeJPから 429 が返った場合は、
・
PHPコードが
ok:falseを返し、
JSコードが「時間をおいて再試行」を表示するなど、ユーザーが手入力にフォールバックできるようにする。
・
curlタイムアウト・接続エラー時は
ok:falseを返し、画面ロードをブロックしない。
実行結果
・ フォーム上で金融機関・支店を選択すると、当該項目にコードと名称が格納され、登録される。
まとめ
本記事では、SPIRAL内で完結する構成として、中継用のページに
curlでBankcodeJPを呼ぶPHPを貼り付け、フォーム側のJSからそのページに
fetchする流れを示しました。