htmx 逆引きレシピ
その場で入力エラーを出すには?(バリデーション)
公開日:
最終更新日:
「送信してからエラーが出る」ではなく、入力しているその場でエラーや注意を返せると、フォームの迷いが減って入力体験が一気に良くなります。
htmxなら、入力イベントに合わせてサーバーへ軽く問い合わせ、結果エリアだけを差し替えるだけで“即フィードバック”を実装できます。
このページでは、社員番号の重複チェック/メール形式/在庫不足を即警告の3例で、業務フォームで使える「その場バリデーション」パターンをデモ付きで解説します。
使うのは hx-post / hx-trigger / hx-target / hx-validate / hx-sync の基本だけです。
使用するhtmx属性
hx-post:入力値をPOSTで送って、検証結果のHTMLを返して差し替える(即バリデーション向き)hx-trigger:検証を走らせるタイミングを指定(例:keyup changed delay:300ms/blur/change)hx-target:返ってきたHTMLを差し替える先の要素(エラーメッセージ表示エリア)を指定hx-validate:リクエスト前にHTML5バリデーションを実行し、無効なら送信を止める(例:必須・形式チェックの下支え)hx-sync:リクエスト競合を制御し、古い結果で上書きされる事故を防ぐ(例:this:replace)
利用シーン
- 「社員番号の重複チェック」:入力した社員番号が既に使われていないか、その場で確認したい
- 「メール形式」:メールの形式・使用済みなどを、入力中に即フィードバックしたい
- 「在庫不足を即警告」:商品と数量の入力に合わせて、在庫不足ならすぐ警告を出したい
① 社員番号の重複チェック
社員番号を入力すると、その場で「数字か」「桁数は足りるか」「重複していないか」をチェックする例です。
入力中に結果が出るので、送信後の差し戻しを減らしてスムーズに登録できます。
HTML
<div class="DEMO">
<h4>① 社員番号の重複チェック</h4>
<label>
社員番号
<input
id="DEMO_EMP_NO"
type="text"
name="empno"
maxlength="4"
inputmode="numeric"
autocomplete="off"
placeholder="例:1001"
hx-post="/htmx/demo/_validate_1.php"
hx-trigger="keyup changed delay:300ms, blur"
hx-target="#DEMO_EMP_NO_RESULT"
hx-validate="true"
hx-sync="this:replace"
>
</label>
<div id="DEMO_EMP_NO_RESULT" class="FORM-RESULT" aria-live="polite">
<span class="HTMX-NOTE">社員番号を入力すると、重複チェックします。</span>
</div>
</div>
PHP
<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// POSTパラメータ empno(社員番号)を受け取る(なければ空)
$empno = (string)($_POST['empno'] ?? '');
// 前後空白を除去する
$empno = trim($empno);
// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);
// 未入力なら案内を出して終了する
if ($empno === '') {
// 入力案内を表示する
echo '<span class="HTMX-NOTE">社員番号を入力すると、重複チェックします。</span>';
// ここで終了する
exit;
}
// 数字以外が混ざっていたらエラーを出して終了する
if (!preg_match('/^\d+$/', $empno)) {
// エラー表示(数字のみ)
echo '<div class="FORM-RESULT is-ng"><strong>数字のみ</strong>で入力してください。</div>';
// ここで終了する
exit;
}
// 桁数が短すぎる場合はエラーを出して終了する(例:4桁以上)
if (strlen($empno) < 4) {
// エラー表示(桁数)
echo '<div class="FORM-RESULT is-ng"><strong>4桁以上</strong>で入力してください。</div>';
// ここで終了する
exit;
}
// 既に存在する社員番号(デモ用。本番はDB検索に置き換え)
$exists = [
'1001',
'1002',
'1024',
'1100',
];
// 重複しているかどうかを判定する
$isDuplicate = in_array($empno, $exists, true);
// 重複している場合はエラーを返す
if ($isDuplicate) {
// 重複エラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>重複</strong>しています。別の番号を入力してください。</div>';
// ここで終了する
exit;
}
// 問題なければOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:使用できます。</div>';
デモ
① 社員番号の重複チェック
社員番号を入力すると、重複チェックします。
解説
- 入力欄に
hx-postを付け、社員番号(empno)をサーバーへ送って検証します。 hx-triggerでkeyup+delayとblurを拾い、入力中と入力後の両方でチェックします。- 検証結果(OK/NGメッセージ)は
hx-targetで指定した結果エリアへ差し替え表示します。 hx-validate="true"により、HTML5の制約がある場合は無効な送信を抑止できます。hx-sync="this:replace"で、連続入力でも古い結果が後から来て上書きされる事故を防ぎます。- PHP側は「未入力」「数字のみ」「桁数」「既存と一致(重複)」を順に判定し、短いHTML断片を返します。
② メール形式
メールアドレスを入力すると、その場で「メール形式か」「既に使用済みか」をチェックする例です。
入力直後にNG理由が分かるので、登録フォームの失敗を減らせます。
HTML
<div class="DEMO">
<h4>② メール形式</h4>
<label>
メールアドレス
<input
id="DEMO_EMAIL"
type="text"
name="email"
maxlength="64"
autocomplete="off"
placeholder="例:user@example.com"
hx-post="/htmx/demo/_validate_2.php"
hx-trigger="keyup changed delay:300ms, blur"
hx-target="#DEMO_EMAIL_RESULT"
hx-validate="true"
hx-sync="this:replace"
>
</label>
<div id="DEMO_EMAIL_RESULT" class="FORM-RESULT" aria-live="polite">
<span class="HTMX-NOTE">メールアドレスを入力すると、その場で形式チェックします。</span>
</div>
PHP
<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// POSTパラメータ email を受け取る(なければ空)
$email = (string)($_POST['email'] ?? '');
// 前後空白を除去する
$email = trim($email);
// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);
// 未入力なら案内を出して終了する
if ($email === '') {
// 入力案内を表示する
echo '<span class="HTMX-NOTE">メールアドレスを入力すると、その場で形式チェックします。</span>';
// ここで終了する
exit;
}
// PHPの組み込みでメール形式を判定する
$isValid = (filter_var($email, FILTER_VALIDATE_EMAIL) !== false);
// 形式が不正ならエラーを返す
if (!$isValid) {
// 形式エラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>メール形式</strong>が正しくありません。</div>';
// ここで終了する
exit;
}
// (デモ)既に使われているメール(本番はDB検索に置き換え)
$used = [
// 既存メール
'test@example.com',
// 既存メール
'admin@example.com',
];
// 既存メールと一致するかを判定する(大文字小文字は同一扱いにする)
$isUsed = in_array(mb_strtolower($email), array_map('mb_strtolower', $used), true);
// 既に使われていたらエラーを返す
if ($isUsed) {
// 使用済みエラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>既に使用</strong>されています。</div>';
// ここで終了する
exit;
}
// 問題なければOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:使用できます。</div>';
デモ
② メール形式
メールアドレスを入力すると、その場で形式チェックします。
解説
- 入力欄に
hx-postを付け、メール(email)をサーバーへ送って検証します。 hx-triggerはkeyup changed delayとblurの組み合わせで、操作感と負荷のバランスを取ります。- 返ってきた検証結果HTMLを、
hx-targetで指定した結果エリアへ差し替えます。 hx-validate="true"で、requiredやtype="email"等のHTML5の制約がある場合は送信前に弾けます。hx-sync="this:replace"で、入力の途中結果がズレて表示されるのを防止します。- PHP側は
filter_varで形式判定し、(デモでは)使用済みリストと照合してOK/NGを返します。
③ 在庫不足を即警告
商品と数量を選ぶたびに在庫を確認し、足りなければその場で警告を出す例です。
「注文してから在庫不足」になりにくく、業務UIの入力ミスを早期に潰せます。
HTML
<div class="DEMO">
<h4>③ 在庫不足を即警告</h4>
<form
id="DEMO_STOCK_FORM"
class="FORM"
hx-post="/htmx/demo/_validate_3.php"
hx-trigger="change, keyup changed delay:300ms"
hx-target="#DEMO_STOCK_RESULT"
hx-validate="true"
hx-sync="this:replace"
>
<label>
商品
<select name="sku">
<option value="">選択してください</option>
<option value="JUICE">みかんジュース</option>
<option value="JELLY">みかんゼリー</option>
<option value="BOX">みかん箱</option>
</select>
</label>
<label>
数量
<input type="text" name="qty" maxlength="2" inputmode="numeric" placeholder="例:2">
</label>
</form>
<div id="DEMO_STOCK_RESULT" class="FORM-RESULT" aria-live="polite">
<span class="HTMX-NOTE">商品と数量を入力すると、在庫を確認して不足なら即警告します。</span>
</div>
</div>
PHP
<?php
// 型を厳密に扱う(意図しない型変換を防ぐ)
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// POSTパラメータ sku(商品コード)を受け取る(なければ空)
$sku = (string)($_POST['sku'] ?? '');
// 前後空白を除去する
$sku = trim($sku);
// POSTパラメータ qty(数量)を受け取る(なければ空)
$qtyRaw = (string)($_POST['qty'] ?? '');
// 前後空白を除去する
$qtyRaw = trim($qtyRaw);
// (デモ)少し待って“チェックしてる感”を出す
usleep(120000);
// 在庫マスター(デモ用。本番はDB/APIに置き換え)
$stockMap = [
// 商品コード => [商品名, 在庫数]
'JUICE' => ['name' => 'みかんジュース', 'stock' => 5],
// 商品コード => [商品名, 在庫数]
'JELLY' => ['name' => 'みかんゼリー', 'stock' => 2],
// 商品コード => [商品名, 在庫数]
'BOX' => ['name' => 'みかん箱', 'stock' => 0],
];
// 商品未選択なら案内を返して終了する
if ($sku === '') {
// 案内を表示する
echo '<span class="HTMX-NOTE">商品を選択してください。</span>';
// ここで終了する
exit;
}
// 商品コードが未知ならエラーを返して終了する
if (!array_key_exists($sku, $stockMap)) {
// エラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>不正な商品</strong>です。</div>';
// ここで終了する
exit;
}
// 数量が未入力なら案内を返して終了する
if ($qtyRaw === '') {
// 案内を表示する
echo '<span class="HTMX-NOTE">数量を入力してください。</span>';
// ここで終了する
exit;
}
// 数量が数字以外ならエラーを返して終了する
if (!preg_match('/^\d+$/', $qtyRaw)) {
// エラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>数量</strong>は数字のみで入力してください。</div>';
// ここで終了する
exit;
}
// 数量を整数に変換する
$qty = (int)$qtyRaw;
// 数量が0以下ならエラーを返して終了する
if ($qty <= 0) {
// エラーを表示する
echo '<div class="FORM-RESULT is-ng"><strong>数量</strong>は1以上で入力してください。</div>';
// ここで終了する
exit;
}
// 商品名を取り出す
$name = (string)$stockMap[$sku]['name'];
// 在庫数を取り出す
$stock = (int)$stockMap[$sku]['stock'];
// 在庫不足なら警告を返す
if ($qty > $stock) {
// 不足数を計算する
$lack = $qty - $stock;
// 在庫不足メッセージを表示する
echo '<div class="FORM-RESULT is-ng"><strong>在庫不足</strong>:' . $name . '(在庫 ' . $stock . ')/不足 ' . $lack . '</div>';
// ここで終了する
exit;
}
// ちょうど在庫ぴったりなら注意メッセージを返す
if ($qty === $stock) {
// ぴったりメッセージを表示する
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:' . $name . '(在庫ちょうど ' . $stock . ')</div>';
// ここで終了する
exit;
}
// 在庫に余裕があればOKを返す
echo '<div class="FORM-RESULT is-ok"><strong>OK</strong>:' . $name . '(在庫 ' . $stock . ')</div>';
デモ
③ 在庫不足を即警告
商品と数量を入力すると、在庫を確認して不足なら即警告します。
解説
- フォーム全体に
hx-postを付け、商品(sku)と数量(qty)をまとめて送ります。 hx-triggerでchange(商品変更)とkeyup changed delay(数量入力)を拾い、条件が変わるたびに再チェックします。- 検証結果(在庫OK/不足警告)は
hx-targetで指定した結果エリアへ差し替え表示します。 hx-validate="true"で、HTML側に制約がある場合は無効な送信を抑止できます。hx-sync="this:replace"で、短時間に条件が変わっても最新結果を表示します。- PHP側は「未選択」「数量の妥当性」「在庫数との比較」を順に判定し、不足なら不足数まで含めた警告HTMLを返します。
エラーまとめ(よくある詰まりどころ)
-
入力中に結果がチラつく/頻繁に通信してしまう
対策:hx-triggerにdelay:300msを入れて間引く(例:keyup changed delay:300ms)。 -
古いチェック結果が後から返ってきて、最新の状態を上書きする
対策:hx-sync="this:replace"を付けて、同じ要素からの重なったリクエストは「新しいものを優先」する。 -
未入力のときもエラーが出続けて、入力しづらい
対策:未入力は「案内(NOTE)」を返す/blur時だけ厳しめ判定、入力中は軽め判定など、段階を分ける。 -
フォーム全体(商品+数量など)で検証したいのに、片方しか送られていない
対策:フォームにhx-postを付けてまとめて送る(今回の在庫チェック方式)。入力単体なら<input>に付ける。 -
サーバー側で「未選択/未入力」の扱いがブレて、表示が不安定になる
対策:PHPは最初に「未入力なら案内を返して終了」を統一する(ガード節を先頭に置く)。 -
HTML5バリデーションで送信が止まって、サーバー検証が走らない
対策:hx-validate="true"を使う場合は、HTML側のrequired/patternなどの制約と役割を整理する(厳しすぎる制約は外す)。 -
結果エリアが入れ替わらない/別の場所に入ってしまう
対策:hx-targetのCSSセレクタを確認(IDの重複、スペルミス、要素の存在タイミング)。 -
PHPがHTMLを返しているのに表示が崩れる
対策:レスポンスは「差し替えたい部分だけ」を返す(余計なHTML/改行/警告表示を出さない)。必要なら先頭でheader('Content-Type: text/html; charset=UTF-8');を統一する。 -
日本語や記号を含む入力で検証が怪しい/比較がズレる
対策:trim+正規化(必要なら)+比較は同一ルールで(メールは小文字化して照合など)。
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール