Analog Studio

フォームを Javascript でバリデーションするコツ

概要

現在では、ブラウザに標準でフォーム要素のバリデーションを行う機能が備わっています。
type 属性によって自動的に適応されるルールや、pattern 属性の正規表現によるルールなどを検証してくれてとても便利です。

このバリデーションのエラー表示は送信ボタンを押下したタイミングで表示されますが、上から順番に検証されて初めにバリデーションエラーが発生した入力要素にエラーを表示すると、そこで検証が終了してしまいます。
フォームの入力要素が数多くある場合には、これでは非常に非効率でユーザに与えるストレスが大きくなります。

そこで Javascript を使ってすべての要素を検証してエラーを全て表示するようにしてみましょう。

まずは完成したコアとなるコードを確認!

中身はさておき、まずはバリデーション動作のコアになるコードを以下に示します。
なるほどね~と理解できる方は、このセクションまでで大丈夫です。

IE 対応したい場合は、for...of 構文やアロー関数、const / let、関数の引数初期値などを書き直して下さい。

// バリデーションを全て確認する HTMLFormElement.prototype.getInvalidElements = function(_isNotCheckEmpty = false){ // エラーメッセージ const invalidMessages = { email : "メールアドレスの形式が正しくありません", tel : "入力形式が正しくありません", number : "数字で入力してください", }; const invalidElements = []; const formElements = this.elements; for(const elem of formElements){ const required = elem.required; const type = elem.type; const value = type !== "checkbox" && elem.value || elem.checked && elem.value || ""; const empty = elem.getAttribute("data-empty") || "この項目は入力必須です"; const invalid = elem.getAttribute("data-invalid") || invalidMessages[type] || "入力内容が正しくありません"; const parent = elem.parentNode; // hidden フィールドは無視する if("hidden" === type){ continue; } // バリデーション検証 if(elem.validity.valid || _isNotCheckEmpty && required && !value){ elem.setErrorMessage(); } else{ const message = (required && value || !required) && invalid || required && !value && empty || "値が正しくありません"; // エラー表示 elem.setErrorMessage(message); // エラー要素を配列に追加 // オブジェクトではなく、elem だけ追加しても良い invalidElements.push({ element: elem, require: required, type : type, message: message, }); } } return invalidElements; } // 入力要素の兄弟要素(<small>か疑似要素)にエラーメッセージを追加する Element.prototype.setErrorMessage = function(_message){ if(!/^(input|textarea|select)$/i.test(this.tagName)){ return false; } const elem = this; const parent = this.parentNode; // エラーメッセージ用の<small>要素 // 疑似要素でエラーを表示する場合は不要 const invalidMessages = parent.querySelectorAll("small.message-invalid"); invalidMessages.forEach( $smi => $smi.remove() ); if(!_message){ parent.removeAttribute("data-message-invalid"); elem.classList.remove("invalid"); return false; } // エラーメッセージ用の<small>要素のposition基準とするため、staticの場合はrelativeに変更 if(window.getComputedStyle(parent).position === "static"){ parent.style.position = "relative"; } parent.setAttribute("data-message-invalid", _message); elem.classList.add("invalid"); // 以下、<small>要素でエラーメッセージを表示するため const parentRect = parent.getBoundingClientRect(); const elementRect = elem.getBoundingClientRect(); const isHidden = !elementRect.left && !elementRect.right; const rightPosition = isHidden ? 0 : parentRect.right - elementRect.right; const topPosition = isHidden ? parentRect.bottom - parentRect.top + 8 : elementRect.bottom - parentRect.top + 8; // <small>要素を入力要素の右端に揃える // 見えない要素の場合は親要素の中央に表示 const invalidMessage = parent.appendChild(document.createElement("small")); invalidMessage.className = "message-invalid"; invalidMessage.innerText = _message; invalidMessage.style.position = "absolute"; invalidMessage.style.left = "0px"; invalidMessage.style.right = rightPosition + "px"; invalidMessage.style.top = topPosition + "px"; invalidMessage.style.textAlign = "right"; if(isHidden){ invalidMessage.style.textAlign = "center"; } return true; }

form 要素に対して、getInvalidElements() メソッドでバリデーションを通過しない要素を取得 & エラーメッセージの表示を行います。

エラーメッセージの表示は、親要素の疑似要素で表示するか、<small> 要素で表示します。
<input> で疑似要素が使えるととてもシンプルなのですが、疑似要素は使えませんので親要素 (できれば入力要素だけを含むラッパー要素) に追加とします。

使い方

使い方はとても単純で、<form> 要素の .getInvalidElements() メソッドを実行するだけでOKです。
送信ボタンの押下タイミングで実行するなら、以下のようにします。

// フォーム (取得方法はなんでも良いです) const form = document.getElementById("form"); // 送信ボタン const submitButton = document.getElementById("submit_button"); // イベント登録 submitButton.onclick = function(){ // バリデーションを通過しない入力要素を全て取得する const invalidElements = form.getInvalidElements(); // コンソールに表示 console.log(invalidElements); // 問題なければ送信処理 if(invalidElements.length < 1){ // 送信 form.submit(); // fetchやXHRでページ遷移なく送信してもOK fetch("https://web.analogstd.com/send_form.php", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new FormData(form), }); } // HTML5 のデフォルトバリデーション動作を停止させる return false; };

入力要素のカスタムデータ属性 (data-empty と data-invalid) にエラーメッセージを追加することで表示をカスタマイズできます。
data-empty 属性値は、入力必須の要素が空の場合に表示されます。
data-invalid 属性値は、入力内容が type 属性値により決まるルールや pattern 属性値によるマッチルール、のいずれかにマッチしない場合に表示されます。

実行サンプル

以下に実際に動くサンプルを用意したのでご確認ください。(送信処理はありません)

ブラウザに標準で組み込まれたバリデーションだと、チェックボックスに required 属性が設定されていた場合にエラーが表示されません。(2022年現在)
プライバシーポリシーへの同意などを求める場合には困ることも多いと思います。

今回紹介したバリデーション
検証状態を表示
ブラウザ組込のバリデーション
検証状態を表示
<html> <form id="test_user_form"> <dl class="user-form-wrapper"> <div class="item-row"> <dt><label for="user_name">お名前<small class="required">必須</small></label></dt> <dd><input type="text" name="user[お名前]" id="user_name" autocomplete="name" placeholder="山葉 五郎" data-empty="お名前を入力してください" required></dd> </div> <div class="item-row"> <dt><label for="user_name_kana">フリガナ</label></dt> <dd><input type="text" name="user[フリガナ]" id="user_name_kana" autocomplete="kana" placeholder="ヤマハ ゴロウ" pattern="^[\u30A0-\u30FF\uFF61-\uFF9F\s\u3000\u00A0]+$" data-empty="フリガナを入力してください" data-invalid="カタカナで入力されていません"></dd> </div> <div class="item-row"> <dt><label for="user_zip">郵便番号<small class="required">必須</small></label></dt> <dd><input type="tel" name="user[郵便番号]" id="user_zip" autocomplete="postal-code" placeholder="436-0222" pattern="^\d{3}-?\d{4}$" data-empty="郵便番号を入力してください" data-invalid="3桁-4桁の郵便番号ではありません" required></dd> </div> <div class="item-row"> <dt><label for="user_address">住所<small class="required">必須</small></label></dt> <dd><input type="text" name="user[ご住所]" id="user_address" autocomplete="address" placeholder="静岡県掛川市下垂木1111-11 アナログアパート101号室" data-empty="住所を入力してください" required></dd> </div> <div class="item-row"> <dt><label for="user_email">メールアドレス<small class="required">必須</small></label></dt> <dd><input type="email" name="user[メールアドレス]" id="user_email" autocomplete="email" placeholder="例: sample@web.analogstd.com" data-empty="メールアドレスを入力してください" data-invalid="メールアドレスの形式が正しくありません" required></dd> </div> <div class="item-row"> <dt><label for="user_tel">電話番号</label></dt> <dd><input type="tel" name="user[電話番号]" id="user_tel" autocomplete="tel" placeholder="例: 090-1234-9876" pattern="(\+\d+\s*)?(\d+[-\s]*)+\d+" data-empty="電話番号を入力してください" data-invalid="電話番号の形式が正しくありません"></dd> </div> </dl> <div class="submit-button-wrapper"> <button type="submit" id="user_submit_button">バリデーションテストを実行</button> </div> </form>
<Javascript> // バリデーションを全て確認する HTMLFormElement.prototype.getInvalidElements = function(_isNotCheckEmpty = false){ // エラーメッセージ const invalidMessages = { email : "メールアドレスの形式が正しくありません", tel : "入力形式が正しくありません", number : "数字で入力してください", }; const invalidElements = []; const formElements = this.elements; for(const elem of formElements){ const required = elem.required; const type = elem.type; const value = type !== "checkbox" && elem.value || elem.checked && elem.value || ""; const empty = elem.getAttribute("data-empty") || "この項目は入力必須です"; const invalid = elem.getAttribute("data-invalid") || invalidMessages[type] || "入力内容が正しくありません"; const parent = elem.parentNode; // hidden フィールドは無視する if("hidden" === type){ continue; } // バリデーション検証 if(elem.validity.valid || _isNotCheckEmpty && required && !value){ elem.setErrorMessage(); } else{ const message = (required && value || !required) && invalid || required && !value && empty || "値が正しくありません"; // エラー表示 elem.setErrorMessage(message); // エラー要素を配列に追加 // オブジェクトではなく、elem だけ追加しても良い invalidElements.push({ element: elem, require: required, type : type, message: message, }); } } return invalidElements; } // 入力要素の兄弟要素(<small>か疑似要素)にエラーメッセージを追加する Element.prototype.setErrorMessage = function(_message){ if(!/^(input|textarea|select)$/i.test(this.tagName)){ return false; } const elem = this; const parent = this.parentNode; // エラーメッセージ用の<small>要素 // 疑似要素でエラーを表示する場合は不要 const invalidMessages = parent.querySelectorAll("small.message-invalid"); invalidMessages.forEach( $smi => $smi.remove() ); if(!_message){ parent.removeAttribute("data-message-invalid"); elem.classList.remove("invalid"); return false; } // エラーメッセージ用の<small>要素のposition基準とするため、staticの場合はrelativeに変更 if(window.getComputedStyle(parent).position === "static"){ parent.style.position = "relative"; } parent.setAttribute("data-message-invalid", _message); elem.classList.add("invalid"); // 以下、<small>要素でエラーメッセージを表示するため const parentRect = parent.getBoundingClientRect(); const elementRect = elem.getBoundingClientRect(); const isHidden = !elementRect.left && !elementRect.right; const rightPosition = isHidden ? 0 : parentRect.right - elementRect.right; const topPosition = isHidden ? parentRect.bottom - parentRect.top + 8 : elementRect.bottom - parentRect.top + 8; // <small>要素を入力要素の右端に揃える // 見えない要素の場合は親要素の中央に表示 const invalidMessage = parent.appendChild(document.createElement("small")); invalidMessage.className = "message-invalid"; invalidMessage.innerText = _message; invalidMessage.style.position = "absolute"; invalidMessage.style.left = "0px"; invalidMessage.style.right = rightPosition + "px"; invalidMessage.style.top = topPosition + "px"; invalidMessage.style.textAlign = "right"; if(isHidden){ invalidMessage.style.textAlign = "center"; } return true; } window.addEventListener("DOMContentLoaded", function(){ // フォーム (取得方法はなんでも良いです) const form = document.getElementById("test_user_form"); // 送信ボタン const submitButton = document.getElementById("user_submit_button"); // 送信ボタン押下前 let isNotCheckEmpty = true; // イベント登録 submitButton.onclick = function(){ // 空の必須入力要素をチェックする isNotCheckEmpty = false; // バリデーションを通過しない入力要素を全て取得する const invalidElements = form.getInvalidElements(isNotCheckEmpty); // コンソールに表示 console.log(invalidElements); // 問題なければ送信処理 document.getElementById("validation_status").innerText = invalidElements.length > 0 ? "入力内容に問題があります。" : "すべて通過しました!"; // HTML5 のデフォルトバリデーション動作を停止させる return false; }; // イベント登録 for(const elem of form.elements){ elem.addEventListener("change", function(){ form.getInvalidElements(isNotCheckEmpty) }, false); elem.addEventListener("blur", function(){ form.getInvalidElements(isNotCheckEmpty) }, false); } }, false);

簡単な解説

まずは、バリデーション処理について仕様などを MDN 等で確認して下さい。
クライアント側のフォームデータ検証 - MDN

現在の Web 技術では、上記のような仕様でブラウザによって自動的に入力要素の値が検証されるようになっています。これはとても便利です。
ユーザビリティを向上させるために Javascript で処理を行う場合には、これらの機能を上手に使うことでとてもスッキリした実装を行うことが可能です。
(かつては入力欄ごとに条件に沿うような正規表現で値を検証する必要がありました)

入力要素の validity.valid プロパティでバリデーション結果を取得する

入力要素には validity.valid プロパティが設定されており、ここにバリデーションの結果が true / false で取得できます。
このプロパティをフォームのすべての要素 (HTMLFormElement.elements) に対して検証すればバリデーションを通らない入力要素が分かります。

if(elem.validity.valid){ // バリデーション通過時の処理 } else{ // バリデーションエラー時の処理 }

本コードのコアはこれが全てです。とても簡単ですね。
詳細は、以下のページをご確認下さい。他にも取得できるプロパティがたくさんあります。細かな処理が必要な際は必要なプロパティを使って下さい。
ValidityState - MDN

入力必須ではない要素は、空白であればバリデーションを通過します。これも自動なのでとても助かります。
もちろん入力必須の要素では空白はエラーになります。

検証結果からエラーメッセージを選択して表示

入力値の検証は全てブラウザに任せることができたので、あとはエラーメッセージを変えるだけです。
値が空の場合と正しくない場合でエラー内容を変えたいことも多いので条件分岐させます。

const message = (required && value || !required) && invalid || required && !value && empty || "値が正しくありません";

入力必須 (required = true) で値が入力されている時は、値が不正であると判断できるので data-invalid 属性値を返します。
任意入力 (required = false) でバリデーションが通らないということは、値が入力されていて不正であると判断できるのでこちらも data-invalid 属性値を返せばよいです。

残りのエラーは必須項目が空の場合です。required && !value && empty の部分ですが、required && !value && はなくてもいいと思います。
invalid の値が空の時に empty の値が表示されてしまうのを避けるためです。が、invalid の値が空のことはないはずなのでいらないかもしれません。念のため入れているだけです。

エラー表示の方法はいろいろとあると思いますが、ここでは <input> 要素に疑似要素が使えないために親要素にエラー用の <small> 要素を追加しています。
また、親要素の data-message-invalid 属性にエラーメッセージを入れるようにしているので、疑似要素で表示することも可能です。

補足; type="email" の場合には組込処理では正しくないメールアドレスだった時に細かなエラーが出たするので、組込のエラーメッセージを使いたい場合には以下のように変更して下さい。

const invalid = elem.getAttribute("data-invalid") || type === "email" && elem.validationMessage || invalidMessages[type] || "入力内容が正しくありません";

validationMessage という読取専用のプロパティで組込のエラーメッセージが取得できます。
例) sample: メール アドレスに「@」を挿入してください。「sample」内に「@」がありません。
例) sample@: 「sample@」は完全なメールアドレスではありません。「@」に続く文字列を入力してください。
例) sample@web.analogstd.com.: 「web.analogstd.com.」内の「.」の位置が間違っています。
例) sample@web.analogstd,com: 「@」に続く文字列に記号「,」を使用しないでください。

ただ、他の文言と統一感がなくなったりエラー内容が長い場合もあるので、使用する際は適宜調節して下さい。
また pattern 属性で正規表現も組み合わせている場合、正規表現の方だけに引っかかる時は「指定されている形式で入力してください。」などと出てきます。

バリデーションを通らなかった入力要素を返す

最後にバリデーションを通過できなかった入力要素を配列に追加していって最後にまとめて返すようにしています。
こうすることで最初のエラーがあった部分までページ内スクロールを実行することができます。
(私はページ内スクロールの設定を良く忘れますが)

サーバ側でのデータ検証は必須

Javascript によるバリデーションを通しているとはいえ、これは開発者ツールなどを使えばいくらでもすり抜けられますし、このフォームを利用しなくてもサーバにデータを送信することも可能です。
その為、サーバサイドの処理ですべてのデータを検証して無害化する必要があります。これは忘れないようにして下さい。

まとめ

以上、フォームのバリデーションを扱うコツをまとめました。
ユーザが実際にどんなことで悩むか、ストレスが溜まるのかを考えながら実装できるといいですね。

郵便番号から住所を検索したり、電話番号の区切りを自動設定したりなども併せて検討するとより良いフォームになりそうです。

こういった多機能なフォームを開発したり実装したいけどうまくいかない……という場合にはぜひご相談下さい!
もっと複雑な機能を実装することもできますので、お気軽にご相談いただければと思います。