htmx 逆引きレシピ
失敗時にエラーだけ差し替えるには?(422など)
通常の成功レスポンスと、入力エラー/権限エラー時の表示先を分けたい場面はよくあります。
このページでは、失敗時だけ target / swap を切り替え、エラー領域のみ安全に差し替えるパターンをまとめます。
使用するhtmx属性
タグ:hx-target / hx-swap / hx-on / hx-request
hx-target:通常時の反映先を定義し、失敗時はイベントで動的に差し替えます。hx-swap:成功時の標準swapを決め、失敗時だけ別戦略(例:innerHTML)へ切り替えます。hx-on:htmx:beforeRequest/htmx:beforeSwapでレスポンス状況に応じた振り分けを行います。hx-request:タイムアウト等を指定し、フォーム送信の振る舞いを明示します(HTMLレスポンス前提)。
利用シーン
- 入力ミスはフォーム直下に表示:422(バリデーション)だけをエラー枠へ差し替え、入力欄やフォーム全体は崩さずに伝えたい
- 権限エラーはバナーで表示:401/403 はページ上部にまとめて通知し、作業中でも気づける導線にしたい
- 成功時は通常表示:200時は結果領域だけ更新して、画面全体を動かさずスムーズに完了させたい
共通PHP(全文)
3つのデモで使う文言・初期値・共通関数は1ファイルへ集約します。
以降のサンプルからはこの共通ファイルを require_once して再利用します。
PHP(_show_errors_data.php)
/htmx/demo/_show_errors_data.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共有するデモ設定を配列で持つ
$SHOW_ERRORS_DEMO_CONFIG = [
'1' => [
'title' => 'DEMO1: 入力チェック(422)',
'defaultName' => 'もちもち みかん',
'defaultEmail' => 'mochi@example.com',
'hint' => 'メールを空欄にすると422を返します。',
],
'2' => [
'title' => 'DEMO2: 権限エラー(401/403)',
'defaultName' => 'tanaka',
'defaultEmail' => 'tanaka@example.com',
'hint' => 'role=guest は401、role=viewer は403、role=editor は成功です。',
],
'3' => [
'title' => 'DEMO3: 成功/失敗でswap戦略を分離',
'defaultName' => 'suzuki',
'defaultEmail' => 'suzuki@example.com',
'hint' => 'メールの形式が不正だと422を返します。',
],
];
// DEMO2で使うロール候補
$SHOW_ERRORS_ROLES = [
'guest' => 'guest(未ログイン想定)',
'viewer' => 'viewer(閲覧のみ)',
'editor' => 'editor(更新可)',
];
// HTMLエスケープ関数
function show_errors_h(string $s): string {
// 特殊文字をエスケープして返す
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// 空白を詰めた文字列を返す
function show_errors_clean(string $s): string {
// 前後空白を除去して返す
return trim($s);
}
// エラーボックスHTMLを返す
function show_errors_error_box(string $title, array $messages): string {
// 先頭HTMLを作る
$html = '<div class="FORM-RESULT is-ng"><strong>' . show_errors_h($title) . '</strong><ul>';
// メッセージを1件ずつ連結する
foreach ($messages as $msg) {
// liを追加する
$html .= '<li>' . show_errors_h((string)$msg) . '</li>';
}
// 閉じタグを連結する
$html .= '</ul></div>';
// 完成HTMLを返す
return $html;
}
// バナーHTMLを返す
function show_errors_banner_box(string $label, string $message): string {
// バナーを返す
return '<div class="FORM-RESULT is-ng"><strong>' . show_errors_h($label) . '</strong> ' . show_errors_h($message) . '</div>';
}
// 成功ブロックHTMLを返す
function show_errors_success_box(string $title, array $rows): string {
// 先頭HTMLを作る
$html = '<div class="FORM-RESULT is-ok"><strong>' . show_errors_h($title) . '</strong><ul>';
// 行を1件ずつ連結する
foreach ($rows as $row) {
// liを追加する
$html .= '<li>' . show_errors_h((string)$row) . '</li>';
}
// 閉じタグを連結する
$html .= '</ul></div>';
// 完成HTMLを返す
return $html;
}
① 最小構成(422でエラー領域のみ更新)
成功(200)は結果領域へ反映し、入力エラー(422)だけフォーム直下のエラー領域へ振り分けます。
htmx:beforeSwap で失敗時の target と swap を切り替える最小構成です。
※HTTPステータス(422/401/403)は、ブラウザの開発者ツール(Network)で確認できます。
HTML
_demo_htmx_1.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once("{$_SERVER[ 'DOCUMENT_ROOT' ]}/htmx/demo/_show_errors_data.php");
// DEMO1設定を取り出す
$cfg = $SHOW_ERRORS_DEMO_CONFIG['1'];
?>
<div class="DEMO">
<h4><?= show_errors_h((string)$cfg['title']) ?></h4>
<p class="HTMX-NOTE"><?= show_errors_h((string)$cfg['hint']) ?></p>
<form
id="DEMO1_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_show_errors_submit.php"
hx-target="#DEMO1_RESULT"
hx-swap="innerHTML"
hx-request='{"timeout":4000}'
hx-on::before-request="
document.getElementById('DEMO1_ERRORS').innerHTML = '';
"
>
<input type="hidden" name="demo" value="1">
<label>
名前
<input type="text" name="name" maxlength="64" value="<?= show_errors_h((string)$cfg['defaultName']) ?>">
</label>
<label>
メール
<input type="email" name="email" maxlength="128" value="<?= show_errors_h((string)$cfg['defaultEmail']) ?>">
</label>
<button type="submit" class="BTN is-ok">送信</button>
</form>
<div id="DEMO1_ERRORS" class="MT1rm"></div>
<div
id="DEMO1_RESULT"
class="FORM-RESULT"
hx-on::before-swap="
if (event.detail.xhr.status === 422) {
document.getElementById('DEMO1_RESULT').innerHTML = '<span class="HTMX-NOTE">成功時はここが更新されます。</span>';
event.detail.shouldSwap = true;
event.detail.isError = false;
event.detail.target = document.getElementById('DEMO1_ERRORS');
event.detail.swapOverride = 'innerHTML';
}
"
>
<span class="HTMX-NOTE">成功時はここが更新されます。</span>
</div>
</div>
PHP(送信先)
/htmx/demo/_show_errors_submit.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once(__DIR__ . '/_show_errors_data.php');
// HTMLを返す
header('Content-Type: text/html; charset=UTF-8');
// リクエストメソッドを取得する
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// ステータスを405で返す
http_response_code(405);
// メッセージを返す
echo show_errors_banner_box('405 Method Not Allowed', 'POSTで送信してください。');
// ここで処理を終える
exit;
}
// demoを受け取る
$demo = show_errors_clean((string)($_POST['demo'] ?? ''));
// nameを受け取る
$name = show_errors_clean((string)($_POST['name'] ?? ''));
// emailを受け取る
$email = show_errors_clean((string)($_POST['email'] ?? ''));
// roleを受け取る
$role = show_errors_clean((string)($_POST['role'] ?? 'guest'));
// nameが空なら補う
if ($name === '') {
// 補助値を入れる
$name = '(未入力)';
}
// DEMO1: 入力チェック(422)
if ($demo === '1') {
// メールが空なら422でエラーを返す
if ($email === '') {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メールアドレスを入力してください。']);
// ここで処理を終える
exit;
}
// 正常時は200のまま成功結果を返す
echo show_errors_success_box('送信に成功しました。', ['名前:' . $name, 'メール:' . $email]);
// ここで処理を終える
exit;
}
// DEMO2: 権限チェック(401/403)
if ($demo === '2') {
// roleがguestなら401を返す
if ($role === 'guest') {
// ステータス401を返す
http_response_code(401);
// バナー用HTMLを返す
echo show_errors_banner_box('401 Unauthorized', 'ログインが必要です。');
// ここで処理を終える
exit;
}
// roleがviewerなら403を返す
if ($role === 'viewer') {
// ステータス403を返す
http_response_code(403);
// バナー用HTMLを返す
echo show_errors_banner_box('403 Forbidden', 'この操作を実行する権限がありません。');
// ここで処理を終える
exit;
}
// 成功結果を返す
echo show_errors_success_box(
'更新に成功しました。',
[
'ユーザー:' . $name,
'メール:' . $email,
'ロール:' . $role,
]
);
// ここで処理を終える
exit;
}
// DEMO3: 成功はouterHTML差し替え、失敗はエラー領域だけ
if ($demo === '3') {
// メール形式をチェックする
$isValidEmail = (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
// 形式不正なら422でエラーを返す
if (!$isValidEmail) {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メール形式が不正です。example@example.com の形式で入力してください。']);
// ここで処理を終える
exit;
}
// 現在時刻を作る
$now = date('Y-m-d H:i:s');
// 成功時はouterHTML用にID付きカード全体を返す
echo '<div id="DEMO3_RESULT_CARD" class="CARD">';
// 見出しを返す
echo '<div><strong>最終保存:</strong>' . show_errors_h($now) . '</div>';
// 本文を返す
echo '<div><strong>担当者:</strong>' . show_errors_h($name) . '</div>';
// 本文を返す
echo '<div><strong>通知先:</strong>' . show_errors_h($email) . '</div>';
// 補足を返す
echo '<div class="HTMX-NOTE">成功時は outerHTML でカード全体を更新しました。</div>';
// 閉じる
echo '</div>';
// ここで処理を終える
exit;
}
// 想定外demoは400を返す
http_response_code(400);
// エラー表示を返す
echo show_errors_banner_box('400 Bad Request', 'demo指定が不正です。');
デモ
DEMO1: 入力チェック(422)
メールを空欄にすると422を返します。
解説
- 通常は
#DEMO1_RESULTへ差し込み、成功表示を更新します。 422のときだけevent.detail.targetを#DEMO1_ERRORSに変更し、shouldSwap=trueで表示します。- 失敗時の更新先を分離するため、フォーム本体や既存結果を崩さず再入力できます。
② 権限エラーをバナーに表示(401/403)
認可失敗(401/403)だけページ上部のバナーへ表示し、結果パネルは成功時だけ更新します。
ログイン切れやロール不足の通知を、フォームと分離して見せたいときの定番です。
※HTTPステータス(422/401/403)は、ブラウザの開発者ツール(Network)で確認できます。
HTML
/_demo_htmx_2.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once("{$_SERVER[ 'DOCUMENT_ROOT' ]}/htmx/demo/_show_errors_data.php");
// DEMO2設定を取り出す
$cfg = $SHOW_ERRORS_DEMO_CONFIG['2'];
?>
<div class="DEMO">
<h4><?= show_errors_h((string)$cfg['title']) ?></h4>
<p class="HTMX-NOTE"><?= show_errors_h((string)$cfg['hint']) ?></p>
<form
id="DEMO2_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_show_errors_submit.php"
hx-target="#DEMO2_RESULT"
hx-swap="innerHTML"
hx-request='{"timeout":5000}'
hx-on::before-request="
this.setAttribute('hx-target', '#DEMO2_RESULT');
this.setAttribute('hx-swap', 'innerHTML');
document.getElementById('DEMO2_BANNER').innerHTML = '<span class="HTMX-NOTE">権限エラー時のみ、ここにバナー表示します。</span>';
"
>
<input type="hidden" name="demo" value="2">
<label>
ユーザー名
<input type="text" name="name" maxlength="64" value="<?= show_errors_h((string)$cfg['defaultName']) ?>">
</label>
<label>
メール
<input type="email" name="email" maxlength="128" value="<?= show_errors_h((string)$cfg['defaultEmail']) ?>">
</label>
<label>
ロール
<select name="role">
<?php foreach ($SHOW_ERRORS_ROLES as $value => $label): ?>
<option value="<?= show_errors_h((string)$value) ?>"><?= show_errors_h((string)$label) ?></option>
<?php endforeach; ?>
</select>
</label>
<button type="submit" class="BTN is-ok">権限チェック付き送信</button>
</form>
<div
id="DEMO2_RESULT"
class="FORM-RESULT"
hx-on::before-swap="
const s = event.detail.xhr.status;
if (s === 401 || s === 403) {
/* 成功表示が残らないように(任意だがおすすめ) */
document.getElementById('DEMO2_RESULT').innerHTML = '<span class="HTMX-NOTE">成功時はここが更新されます。</span>';
event.detail.shouldSwap = true;
event.detail.isError = false;
event.detail.target = document.getElementById('DEMO2_BANNER');
event.detail.swapOverride = 'innerHTML';
}
"
>
<span class="HTMX-NOTE">成功時はここが更新されます。</span>
</div>
<div id="DEMO2_BANNER" class="FORM-RESULT MT1rm">
<span class="HTMX-NOTE">権限エラー時のみ、ここにバナー表示します。</span>
</div>
</div>
PHP(送信先)
/htmx/demo/_show_errors_submit.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once(__DIR__ . '/_show_errors_data.php');
// HTMLを返す
header('Content-Type: text/html; charset=UTF-8');
// リクエストメソッドを取得する
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// ステータスを405で返す
http_response_code(405);
// メッセージを返す
echo show_errors_banner_box('405 Method Not Allowed', 'POSTで送信してください。');
// ここで処理を終える
exit;
}
// demoを受け取る
$demo = show_errors_clean((string)($_POST['demo'] ?? ''));
// nameを受け取る
$name = show_errors_clean((string)($_POST['name'] ?? ''));
// emailを受け取る
$email = show_errors_clean((string)($_POST['email'] ?? ''));
// roleを受け取る
$role = show_errors_clean((string)($_POST['role'] ?? 'guest'));
// nameが空なら補う
if ($name === '') {
// 補助値を入れる
$name = '(未入力)';
}
// DEMO1: 入力チェック(422)
if ($demo === '1') {
// メールが空なら422でエラーを返す
if ($email === '') {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メールアドレスを入力してください。']);
// ここで処理を終える
exit;
}
// 正常時は200のまま成功結果を返す
echo show_errors_success_box('送信に成功しました。', ['名前:' . $name, 'メール:' . $email]);
// ここで処理を終える
exit;
}
// DEMO2: 権限チェック(401/403)
if ($demo === '2') {
// roleがguestなら401を返す
if ($role === 'guest') {
// ステータス401を返す
http_response_code(401);
// バナー用HTMLを返す
echo show_errors_banner_box('401 Unauthorized', 'ログインが必要です。');
// ここで処理を終える
exit;
}
// roleがviewerなら403を返す
if ($role === 'viewer') {
// ステータス403を返す
http_response_code(403);
// バナー用HTMLを返す
echo show_errors_banner_box('403 Forbidden', 'この操作を実行する権限がありません。');
// ここで処理を終える
exit;
}
// 成功結果を返す
echo show_errors_success_box(
'更新に成功しました。',
[
'ユーザー:' . $name,
'メール:' . $email,
'ロール:' . $role,
]
);
// ここで処理を終える
exit;
}
// DEMO3: 成功はouterHTML差し替え、失敗はエラー領域だけ
if ($demo === '3') {
// メール形式をチェックする
$isValidEmail = (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
// 形式不正なら422でエラーを返す
if (!$isValidEmail) {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メール形式が不正です。example@example.com の形式で入力してください。']);
// ここで処理を終える
exit;
}
// 現在時刻を作る
$now = date('Y-m-d H:i:s');
// 成功時はouterHTML用にID付きカード全体を返す
echo '<div id="DEMO3_RESULT_CARD" class="CARD">';
// 見出しを返す
echo '<div><strong>最終保存:</strong>' . show_errors_h($now) . '</div>';
// 本文を返す
echo '<div><strong>担当者:</strong>' . show_errors_h($name) . '</div>';
// 本文を返す
echo '<div><strong>通知先:</strong>' . show_errors_h($email) . '</div>';
// 補足を返す
echo '<div class="HTMX-NOTE">成功時は outerHTML でカード全体を更新しました。</div>';
// 閉じる
echo '</div>';
// ここで処理を終える
exit;
}
// 想定外demoは400を返す
http_response_code(400);
// エラー表示を返す
echo show_errors_banner_box('400 Bad Request', 'demo指定が不正です。');
デモ
DEMO2: 権限エラー(401/403)
role=guest は401、role=viewer は403、role=editor は成功です。
解説
- 成功時は
#DEMO2_RESULTを通常更新します。 401/403のときだけ#DEMO2_BANNERを target に切り替え、バナー表示へ統一します。- 権限系エラーを共通バナーへ集約すると、画面全体で通知体験を揃えやすくなります。
③ 成功/失敗でswap戦略を使い分け
成功時はカード全体を outerHTML で置換し、失敗時はエラー枠だけ innerHTML 更新します。
「成功時は大きく更新、失敗時は局所更新」の使い分けを、同じフォームで実現する例です。
※HTTPステータス(422/401/403)は、ブラウザの開発者ツール(Network)で確認できます。
HTML
/_demo_htmx_3.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once("{$_SERVER[ 'DOCUMENT_ROOT' ]}/htmx/demo/_show_errors_data.php");
// DEMO3設定を取り出す
$cfg = $SHOW_ERRORS_DEMO_CONFIG['3'];
?>
<div
class="DEMO"
hx-on::before-swap="
if (event.detail.xhr && event.detail.xhr.status === 422) {
const req = event.detail.requestConfig && event.detail.requestConfig.elt;
if (req && req.id === 'DEMO3_FORM') {
// (任意)成功表示が残るのを防ぐ
const card = document.getElementById('DEMO3_RESULT_CARD');
if (card) {
card.innerHTML =
'<div><strong>最終保存:</strong>未実行</div>' +
'<div><strong>備考:</strong>成功時はこのカード全体を outerHTML で差し替えます。</div>';
}
// 422でもswapして、エラー枠へ
event.detail.shouldSwap = true;
event.detail.isError = false;
event.detail.target = document.getElementById('DEMO3_ERRORS');
event.detail.swapOverride = 'innerHTML';
}
}
"
>
<h4><?= show_errors_h((string)$cfg['title']) ?></h4>
<p class="HTMX-NOTE"><?= show_errors_h((string)$cfg['hint']) ?></p>
<form
id="DEMO3_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_show_errors_submit.php"
hx-target="#DEMO3_RESULT_CARD"
hx-swap="outerHTML"
hx-request='{"timeout":5000}'
hx-on::before-request="
this.setAttribute('hx-target', '#DEMO3_RESULT_CARD');
this.setAttribute('hx-swap', 'outerHTML');
document.getElementById('DEMO3_ERRORS').innerHTML = '';
"
>
<input type="hidden" name="demo" value="3">
<label>
担当者
<input type="text" name="name" maxlength="64" value="<?= show_errors_h((string)$cfg['defaultName']) ?>">
</label>
<label>
通知先メール
<input type="text" name="email" maxlength="128" value="<?= show_errors_h((string)$cfg['defaultEmail']) ?>">
</label>
<button type="submit" class="BTN is-ok">保存(swap切替)</button>
</form>
<div id="DEMO3_ERRORS" class="MT1rm"></div>
<div id="DEMO3_RESULT_CARD" class="CARD">
<div><strong>最終保存:</strong>未実行</div>
<div><strong>備考:</strong>成功時はこのカード全体を outerHTML で差し替えます。</div>
</div>
</div>
PHP(送信先)
/htmx/demo/_show_errors_submit.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通データを読み込む
require_once(__DIR__ . '/_show_errors_data.php');
// HTMLを返す
header('Content-Type: text/html; charset=UTF-8');
// リクエストメソッドを取得する
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// ステータスを405で返す
http_response_code(405);
// メッセージを返す
echo show_errors_banner_box('405 Method Not Allowed', 'POSTで送信してください。');
// ここで処理を終える
exit;
}
// demoを受け取る
$demo = show_errors_clean((string)($_POST['demo'] ?? ''));
// nameを受け取る
$name = show_errors_clean((string)($_POST['name'] ?? ''));
// emailを受け取る
$email = show_errors_clean((string)($_POST['email'] ?? ''));
// roleを受け取る
$role = show_errors_clean((string)($_POST['role'] ?? 'guest'));
// nameが空なら補う
if ($name === '') {
// 補助値を入れる
$name = '(未入力)';
}
// DEMO1: 入力チェック(422)
if ($demo === '1') {
// メールが空なら422でエラーを返す
if ($email === '') {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メールアドレスを入力してください。']);
// ここで処理を終える
exit;
}
// 正常時は200のまま成功結果を返す
echo show_errors_success_box('送信に成功しました。', ['名前:' . $name, 'メール:' . $email]);
// ここで処理を終える
exit;
}
// DEMO2: 権限チェック(401/403)
if ($demo === '2') {
// roleがguestなら401を返す
if ($role === 'guest') {
// ステータス401を返す
http_response_code(401);
// バナー用HTMLを返す
echo show_errors_banner_box('401 Unauthorized', 'ログインが必要です。');
// ここで処理を終える
exit;
}
// roleがviewerなら403を返す
if ($role === 'viewer') {
// ステータス403を返す
http_response_code(403);
// バナー用HTMLを返す
echo show_errors_banner_box('403 Forbidden', 'この操作を実行する権限がありません。');
// ここで処理を終える
exit;
}
// 成功結果を返す
echo show_errors_success_box(
'更新に成功しました。',
[
'ユーザー:' . $name,
'メール:' . $email,
'ロール:' . $role,
]
);
// ここで処理を終える
exit;
}
// DEMO3: 成功はouterHTML差し替え、失敗はエラー領域だけ
if ($demo === '3') {
// メール形式をチェックする
$isValidEmail = (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
// 形式不正なら422でエラーを返す
if (!$isValidEmail) {
// ステータス422を返す
http_response_code(422);
// エラーHTMLを返す
echo show_errors_error_box('入力エラー', ['メール形式が不正です。example@example.com の形式で入力してください。']);
// ここで処理を終える
exit;
}
// 現在時刻を作る
$now = date('Y-m-d H:i:s');
// 成功時はouterHTML用にID付きカード全体を返す
echo '<div id="DEMO3_RESULT_CARD" class="CARD">';
// 見出しを返す
echo '<div><strong>最終保存:</strong>' . show_errors_h($now) . '</div>';
// 本文を返す
echo '<div><strong>担当者:</strong>' . show_errors_h($name) . '</div>';
// 本文を返す
echo '<div><strong>通知先:</strong>' . show_errors_h($email) . '</div>';
// 補足を返す
echo '<div class="HTMX-NOTE">成功時は outerHTML でカード全体を更新しました。</div>';
// 閉じる
echo '</div>';
// ここで処理を終える
exit;
}
// 想定外demoは400を返す
http_response_code(400);
// エラー表示を返す
echo show_errors_banner_box('400 Bad Request', 'demo指定が不正です。');
デモ
DEMO3: 成功/失敗でswap戦略を分離
メールの形式が不正だと422を返します。
解説
- フォームの標準設定は
hx-target="#DEMO3_RESULT_CARD"+hx-swap="outerHTML"です。 422失敗時は#DEMO3_ERRORSへ target を切り替え、innerHTMLでメッセージだけ更新します。htmx:beforeRequestで毎回デフォルトを戻すため、次回送信時の挙動がブレません。
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール