JavaScript フォームの検証・入力チェック(制約検証 API)
HTML5 のフォームの検証機能を使えば JavaScript を使わずに簡単に検証機能(入力チェック)を実装できます。但し、表示方法やエラーメッセージをカスタマイズするには制約検証 API(Constraint Validation API)を使うか、独自に JavaScript で検証機能を実装する必要があります。
以下は HTML5 のフォーム検証機能とその基本的な使い方、制約検証 API を使った JavaScript での検証方法、独自のエラーメッセージの表示方法や自動検証を無効にして任意の位置にカスタマイズしたエラーメッセージを表示する方法、制約検証 API を使わずに独自の JavaScript で検証する方法や制約検証との併用の方法などについてサンプルを交えて解説しています。
[更新]エラーメッセージを表示する関数や「制約検証 API を使わない方法」でのチェックボックス及びラジオボタンの検証を書き換えました。(2021年11月8日)
送信時にのみ入力チェックをするシンプルなフォームの検証は「JavaScript を使ったフォームの検証」を御覧ください。
作成日:2021年8月22日
参考サイト:
関連ページ:
- JavaScript を使ったフォームの検証(入力チェック)
- JavaScript フォームとフォームコントロールの使い方
- jQuery フォームの操作
- HTML5 のフォームの検証機能
- HTML フォームの設置
- PHP フォーム / アンケート
- コンタクトフォームの作り方
- CSS 疑似クラス :has() の使い方
フォームの検証
フォームに入力されたデータをサーバーへ送信する前(サーバー側での検証の前)にクライアント側で検証することにより、フォームのユーザビリティを高めることができます。
クライアント側での検証はサーバー側で行う検証と一貫性のある方法で行います。基本的にはクライアント側の検証をパスすればサーバー側の検証をパスするようにします。また、クライアント側の検証はセキュリティ対策にはならないため、クライアント側の検証を実施してもサーバー側の検証は必要です。
クライアント側での検証には以下のような方法があります。
- HTML5 のフォームの検証機能を使う
- Constraint Validation API(制約検証 API)を使う
- 独自の JavaScript で検証する
HTML5 のフォームの検証機能
HTML5 のフォームの検証機能を利用すると、JavaScript を使わずにフォームのデータを検証(入力をチェック)することができます(ブラウザ対応状況:Can I Use Form-Validation)。
フォームコントロール要素に required や minlength などの検証属性を指定したり、内容に合わせて input 要素に email などの適切な type 属性を指定することで入力された値を検証することができます。
概要
検証属性を設定したコントロール要素や適切な type 属性を指定した input 要素では、自動的に入力値が検証され、値がすべて制約を満たしていれば妥当とみなされ、そうでなければ不正とみなされます。
- 不正とみなされた場合
- CSS の :invalid 疑似クラスが適用されます。これにより、不正な要素に特定のスタイルを適用することができます。フォームを送信しようとするとブラウザーはエラーメッセージを表示し、フォームは送信されません。
- 妥当とみなされた場合
- CSS の :valid 疑似クラスが適用されます。これにより、妥当な要素に特定のスタイルを適用することができます。フォームを送信しようとすると、フォームは送信されます。
input 要素の type 属性
input 要素の type 属性に以下の値を指定することで値が妥当かどうかをチェックする制約が自動的に作成され検証が適用されます。
type 属性 | 説明 |
---|---|
type 属性の値に"email"を指定すると、input 要素はメールアドレスの入力欄になり、入力された値は送信前に空文字列またはひとつの妥当なメールアドレスが含まれているかの検証を受けます。multiple 属性が設定されていたら、カンマ区切りのリストで複数の値(メールアドレス)を設定することができます。@ 以降の部分ではドット(.)が含まれているかどうかの検証はしません。必要に応じてpattern や minlength などの検証属性を追加します。 | |
number | type 属性の値に"number"を指定すると、input 要素は数値の入力欄となり、対応しているブラウザーでは数値用のコントロールが表示され、送信前に数値かどうかの検証を受けます。min 属性、max 属性で最小値と最大値を指定でき、step 属性を使用すると数値の刻みを指定できます。また、step 属性の値を小数点以下に指定することで、小数点以下の値も扱えるようになります。Chrome などでは数値以外は入力できないようになります。 |
url | type 属性の値に"url"を指定すると、input 要素は URL の入力欄になり、入力された値は送信前に空文字列またはひとつの妥当な絶対 URL が含まれているかの検証を受けます。改行および先頭または末尾のホワイトスペースは、自動的に入力値から取り除かれます。multiple 属性が設定されていたら、カンマ区切りのリストで複数の値(url)を設定することができます。必要に応じて pattern や maxlength などの検証属性を使用して、コントロールに入力する値を制限できます。 |
上記 type 属性を指定した input 要素に入力された値が制約を満たさない場合、Type mismatch 制約違反が発生します。
検証属性
入力される値を検証するために以下の検証属性を input 要素などに指定することができます。指定する属性によりサポートされている input 要素の type 属性や要素の種類が異なります。
属性 | 説明 |
---|---|
required | この属性を指定した要素は入力が必須になります。空欄のまま送信しようとすると、対応しているブラウザーではエラーメッセージが表示され、フォームの送信は行われません。 |
使用できる type 属性と要素:text, search, url, tel, email, password, date, datetime, datetime-local, month, week, time, number, checkbox, radio, file 及び <select> と <textarea> | |
pattern | 値をチェックするための正規表現を指定します。パターン文字列の前後のスラッシュは不要です(指定してはいけません)。この属性で指定する正規表現は全体一致でチェックするため、先頭と末尾に^と$をつける必要はありません。部分一致で指定する場合は、先頭と末尾に .*? と .* を付ける必要があります。また、パターンを説明する title 属性を含めることができます。pattern attribute |
使用できる type 属性:text, search, url, tel, email, password | |
minlength maxlength |
minlength 属性はユーザーが入力できる最小文字数を、maxlength 属性は最大文字数を指定します(文字数は UTF-16 の符号単位の数なので、絵文字などでは期待通りに機能しません)。textarea 要素にも指定可能です。入力文字数の範囲を限定したい場合は minlength 属性と maxlength 属性を組み合わせます。maxlength 属性を設定すると、ほとんどのブラウザーは設定した文字数以上は入力できないようになっています。maxlength を使う代わりに、pattern を使って最大文字数の制限を設定することもできます(textarea 要素では pattern は使えません)。 |
使用できる type 属性と要素:text, search, url, tel, email, password 及び <textarea> | |
min max |
min 属性は入力可能な値(数値または日時) の最小値を、max 属性は最大値を指定します。数値の場合、指定できる値は浮動小数点数(整数も含む)で、日時の場合に指定できる値は日付け文字列(2017-06-03T11:07:24 のような形式)になります。入力範囲を限定したい場合は max 属性と min 属性を組み合わせます。 |
使用できる type 属性:range, number, date, month, week, datetime, datetime-local, time |
詳細は MDN の制約検証ガイド(検証関連属性)に掲載されています。
以下は HTML5 のフォームの検証機能を使って送信時に入力されている値を検証する例です。
設定した制約を満たしていない要素があれば、送信時にエラーが表示され、フォームは送信されません。全ての制約が満たされていればフォームが送信されます。
以下の例では、全てのコントロールに required 属性を設定しています。本来なら各項目に「必須」などと表示するべきですが省略しています。また、コントロールによっては以下のような制限を設定しています。
- 名前:maxlength 属性を指定して最大文字数を制限
- 電話番号:pattern 属性を指定して0から始まる数値またはハイフンを使った形式を許容
- メールアドレス:type 属性に email を指定して妥当なメールアドレスのみを許容
- お問い合わせ内容:maxlength 属性を指定して最大文字数を制限
電話番号の input 要素は type="tel" を指定していますが特に電話番号の形式が検証されるわけではないので、pattern 属性を指定して妥当な電話番号の形式で入力されているかを検証しています。
ラジオボタンの選択を必須にするには、同じ name 属性の input 要素のどれか1つに required 属性を設定すれば良いようです(全てに指定しても同じ)。
select 要素の場合、選択状態の option 要素の値を空にしています。これにより他の項目を選択された場合に required が満たされます。
チェックボックスでは項目が1つだけなので以下のように required 属性をその項目に設定して期待通りに機能しますが、複数の同じ name 属性の項目がある場合はラジオボタンのようにはならないようです。
<form name="myForm" method="post"> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" maxlength="15" required> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" required> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" required> </div> <div> <p>色を選択してください</p> <!-- 必須にするには同じ name 属性の input 要素のいずれかを required に --> <input type="radio" name="color" value="blue" id="blue" required> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> </div> <div> <select name="season" required> <!-- 初期状態で選択されている option の value を ""(空)に --> <option value="">季節を選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" required maxlength="200" rows="3" cols="50"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
何も入力していない状態で送信ボタンをクリックすると最初の required を設定した名前の入力欄に「このフィールドを入力してください」のようなブラウザ既定のメッセージが表示されます。
値を入力して全ての検証項目が制約を満たしていれば送信できますが、検証を満たしていない項目があればその項目にエラーメッセージが表示されて送信されません。
このページのフォームのサンプル
このページのフォームのサンプルでは preventDefault() を使って実際にはフォームを送信していませんが、送信ボタンをクリックしたり入力欄にカーソルを置いて return キーを押すと検証は(送信の前に)実施されます(後半のサンプルでは実際に送信するフォームを iframe を使って表示しています)。
以下は全ての制約の検証をパスすればフォームを送信したように見せかけるための記述例です。
実際の送信では action 属性に指定したページが読み込まれてページの先頭に移動し値や選択状態はクリアされますが、preventDefault() で送信を止めると入力された値や状態はそのままでクリアされません。
そのため、以下では入力された値をクリアしたり、ラジオボタンのなどの選択状態を解除しています。
/*フォームを送信したように見せかけるための記述(実際に送信する通常のフォームでは不要)*/ //document.forms を使って name="myForm" の form 要素を取得 const myForm = document.forms.myForm; //または document.myForm //フォームの イベントにリスナーを登録 myForm.addEventListener('submit', (e) => { //サンプル用なので実際には送信しないためデフォルトの動作(送信)を中止 e.preventDefault(); //入力欄をクリア myForm.name.value = ''; myForm.tel.value = ''; myForm.mail.value = ''; myForm.inquiry.value = ''; //ラジオボタンの集まり const colors = myForm.elements['color']; //全ての選択状態を解除 for(let i=0; i<colors.length; i++) { colors[i].checked = false; } //select 要素の options 要素の集まり const seasons = myForm.season.options; //全ての選択状態を解除 for(let i=0; i<seasons.length; i++) { seasons[i].selected = false; } //チェックボックスの選択状態を解除 myForm.agreement.checked = false; });
関連ページ:JavaScript フォームとフォームコントロールの使い方
複数項目のチェックボックス
ラジオボタンの選択を必須にするには、同じ name 属性の input 要素のどれか1つ(または全て)に required 属性を指定しますが、チェックボックスの場合、同じ name 属性の input 要素のどれか1つに required 属性を指定すると、その項目のみが必須になります。
<form> <div> <input type="checkbox" name="contact" id="byEmail" value="Email" required> <label for="byEmail"> メール</label> <input type="checkbox" name="contact" id="byTel" value="Telephone"> <label for="byTel"> 電話</label> <input type="checkbox" name="contact" id="byMail" value="Mail"> <label for="byMail"> 郵便 </label> </div> <button>送信</button> </form>
全てのチェックボックスの項目に required を設定すると、全ての項目を選択しなければエラーになり送信できません。
<form> <div> <input type="checkbox" name="contact" id="byEmail" value="Email" required> <label for="byEmail"> メール</label> <input type="checkbox" name="contact" id="byTel" value="Telephone" required> <label for="byTel"> 電話</label> <input type="checkbox" name="contact" id="byMail" value="Mail" required> <label for="byMail"> 郵便 </label> </div> <button name="send">送信</button> </form>
複数のチェックボックス項目のいずれかを選択することを必須にするという制約を設定するには JavaScript を使う必要があるようです。
検証用 CSS 疑似クラス
HTML5 のフォームの検証では、入力された値が設定されている検証属性や type 属性の要件を満たしていない場合、その要素には CSS の :invalid 疑似クラスが適用され、ユーザーがデータを送信しようとするとブラウザーはエラーメッセージを表示してフォームを送信しません。
要素の値が検証属性や type 属性の要件を満たしていれば、妥当とみなされ、CSS の :valid 疑似クラスが適用されます。その他の要素も要件を満たしていればフォームは送信可能になります。
:valid 及び :invalid 疑似クラスを使うことでそれぞれの状態の要素にスタイルを適用することができます。
検証関連の疑似クラスには以下のようなものがあります。
擬似クラス | 説明 |
---|---|
:valid | 入力値がすべての検証要件を満たす場合に適用される擬似クラス |
:invalid | 入力値が検証要件を満たさない(検証に失敗した)場合に適用される擬似クラス |
:required | required 属性が設定された入力要素に適用される擬似クラス |
:optional | required 属性が設定されていない入力要素に適用される擬似クラス |
:in-range | 値が範囲内にある数値入力要素に適用される擬似クラス |
:out-of-range | 値が範囲外にある数値入力要素に適用される擬似クラス |
擬似クラス | 説明 |
---|---|
:user-valid | 入力値がすべての検証要件を満たす場合に適用される擬似クラス。※ :valid と異なり、ユーザーの操作が行われたあとに検証されます。 |
:user-invalid | 入力値が検証要件を満たさない場合に適用される擬似クラス。※ :invalid と異なり、ユーザーの操作が行われたあとに検証されます。 |
以下は :invalid 擬似クラスを使って検証を満たしていない要素にスタイルを設定する例です。HTML は前述の例と同じです。
検証を満たしていない input、textarea、select 要素に背景色を設定しています。チェックボックスとラジオボタンは背景色が適用されないので、box-shadow を使っていますが、Safari では box-shadow も反映されないのでラベルの文字列を赤くしています(あまり見栄えの良いスタイルではありませんが)。
検証は即座に実行されるため、ページが表示された直後でまだユーザーがフィールドに入力する前でも、検証を満たしていない要素では CSS の :invalid 疑似クラスが適用されます。
input { border: 1px solid #333; margin: 0; font-size: 90%; box-sizing: border-box; } /* 検証の要件を満たさない(:invalid を適用された)要素のスタイル */ input:invalid, textarea:invalid, select:invalid { border-color: #900; background-color: #FDD; } /* 検証の要件を満たさないラジオボタンとチェックボックスのスタイル(背景色が適用されないので) */ input[type="radio"]:invalid, input[type="checkbox"]:invalid { box-shadow: 0 0 2px 1px red; } /* Safari では上記の が反映されないので文字色を変更 */ input[type="radio"]:invalid + label, input[type="checkbox"]:invalid + label { color: red; }
:user-valid と :user-invalid
以下は新しく追加された :user-valid と :user-invalid 擬似クラスを使った例です。
:valid と :invalid は入力要素の初期状態に対しても検証を行いますが、:user-valid と :user-invalid はユーザーの操作が行われたあとに検証を行います。
そのため、以下のように初期状態ではエラー表示にならないですみます。
input { border: 1px solid #333; margin: 0; font-size: 90%; box-sizing: border-box; } /* :user-invalid を使用 */ input:user-invalid, textarea:user-invalid, select:user-invalid { border-color: #900; background-color: #FDD; } /* :user-invalid を使用 */ input[type="radio"]:user-invalid, input[type="checkbox"]:user-invalid { box-shadow: 0 0 2px 1px red; } /* :user-invalid を使用 */ input[type="radio"]:user-invalid + label, input[type="checkbox"]:user-invalid + label { color: red; }
以下のサンプルでは、送信ボタンをクリックすると実際には送信せずに値をクリアしているだけなので、エラー表示が残ってしまいますが、実際に送信されれば初期状態に戻ります。
:user-invalid と :user-invalid は比較的新しく追加されたので、古いブラウザではサポートされていません。サポート状況は以下で確認できます。
https://caniuse.com/mdn-css_selectors_user-invalid
関連ページ
:has() 擬似クラス/フォームの検証結果によりスタイルを設定
:invalid 擬似クラスを使ってエラーメッセージを表示
以下は :invalid 擬似クラスを使って入力時(その要素にフォーカスした際)に検証を満たしていない場合はエラーメッセージを表示する例です。
<form> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{0,10}" title="10文字以内"> <p><span class="error">10文字以内で入力ください</span></p> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" title="半角数字で入力"> <p><span class="error">0から始まる半角数字で入力ください(ハイフンを含めることができます)</span></p> </div> <div> <label for="mail">メールアドレス(必須): </label> <input type="email" name="mail" id="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" title="@ やドメインを含む正しい形式で入力" required> <p><span class="error">メールアドレスは必須です。@ やドメインを含む正しい形式で入力ください。</span></p> </div> <div> <button>送信</button> </div> </form>
上記のフォームの HTML では次のような制約検証を設定しています。
- 名前:最大10文字(pattern 属性)
- 電話番号:0から始まる数値のみまたはハイフンを使った形式(pattern 属性)
- メールアドレス:必須(required 属性)と正しいメール形式(type="email" 及び pattern 属性)
名前の最大文字数の制限は maxlength ではなく pattern 属性 を使っています(maxlength を設定すると指定した文字数以上は入力できません)。
メールアドレスでは type="email" の指定に加え、pattern 属性を設定してメールアドレスの @ 以降の部分にドット(.)が含まれているかを検証するようにしています。
エラーメッセージの表示は、CSS の :invalid と :focus 擬似クラスを利用しています。
/* 必須で検証要件を満たさない input 要素の背景色をピンクに */ input:required:user-invalid { background-color:#FAD9D9; /* 薄ピンク色 */ } /* input 要素に続く p 要素内の .error の要素(エラーメッセージ)は初期状態で非表示 */ input + p .error { display: none; } /* フォーカス状態の input 要素で検証要件を満たさない場合はエラーメッセージを表示 */ input:focus:invalid + p .error { display: inline; color: red; }
input 要素に続く p 要素の子要素である error クラスを指定した span 要素(input + p .error)には予めエラーメッセージを記述して非表示にしてあります(+ は隣接兄弟を表すセレクタ)。
フォーカスした際に検証要件を満たさない場合(:focus:invalid)は display: inline と color: red で赤色で表示します。
この場合、何も入力せずに送信すると、メールアドレスは必須なのでエラーメッセージが表示されてフォームは送信されません。また、電話番号とメールアドレスの入力では入力時にその値が検証されて、要件を満たしていない場合は赤字のメッセージが表示されます。
制限の要件を満たしていない状態で送信しようとすると、ブラウザにより検証が実施され、ブラウザの既定のメッセージと title 属性に指定した文字が表示されてフォームは送信されません。
HTML5 のフォームの検証機能を使えば、上記のように CSS だけで比較的簡単に検証機能を実装できますが、表示されるエラーメッセージや表示方法はブラウザによって異なります(ブラウザに依存します)。カスタマイズするには次項の「制約検証 API」を使います。
Constraint Validation API(制約検証)
Constraint Validation API(制約検証)を使うと、ユーザーがフォームのコントロール要素に入力した値を、サーバーに送信する前に JavaScript を使って検証することができます。
制約検証を使えば、ブラウザのフォーム検証機能の既定のエラーメッセージをカスタマイズしたり、より複雑な制約などを設定することができます。
最近のブラウザは制約検証に対応しています(caniuse.com/constraint-validation)。
参考ページ:
- Constraint Validation API(HTML Living Standard 日本語訳)
- The constraint validation API(HTML 5.1 2nd Edition)
- 制約検証 API (MDN)
- 制約検証ガイド(MDN)
invalid イベント
invalid イベントは フォームコントロール要素が制約を満たさない場合に発生します。
具体的には required や pattern などの検証属性を設定した要素や type 属性が email や url などの検証対象の要素が、制約を満たしていない場合に invalid イベントが発行されます。
例えば、フォームが送信される際に検証対象の要素はその値を検証されるので、それぞれの検証の要件を満たさない状態にあるコントロール要素で invalid イベントが発生します。
また、checkValidity() や reportValidity() メソッドを実行する際にも検証対象の要素はその値を検証されるので、検証の要件を満たしていない要素で invalid イベントが発生します。
以下は checkValidity() と reportValidity()、及びフォームの送信を使って値を検証する例です。
テキストフィールドには minlength="5" と required 属性が設定されているので、入力された文字が5文字未満の場合や何も入力されていない場合は検証の要件を満たさないので「送信」ボタンをクリックすると検証結果のエラーメッセージが表示されフォームは送信されません。
<form id="myForm"> <input type="text" id="myText" minlength="5" required> <button id="check" type="button">checkValidity</button> <button id="report" type="button">reportValidity</button> <button>送信</button> <button type="reset">クリア</button> </form>
checkValidity ボタンと reportValidity ボタンにはクリックイベントを設定して、クリックするとそれぞれ checkValidity() と reportValidity() を実行して検証を行います。
checkValidity()、reportValidity() 及びフォームの送信により実行される制約検証では、要素の値が検証の要件を満たさない場合は invalid イベントが発生します。invalid イベントのリスナー関数では、発生した要素の validity のプロパティの該当するプロパティを調べ、それらが true の場合は該当するプロパティ名と validationMessage をアラート表示します。
送信及び reportValidity() の場合はアラートの後に検証結果のエラーメッセージも表示されます。
const myForm = document.getElementById('myForm'); const myText = document.getElementById('myText'); const checkButton = document.getElementById('check'); const reportButton = document.getElementById('report'); //checkValidity ボタンにクリックイベントを設定 checkButton.addEventListener('click', () => { //checkValidity() を実行 myText.checkValidity(); }); //reportValidity ボタンにクリックイベントを設定 reportButton.addEventListener('click', () => { //reportValidity() を実行 myText.reportValidity(); }); //テキストフィールドに invalid イベントのリスナーを設定 myText.addEventListener('invalid', (e) => { //発生した要素の validity のプロパティを調べる if(e.target.validity.valueMissing) { //valueMissing が true であれば「valueMissing:」と validationMessage をアラート表示 alert('valueMissing: \n' + e.target.validationMessage); }else if(e.target.validity.tooShort){ //tooShort が true であれば「tooShort:」と validationMessage をアラート表示 alert('tooShort: \n' + e.target.validationMessage); } });
プロパティ
制約検証 API は各フォーム要素で使用できるプロパティやメソッドで構成されています。
以下は制約検証 API のプロパティやメソッドを持つフォームコントロール要素(オブジェクト)です。
要素 | オブジェクト |
---|---|
button | HTMLButtonElement |
fieldset | HTMLFieldSetElement |
input | HTMLInputElement |
output | HTMLOutputElement |
select | HTMLSelectElement |
textarea | HTMLTextAreaElement |
以下は上記の要素で利用できる制約検証 API のプロパティで、いずれも読取専用です。
プロパティ | 説明 | 型 |
---|---|---|
validity | 要素の検証状態を表す ValidityState オブジェクトを返します。このオブジェクトのプロパティを調べることで各検証の状態を真偽値で取得できます。 | オブジェクト |
validationMessage | そのコントロール要素が制約検証を満たさなかった場合、その内容を記述したメッセージを返します。コントロール要素が制約の検証の対象ではない場合 (willValidate が false) や要素の値が制約を満たしている(合格している場合)は空文字列を返します。この値は setCustomValidity() メソッドでカスタマイズできます。 | 文字列 |
willValidate | その要素が制約検証の候補であるか(検証を設定できるか)どうかの真偽値を返します。要素が検証可能な場合は true を返し、そうでない場合は false を返します。例えば、type 属性が hidden や reset、 button の場合や disabled 属性が設定されいている要素では false を返します(その要素に検証属性が設定されているかどうかを判定するものではありません)。 | 真偽値 |
メソッド
以下はフォームコントロール要素で利用できる制約検証 API のメソッドです。
メソッド | 説明 |
---|---|
checkValidity() | 要素の値が制約検証を満たしている場合に true を返します。制約検証を満たしていない場合は false を返し、その制約検証を満たしていない要素で invalid イベントを発生させます。 |
reportValidity() | checkValidity() メソッドを実行し、false が返された場合 (制約検証を満たしていない場合) は、フォームを送信する際と同様、入力が無効であることをユーザーに報告(エラーメッセージを表示)します。 |
setCustomValidity() | 要素に独自の検証メッセージ(カスタム検証メッセージ)設定します。設定するメッセージは引数に指定します。カスタム検証メッセージを設定すると、要素が制約検証を満たしていない場合に設定したメッセージが表示されます。このメッセージが設定されていると(空の文字列ではない場合)、その要素は独自の検証エラーがある状態になり、検証に不合格になります。独自の検証エラーを解除するには setCustomValidity() に空文字列を指定します。 |
form 要素
以下は form 要素(HTMLFormElement)で利用できる制約検証 API のメソッドとプロパティです。
メソッド | 説明 |
---|---|
checkValidity() | このフォーム要素の子コントロールが制約検証の対象となっていて、それらの制約を満たしている場合は true を返します。制約を満たさないコントロールがある場合は false を返します。制約を満たさないコントロールに対して、invalid イベントを発生させます。イベントがキャンセルされない場合、そのようなコントロールは無効とみなされます。 |
reportValidity() | このフォーム要素の子コントロールがその検証する制約を満たしている場合、true を返します。false が返された場合、無効な子要素それぞれにキャンセル可能な invalid イベントが発生し、検証の内容がユーザーに報告されます(エラーメッセージが表示されます)。 |
プロパティ | 説明 |
noValidate | フォームの novalidate 属性の値を反映し、フォームの検証を行わないかどうかを示す真偽値を返します。検証を行わない場合は true を返します。 |
ValidityState
ValidityState はフォームコントロール要素に設定された制約の検証状態を表すオブジェクト(インターフェイス)で、コントロール要素の読み取り専用のプロパティ validity で参照できます。
ValidityState のプロパティには以下のようなものがあり、いずれも真偽値(Boolean)を返します。
true は指定された検証が失敗したことを表しますが、valid プロパティだけは例外で、true は要素の値がすべての制約に適合している(valid:有効である)ことを表します。
プロパティ | 説明 |
---|---|
badInput | 入力値をブラウザーが処理できない(変換できない)場合 true。例:数値の入力欄に文字列がある場合など |
customError | setCustomValidity() によってその要素のカスタム検証メッセージが設定されていれば true、設定されていなければ false。 |
patternMismatch | 値が pattern 属性の指定(正規表現)と一致しない場合は true、一致する場合は false。true の場合、その要素は :invalid 擬似クラスが適用されます。 |
rangeOverflow | 値が max 属性で指定された最大値を超えている場合は true、その最大値以下である場合は false。true の場合、その要素は :invalid 及び :out-of-range の各擬似クラスが適用されます。 |
rangeUnderflow | 値が min 属性で指定された最小値未満である場合は true、その最小値以上であるである場合は false。true の場合、その要素は :invalid 及び :out-of-range の各擬似クラスが適用されます。 |
stepMismatch | 値が step 属性で指定された規則に合致しない場合は true、刻みの規則に合致している場合は false。 true の場合、その要素は :invalid 及び :out-of-range の各擬似クラスが適用されます。 |
tooLong | 値が input または textarea 要素の maxlength 属性で指定された長さを超えている場合は true、超えていない場合は false。殆どのブラウザでは要素の値の長さが maxlength を超えないようになっているため、このプロパティが true になることはありません。true の場合、その要素は :invalid 及び :out-of-range の各擬似クラスが適用されます。 |
tooShort | 値が input 要素または textarea 要素の minlength 属性で指定された長さに満たない場合は true、満たす場合は false。 true の場合、その要素は :invalid 及び :out-of-range の各擬似クラスが適用されます。 |
typeMismatch | 値が email や url などの type 属性で指定されたタイプの構文に合っていない場合は true、構文に合致している場合は false。 true の場合、その要素は :invalid 擬似クラスが適用されます。 |
valid | その要素がすべての制約検証に適合して合格した場合は true、いずれかの制約に適合しない場合は false。 true の場合、その要素は :valid 擬似クラスが適用され、false の場合は :invalid 擬似クラスが適用されます。 |
valueMissing | その要素に required 属性が指定されているが値がない場合は true、値がある場合は false。true の場合、その要素は :invalid 擬似クラスが適用されます。 |
独自のエラーメッセージを表示
以下は input 要素の setCustomValidity() メソッドを使って独自の検証メッセージ(カスタム検証メッセージ)を設定し、ブラウザの既定のエラーメッセージの代わりに表示する例です。
setCustomValidity() メソッドを使ってその要素にカスタム検証メッセージ設定すると、その要素の検証状態を表す validity プロパティ(ValidityState オブジェクト)の customError プロパティが true になり、その要素はエラーの状態(カスタムエラーが設定された状態)になります。
カスタムエラーが設定された状態を解除するには、setCustomValidity() に空の文字列を指定します。
以下では input イベントを使って、入力される値が変更されるたびに input 要素の validity プロパティ(ValidityState オブジェクト)の typeMismatch プロパティの値を調べています。
typeMismatch はその要素の type 属性に基づく検証結果を真偽値で表し、true の場合は制約に適合しない(形式が正しくない)ことを意味し、false の場合は制約に適合することを意味します。
typeMismatch の値が true の場合は setCustomValidity() を使ってカスタム検証メッセージを設定し、カスタムエラーが設定された状態(検証をパスしていない状態)にしています。
typeMismatch の値が false の場合は、setCustomValidity() に空の文字列を指定してカスタム検証メッセージをクリアし、カスタム検証がエラーの状態を解除しています(これを行わないとエラー状態が続いていることになり、フォームを送信できません)。
<form> <label for="mail">Email アドレス</label> <input type="email" id="mail" name="mail"> <button id="send">送信</button> </form> <script> //id="mail" の input 要素を取得 const email = document.getElementById("mail"); //上記で取得した input 要素に input イベントのリスナーを登録 email.addEventListener('input', () => { //input 要素の validity プロパティの typeMismatch が true の場合 if (email.validity.typeMismatch) { //独自の検証メッセージ設定し、カスタム検証をエラー状態に email.setCustomValidity('ちゃんとしたメアドを入力してね'); //以下のコメントを外すと検証を満たさない場合、入力するたびにエラーが表示される //email.reportValidity(); } else { //空文字を設定して input 要素のカスタム検証のエラー状態を解除 email.setCustomValidity(''); } }); </script>
以下は上記の例を invalid イベントを使って書き換えたものです。
input イベントのリスナー関数では値が変更されるたびに checkValidity() を実行して input 要素の有効状態をチェックしています。checkValidity() はその要素が検証の要件を満たさない場合は false を返し、その要素で invalid イベントが発生します。
そして、invalid イベントのリスナーで invalid イヴェントを検知したら setCustomValidity() で独自の検証メッセージ設定し、カスタム検証をエラーの状態にしています。
検証の要件を満たす(入力値が有効な)場合は、setCustomValidity() でカスタム検証のエラー状態を解除する必要があるので、input イベントではまず setCustomValidity() に空文字を設定しています。
const email = document.getElementById("mail"); //input 要素に input イベントのリスナーを登録 email.addEventListener('input', () => { //input 要素のカスタム検証のエラーの状態を解除 email.setCustomValidity(''); //input 要素の有効状態をチェック(値が無効な場合、invalid イベントが発生) email.checkValidity(); }); //input 要素に invalid イベントのリスナーを登録 email.addEventListener('invalid', () => { //独自の検証メッセージ設定し、カスタム検証をエラーの状態に email.setCustomValidity('ちゃんとしたメアドを入力してね'); });
上記で checkValidity() の代わりに reportValidity() を使うと、入力される値が変更されるたびにカスタムエラーメッセージが表示されてしまいます。
required 属性
以下は前述の例の type="email" の input 要素に required 属性を追加して、値が空の場合は「必須よ!」というカスタム検証メッセージを表示する例です。
input イベントは入力される値が変更されるたびに発生するイベントなので、値が空かどうかの判定は、送信ボタンがクリックされる際に発生する click イベントを使っています。
click イベントは送信ボタンがクリックされる際に発生する submit イベントの前に発生します。そのため、その時点でカスタム検証メッセージを設定すれば、エラーとなり送信はされません。
<form> <label for="mail">Email アドレス</label> <input type="email" id="mail" name="mail" required placeholder="必須"> <button id="send">送信</button> </form> <script> const email = document.getElementById("mail"); const btn = document.getElementById("send"); //input 要素に input イベントのリスナーを登録 email.addEventListener('input', () => { //input 要素の validity プロパティの typeMismatch が true の場合 if (email.validity.typeMismatch) { //独自の検証メッセージ設定 email.setCustomValidity('ちゃんとしたメアドを入力してね'); } else { email.setCustomValidity(''); } }); //ボタンにクリックイベントを設定 btn.addEventListener('click', () => { //input 要素の値が空であれば if(email.value === '') { //独自の検証メッセージ設定 email.setCustomValidity('必須よ!'); } }); </script>
以下は上記を invalid イベントを使って書き換えた例です。
invalid イベントが発生した際に、値が空の場合はカスタム検証メッセージを設定しています。
この例の場合、invalid が発生するのは値が空かメール形式が正しくない場合なので以下のようにしていますが、他にも検証があれば、メール形式のエラーかどうかは validity.typeMismatch で判定するなどします。
const email = document.getElementById("mail"); //input 要素に input イベントのリスナーを登録 email.addEventListener('input', () => { //input 要素のカスタム検証のエラーの状態を解除 email.setCustomValidity(''); //input 要素の有効状態をチェック(値が無効な場合、invalid イベントが発生) email.checkValidity(); }); //input 要素に invalid イベントのリスナーを登録 email.addEventListener('invalid', () => { //input 要素のプロパティを調べ、独自の検証メッセージ設定(カスタム検証をエラーの状態に) if(email.value === '') { //値が空の場合 email.setCustomValidity('必須よ!'); } else { //それ以外の場合(email.validity.typeMismatch で判定も可能) email.setCustomValidity('ちゃんとしたメアドを入力してね'); } });
data-* 属性で独自のエラーメッセージ
以下は必要に応じてコントロール要素に data-* 属性(カスタムデータ属性)を設定して、独自のエラーメッセージを表示する例です。
以下の data-* 属性をコントロール要素に指定すると、その値を独自のエラーメッセージとして表示します。指定しない場合は、デフォルトのエラーメッセージが表示されます。
検証属性 | 設定する data-* 属性 | 説明 | ValidityState |
---|---|---|---|
required | data-cem-required | 値が空の場合 | valueMissing |
type 属性 | data-cem-type | type 属性の構文に合っていない場合 | typeMismatch |
pattern | data-cem-pattern | pattern 属性の指定と一致しない場合 | patternMismatch |
minlength | data-cem-minlength | minlength 属性の長さに満たない場合 | tooShort |
maxlength | data-cem-maxlength | maxlength 属性の長さを超えている場 | tooLong |
min | data-cem-min | min 属性の値より小さい場合 | rangeUnderflow |
max | data-cem-max | max 属性の値を超えている場合 | rangeOverflow |
step | data-cem-step | step 属性の規則に合致しない場合 | stepMismatch |
入力値 | data-cem-badinput | 入力値をブラウザーが処理できない場合 | badInput |
以下の例では、data-* 属性を指定して独自のエラーメッセージを表示する要素には、cem(custom error message の略のつもり)というクラス(class="cem")を一緒に指定する必要があります。
最初の「名前」の入力では、required 属性と data-cem-required 属性が指定されているので、送信時に値が空の場合は data-cem-required 属性に指定されたエラーメッセージが表示されます。また、同時に minlength 属性と data-cem-minlength 属性も指定されているので、2文字未満の場合は data-cem-minlength 属性に指定されたエラーメッセージが表示されます。
<form name="myForm"> <div> <label for="name">名前: </label> <input class="cem" type="text" name="name" id="name" required data-cem-required="名前は必須です" minlength="2" data-cem-minlength="2文字以上でご入力ください" > </div> <div> <label for="tel">電話番号: </label> <input class="cem" type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" data-cem-pattern="0から始まる番号を半角数字のみまたはハイフンを付けて入力" required data-cem-required="電話番号は必須です"> </div> <div> <label for="mail">メールアドレス</label> <input class="cem" type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" data-cem-pattern="メールの形式が正しくないようです。@ と . 及びドメイン名が必要です。" required data-cem-required="メールアドレスは必須です" minlength="8"> </div> <div> <p>色を選択してください</p> <input class="cem" type="radio" name="color" value="blue" id="blue" required data-cem-required="色の選択は必須です"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> </div> <div> <select class="cem" name="season" id="season" required data-cem-required="季節の選択は必須です"> <option value="">季節を選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea class="cem" name="inquiry" id="inquiry" maxlength="100" minlength="10" required data-cem-required="お問い合わせ内容は必須です" rows="3" cols="50"></textarea> </div> <div> <input class="cem" type="checkbox" name="agreement" id="agreement" value="agree" required data-cem-required="送信するにはチェックを入れてください"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
独自のエラーメッセージを設定する関数 setCustomErrorMessage() では、最初に setCustomValidity() に空文字を設定してカスタム検証メッセージを初期化しておきます。
そして、その要素が全ての制約検証に適合しない場合は validity のプロパティ(ValidityState)を1つ1つチェックして、適合していないプロパティがあれば、その要素の data-cem-xxxx 属性を確認し、設定されていればその値を setCustomValidity() でカスタム検証メッセージとしています。
対象の要素には input イベントのリスナーとして setCustomErrorMessage() を登録して、入力が変更される都度にエラーメッセージの内容を更新します。
また、送信ボタンの click イベントのリスナーに setCustomErrorMessage() を登録して、送信の前にエラーメッセージの内容を更新します。
※ setCustomErrorMessage() はフォームの submit イベントではなく、submit イベントの前に発生するボタンの click イベントに登録する必要があります。
上記のサンプルでは、body の閉じタグの直前で script 要素に以下の JavaScript を記述しています。記述する位置や別ファイルとして読み込む場合は必要に応じて DOMContentLoaded イベントを使います。
//cem クラスを指定された要素を取得 const targetElems = document.querySelectorAll('.cem'); //独自のエラーメッセージを設定する関数 const setCustomErrorMessage = (elem) => { //setCustomValidity() に空文字を設定してエラー状態を解除 elem.setCustomValidity(''); //その要素が制約検証に適合しない場合は validity のプロパティ(ValidityState)をチェック if(!elem.validity.valid) { //required 属性が満たされていない(valueMissing が true)場合 if(elem.validity.valueMissing) { //data-cem-required 属性の値を取得 const dataError = elem.getAttribute('data-cem-required'); //取得した data-cem-required 属性の値があれば if(dataError) { //独自の検証メッセージ設定し、カスタム検証をエラー状態に elem.setCustomValidity(dataError); } } else if(elem.validity.typeMismatch) { //type 属性で指定されたタイプの構文に合っていない場合 const dataError = elem.getAttribute('data-cem-type'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.patternMismatch) { //pattern 属性の指定と一致しない場合 const dataError = elem.getAttribute('data-cem-pattern'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.tooShort) { //minlength 属性で指定された長さに満たない場合 const dataError = elem.getAttribute('data-cem-minlength'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.tooLong) { //maxlength 属性で指定された長さを超えている場合 const dataError = elem.getAttribute('data-cem-maxlength'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.rangeOverflow) { //max 属性で指定された最大値を超えている場合 const dataError = elem.getAttribute('data-cem-max'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.rangeUnderflow) { //min 属性で指定された最小値未満の場合 const dataError = elem.getAttribute('data-cem-min'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.stepMismatch) { //step 属性で指定された規則に合致しない場合 const dataError = elem.getAttribute('data-cem-step'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.badInput) { //入力値がブラウザーが処理できない(変換できない)場合 const dataError = elem.getAttribute('data-cem-badinput'); if(dataError) { elem.setCustomValidity(dataError); } } } } //対象の要素に input イベントのリスナーを登録 targetElems.forEach((elem) => { elem.addEventListener('input', (e) => { setCustomErrorMessage(elem); }); }); //送信ボタンを取得 const submitButton = document.myForm.send; //送信ボタンのクリックイベント(フォームの submit では NG) submitButton.addEventListener('click', () => { if(!myForm.checkValidity()){ targetElems.forEach( (elem) => { if(!elem.validity.valid) { setCustomErrorMessage(elem); } }) } });
クラス属性を指定しない例
前述の例では、独自のエラーメッセージを表示する要素にクラス(class="cem")を指定する必要がありましたが、以下の例では data-cem-xxxx 属性を指定するだけで、別途クラスの指定をしなくてもすみます。
また、以下では同じページに複数のフォームがある場合も対応できるようにしています(特定のクラスなどは指定する必要はありません)。但し、それぞれのフォームの送信ボタンは、異なるマークアップ(button または input 要素)になる可能性があるので、以下の例では送信ボタンに class="submitBtn" を指定して、querySelectorAll() で取得するようにしています。
そのページのフォームは document.forms で取得できます。document.forms は HTMLCollection なので ES6(ECMAScript 2015) からは for of 文が使え、個々の form に属する全てのコントロール要素はその elements プロパティ で取得できます。
要素に設定されている data-* 属性は dataset プロパティでまとめて取得して、その属性の名前に cem が含まれているかは startsWith() を使って判定しています。
送信ボタンにクリックイベントを設定する際に、そのボタンの所属する form 要素は form プロパティで取得しています。
//そのページのフォームを全て取得 const myforms = document.forms; //独自のエラーメッセージを表示する要素を格納する配列 let targetElems = []; //取得した全てのフォームのそれぞれについて独自のエラーメッセージを表示する要素を取得 for(let form of myforms) { //フォームの elements プロパティ(全てのコントロール要素)を取得 const elements = form.elements; //それぞれのコントロール要素を調査 for(let elem of elements) { //要素に設定されている data-* 属性をまとめて取得 const datasets = elem.dataset; //まとめて取得したそれぞれの data-* 属性を調査 for(let key in datasets){ //data-* 属性の名前の data- 以降の部分が cem で始まっていれば if(key.startsWith('cem')) { //配列に追加 targetElems.push(elem); //最初の1つだけで良いので for 文を抜ける break; } } } } //独自のエラーメッセージを設定する関数(前述の例と同じ) const setCustomErrorMessage = (elem) => { elem.setCustomValidity(''); if(!elem.validity.valid) { if(elem.validity.valueMissing) { const dataError = elem.getAttribute('data-cem-required'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.typeMismatch) { const dataError = elem.getAttribute('data-cem-type'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.patternMismatch) { const dataError = elem.getAttribute('data-cem-pattern'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.tooShort) { const dataError = elem.getAttribute('data-cem-minlength'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.tooLong) { const dataError = elem.getAttribute('data-cem-maxlength'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.rangeOverflow) { const dataError = elem.getAttribute('data-cem-max'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.rangeUnderflow) { const dataError = elem.getAttribute('data-cem-min'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.stepMismatch) { const dataError = elem.getAttribute('data-cem-step'); if(dataError) { elem.setCustomValidity(dataError); } } else if(elem.validity.badInput) { const dataError = elem.getAttribute('data-cem-badinput'); if(dataError) { elem.setCustomValidity(dataError); } } } } //対象の要素に input イベントのリスナーを登録 targetElems.forEach((elem) => { elem.addEventListener('input', (e) => { setCustomErrorMessage(elem); }); }); //class="submitBtn" の送信ボタンを全て取得 const submitButtons = document.querySelectorAll('.submitBtn'); //取得した送信ボタンにクリックイベントを設定 submitButtons.forEach((button) => { button.addEventListener('click', () => { //送信ボタンが属する form 要素を form プロパティで取得して checkValidity() を実行 if(!button.form.checkValidity()){ targetElems.forEach( (elem) => { if(!elem.validity.valid) { setCustomErrorMessage(elem); } }) } }); });
以下は上記サンプルの HTML です。
<form name="myForm1"> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" required data-cem-required="名前は必須です" minlength="2" data-cem-minlength="2文字以上でご入力ください" > </div> ・・・中略(前述の例のフォームと同じ)・・・ <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required data-cem-required="送信するにはチェックを入れてください"> <label for="agreement"> 同意する </label> </div> <button class="submitBtn" name="send">送信</button><!-- submitBtn クラスを指定 --> </form> <form name="myForm2"> <div> <label for="userName">ユーザー名: </label> <input type="text" name="userName" id="userName" required data-cem-required="ユーザー名は必須です" pattern="[a-zA-Z0-9]{4,10}" data-cem-pattern="半角英数字4〜10文字です" > </div> <div> <label for="age">年齢 </label> <input type="number" name="age" id="age" required data-cem-required="年齢は必須です" min="18" data-cem-min="18歳未満は対象外です" > </div> <button class="submitBtn" name="send2">送信</button><!-- submitBtn クラスを指定 --> </form>
自動検証を無効にする
ブラウザーの自動検証を無効にするには form 要素に novalidate 属性を指定します。
novalidate 属性を指定するとブラウザーによるエラーメッセージは表示されませんが、制約検証 API や CSS の検証用の疑似クラス(:invalid など)の適用を無効にするわけではないので、それらを利用して独自のエラーメッセージを任意の位置に表示することができます。
以下は form 要素に novalidate 属性を指定してブラウザーの自動検証を無効にして(ブラウザーによるメッセージは表示させず)独自のエラーメッセージを指定した位置に表示する例です。
この例では入力される値が変更されるたびにそれが妥当な値かをチェックして、値が無効な場合は独自のエラーメッセージを error クラスを指定した span 要素に表示します。
span 要素に指定してある aria-live="polite" はスクリーンリーダーのユーザにエラーが発生した際にエラーの内容を伝えるための属性です。aria-live の値には通常 polite を指定します。
<form name="myForm" novalidate> <!-- novalidate 属性を指定 --> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" required minlength="6"> <span class="error" aria-live="polite"></span> <!-- エラーメッセージを表示 --> </div> <button name="send">送信</button> </form>
CSS では検証の要件を満たさない要素に適用される :invalid を使ってデータが無効な場合のスタイルを設定しています。
※ 新しく追加された :user-invalid 擬似クラスを使用すれば、初期状態ではエラー表示になりません。
input[type=email]{ border: 1px solid #333; margin: 0; font-size: 90%; box-sizing: border-box; } /* 検証の要件を満たさない(:invalid を適用された)要素のスタイル */ input:invalid{ border-color: #900; background-color: #FDD; } input:focus:invalid { outline: none; } /* エラーメッセージの基本のスタイル */ .error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color:red; box-sizing: border-box; } /* .active はエラーを表示する関数で追加されるクラス */ .error.active { padding: 0.3em; }
この例では form 要素やコントロール要素は document.forms を使って取得していますが、 DOM のメソッドを使って取得することもできます。
span 要素はフォームコントロールではないので、document.querySelector() を使って取得しています。
入力される値が変更されるたびにそれが妥当な値かをチェックするために、name="mail" の input 要素に input イベントのリスナー登録して、input 要素の validity プロパティ(ValidityState オブジェクト)の valid プロパティ(検証が妥当な場合は true)を確認します。
入力された値が妥当であればエラーメッセージを空にしてエラーメッセージを削除し、スタイルを初期化します。値が無効な場合はエラーメッセージを表示する関数 showError() を実行します。
フォーム要素には submit イベントを設定して、フォームが送信される際に入力された値が妥当かチェックします。入力された値が妥当であれば、何もせずフォームを送信します。入力された値が妥当でない場合は showError() を実行し、preventDefault() でフォームの送信を停止します。
エラーメッセージを表示する関数 showError()は、入力要素の validity のプロパティを使ってエラーの内容を判定しエラーメッセージを表示します。
//document.forms を使って name="myForm" の form 要素を取得 const myForm = document.forms.myForm; //または const myForm = document.myForm; //name="mail" の input 要素 const email = myForm.mail; //class="error" の span 要素 const errorText = document.querySelector('#mail + span.error'); email.addEventListener('input', () => { // 入力される値が変更されるたびに値が有効かどうかを確認 if (email.validity.valid) { //入力された値が有効であれば、エラーメッセージを空にしてその span 要素のクラスを初期化 errorText.textContent = ''; // エラーメッセージを空に errorText.className = 'error'; // クラスを初期値の error のみに //または errorText.classList.remove('active'); } else { // 入力された値が検証の要件を満たしていなければエラーを表示 showError(); } }); myForm.addEventListener('submit', (e) => { // 入力された値が有効であればフォームを送信 if(!email.validity.valid) { // 入力された値が有効でなければエラーを表示 showError(); // フォームのデフォルトの動作(送信)を中止 e.preventDefault(); } }); const showError = () => { if(email.validity.valueMissing) { // 値が空の場合のエラーメッセージを設定 errorText.textContent = 'メールアドレスは必須です。'; } else if(email.validity.typeMismatch) { // 入力された値が正しいメールの形式でない場合のエラーメッセージを設定 errorText.textContent = '正しい形式のメールアドレスを入力してください'; } else if(email.validity.tooShort) { // 入力された値が6文字未満の場合のエラーメッセージを設定 errorText.textContent = `メールアドレスは ${ email.minLength } 文字以上です。現在の文字数:${ email.value.length }`; } // エラーメッセージの span 要素に active クラスを追加 errorText.className = 'error active'; //または errorText.classList.add('active'); }
入力される値が変更されるたびにエラーを表示するのではなく、送信時にのみエラーを表示するのであれば例えば以下のように前述の input イベントの部分を focus イベントに変えて単にエラーメッセージと追加のエラークラスをクリアします(関連:イベントのタイミング)。
email.addEventListener('focus', () => { errorText.textContent = ''; // エラーメッセージを空に errorText.className = 'error'; // クラスを初期値の error だけに //または errorText.classList.remove('active'); });
複数コントロールのサンプル
以下は複数のコントロールがある場合の例です。
全てに以下のような pattern 属性を設定し、名前とメールアドレスには required 属性を設定しています。
- 名前:2文字以上10文字以内(機能を確認するためのものであまり意味はありません)
- 電話番号:0 から始まる半角数字で特定の桁数でハイフンを許容(ハイフンなしも許容)
- メールアドレス:type 属性に加え、ドメイン部分にドット(.)が必要。minlength で最低10文字(機能を確認するためのものであまり意味はありません)
検証を満たさない場合のエラーは error クラスを指定した span 要素に表示します。
<form name="myForm" novalidate><!-- novalidate 属性を指定 --> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" required> <span class="error" aria-live="polite"></span> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}"> <span class="error" aria-live="polite"></span> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" minlength="10" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required> <span class="error" aria-live="polite"></span> </div> <button name="send">送信</button> </form>
以下がエラーメッセージのスタイルで、前述の例と同じですが、この例では :invalid 擬似クラスを使ったエラー時のスタイルを input 要素には設定していません。
/* エラーメッセージの基本のスタイル */ .error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color:red; box-sizing: border-box; } /* .active はエラーを表示する関数で追加されるクラス */ .error.active { padding: 0.3em; }
JavaScript では検証するコントロール要素を取得してそれらをまとめて処理するために配列に格納しています(フォームの elements プロパティを使えばコントロール要素をまとめて取得できます)。
そして for 文でそれぞれのコントロール要素に input イベントのリスナーを登録しています。
リスナー関数では、エラーを表示する要素を取得して textContent に空文字を設定してクリアし、active クラスを削除して初期状態に戻し、値が検証の要件を満たしていない場合は showError() を呼び出してエラーを表示します。
エラーを表示する .error の span 要素の取得は、コントロール要素の親要素の div 要素を取得して div 要素のメソッドとして querySelector() を使っています。
form 要素の submit イベントのリスナー関数では、検証が満たされていない場合、それぞれのコントロール要素の validity.valid プロパティを調べてそのコントロール要素が検証を満たしていなければ showError() を呼び出してエラーを表示します。
//document.forms を使って name="myForm" の form 要素を取得 const myForm = document.forms.myForm; //name="name" の input 要素 const name = myForm.name; //name="tel" の input 要素 const tel = myForm.tel; //name="mail" の input 要素 const email = myForm.mail; //検証するコントロール要素の配列 const targets = [name, tel, email]; //検証するコントロール要素に input イベントのリスナーを登録 for(let i=0; i<targets.length; i++) { // 入力される値が変更されるたびに値を確認 targets[i].addEventListener('input', (e) => { //その要素の親要素(div 要素)。e.currentTarget は targets[i] でも同じ const parentDiv = e.currentTarget.parentElement; //親要素の div 要素のメソッドとして querySelector() で .error の span 要素を取得 const errorSpan = parentDiv.querySelector('span.error'); //エラーをクリア(初期化) errorSpan.textContent = ''; errorSpan.classList.remove('active'); // 入力された値が検証の要件を満たしていなければエラーを表示 if(!targets[i].validity.valid) { showError(e.currentTarget); //または showError(targets[i]); } }); } //form 要素に submit イベントのリスナーを設定 myForm.addEventListener('submit', (e) => { //form 要素で checkValidity() を実行して検証が満たされていなければ if(!e.currentTarget.checkValidity()){ //それぞれのコントロール要素の validity.valid を調べる for(let i=0; i < targets.length; i++) { //そのコントロール要素が検証を満たしていなければ if(!targets[i].validity.valid) { //エラーを表示 showError(targets[i]); } } //いずれかのコントロール要素で検証が満たされていなければデフォルトの動作(送信)を中止 e.preventDefault(); } }); //エラーを表示し、span 要素に active クラスを追加する関数 const showError = (elem) => { //エラーを表示する span 要素を取得 const errorSpan = elem.parentElement.querySelector('span.error'); //validity の該当するプロパティがあればメッセージを表示 if(elem.validity.valueMissing) { //要素の値が空の場合のエラーメッセージを span 要素に設定 errorSpan.textContent = '必須です' } else if(elem.validity.typeMismatch) { //typeMismatch に該当し且つ type 属性が email の要素の場合のメッセージ if(elem.type === 'email') { errorSpan.textContent = '正しいメールアドレスの形式でお願いします'; }else{ //typeMismatch に該当し type 属性が email 以外(この場合該当する要素はなし) errorSpan.textContent = '値をお確かめください(タイプが合いません)'; } } else if(elem.validity.patternMismatch) { //指定された pattern に合致しない要素の場合 if(elem.name ==='name') { //name 属性が name の要素 errorSpan.textContent = '2文字以上10文字以内でお願いします'; } else if(elem.name ==='tel') { //name 属性が tel の要素 errorSpan.textContent = '0から始まる半角数字で入力ください(ハイフンを含めることができます)'; } else if(elem.name ==='mail'){ //name 属性が mail の要素 errorSpan.textContent = '@ とドメイン名を含む正しいメールアドレスの形式でお願いします'; } } else if(elem.validity.tooShort) { // 入力された値が minLength を満たさない要素のエラーメッセージを設定 errorSpan.textContent = `${ elem.minLength } 文字以上でお願いします。現在の文字数:${ elem.value.length }`; } // エラーメッセージの span 要素に active クラスを追加 errorSpan.classList.add('active'); }
エラーメッセージのカスタマイズ
showError() では引数にコントロール要素を受け取り、エラーを表示する span 要素を取得して、validity の各プロパティ(ValidityState)を調べて該当するエラーがあればメッセージを設定しています。
validity の同じプロパティに対しては、必要に応じてその要素の type 属性や name 属性で更にメッセージを細分化しています。
この方法の場合、使用しているフォームの構造や構成(要素の name 属性や type 属性の値など)によりエラーメッセージをカスタマイズしているので、異なる構造の場合は、そのフォームの構造に合わせて書き換える必要があります。
「data-* 属性でエラーをカスタマイズ」のような方法を使えば少し汎用的になります。
elements プロパティ
前述の例では検証対象のコントロール要素をそれぞれ取得して配列に格納しましたが、form 要素の elements プロパティを使えば、そのフォームのコントロール要素を全て取得することができます。
elements は全てのコントロールが含まれる配列のようなオブジェクト(HTMLCollection)です。
以下では全てのコントロールに required 属性を設定し、コントロールによっては追加の検証属性を設定しています。
<form name="myForm" novalidate><!-- novalidate 属性を指定 --> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" required> <span class="error" aria-live="polite"></span> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" required> <span class="error" aria-live="polite"></span> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required minlength="8"> <span class="error" aria-live="polite"></span> </div> <div> <p>選択してください</p> <!-- 必須にするには name 属性の input 要素のいずれかを required に --> <input type="radio" name="color" value="blue" id="blue" required> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> <span class="error" aria-live="polite"></span> </div> <div> <select name="season" id="season" required> <!-- 初期状態で選択されている項目のvalue を空に --> <option value="">選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> <span class="error" aria-live="polite"></span> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" maxlength="100" required rows="3" cols="50"></textarea> <span class="error" aria-live="polite"></span> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required> <label for="agreement"> 同意する </label> <span class="error" aria-live="polite"></span> </div> <button name="send">送信</button> </form>
以下がエラーメッセージのスタイルで前述の例と同じです。
/* エラーメッセージの基本のスタイル */ .error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color:red; box-sizing: border-box; } /* .active はエラーを表示する関数で追加されるクラス */ .error.active { padding: 0.3em; }
前述の例では個々のコントロール要素を取得して配列を作成しましたが、以下ではその代わりに elements プロパティを使っているだけで内容的には同じです。
この例では最後のコントロールの送信ボタンは除外するため、for 文では elements.length-1 で最後の要素は除外しています。
検証対象ではないコントロールがある場合、無駄なイベント登録が増えるのと送信時のエラー確認で無駄な確認が増えますがあまり問題はないと思います。
検証対象ではないコントロールを除外したい場合は、対象の要素にクラス属性を設定するなどして検証の対象を限定することもできます。type 属性が hidden や reset、 button の場合や disabled 属性が設定されいている要素を除外する必要があれば、willValidate 属性を調べて判定することもできます。
※ 以下ではコントロール要素に変更があった場合のイベントは全て input イベントを使っていますが、チェックボックスやラジオボタンでは change イベントを使用するほうが互換性が高いようです。
また、input イベントは値が変更されるたびに発生するので、煩わしいと感じる場合は change イベントを使用します(イベントのタイミング)。
エラーを表示する関数 showError() は、このフォームの要素の name 属性や type 属性の値などを使ってエラーメッセージをカスタマイズしているので、フォームの構造が異なれば、それに合わせて書き換える必要があります(エラーメッセージのカスタマイズ)。
//document.forms を使って name="myForm" の form 要素を取得 const myForm = document.forms.myForm; //フォームのコントロール要素全てを取得 const elements = myForm.elements; //コントロール要素(最後のコントロールのボタンを除く)に input イベントのリスナーを登録 //または change イベントに登録(10行目の input を change に変更) for(let i=0; i<elements.length-1; i++) { // 入力される値が変更されるたびに値が有効かどうかを確認 elements[i].addEventListener('input', (e) => { //エラーメッセージを空にしてその span 要素のクラスを初期化 //その要素の親要素(div 要素)。e.currentTarget は elements[i] でも同じ const parentDiv = e.currentTarget.parentElement; //親要素の div 要素のメソッドとして querySelector() で .error の span 要素を取得 const errorSpan = parentDiv.querySelector('span.error'); errorSpan.textContent = ''; errorSpan.classList.remove('active'); // 入力された値が検証の要件を満たしていなければエラーを表示 if(!elements[i].validity.valid) { showError(e.currentTarget); //または showError(elements[i]); } }); } //form 要素に submit イベントのリスナーを設定 myForm.addEventListener('submit', (e) => { //form 要素で checkValidity() を実行して検証が満たされていなければ if(!e.currentTarget.checkValidity()){ //全てのコントロール要素の validity.valid を調べる for(let i=0; i < elements.length-1; i++) { //そのコントロール要素が検証を満たしていなければ if(!elements[i].validity.valid) { //エラーを表示 showError(elements[i]); } } //いずれかのコントロール要素で検証が満たされていなければデフォルトの動作(送信)を中止 e.preventDefault(); } }); //エラーを表示し、span 要素に active クラスを追加する関数 const showError = (elem) => { const errorSpan = elem.parentElement.querySelector('span.error'); if(elem.validity.valueMissing) { //要素の値が空の場合のエラーメッセージを span 要素に設定 errorSpan.textContent = '必須です' } else if(elem.validity.typeMismatch) { //typeMismatch に該当し且つ type 属性が email の要素の場合のメッセージ if(elem.type === 'email') { errorSpan.textContent = '正しいメールアドレスの形式でお願いします'; }else{ //typeMismatch に該当し type 属性が email 以外(この場合該当する要素はなし) errorSpan.textContent = '値をお確かめください(タイプが合いません)'; } } else if(elem.validity.patternMismatch) { //指定された pattern に合致しない要素の場合 if(elem.name ==='name') { //name 属性が name の要素 errorSpan.textContent = '2文字以上10文字以内でお願いします'; } else if(elem.name ==='tel') { //name 属性が tel の要素 errorSpan.textContent = '0から始まる半角数字で入力ください(ハイフンを含めることができます)'; } else if(elem.name ==='mail'){ //name 属性が mail の要素 errorSpan.textContent = '@ とドメイン名を含む正しいメールアドレスの形式でお願いします'; } } else if(elem.validity.tooShort) { // 入力された値が minLength を満たさない要素のエラーメッセージを設定 errorSpan.textContent = `${ elem.minLength } 文字以上でお願いします。現在の文字数:${ elem.value.length }`; } else if(elem.validity.tooLong) { // 入力された値が maxLength を満たさない要素のエラーメッセージを設定(これは発生しないはず) errorSpan.textContent = `${ elem.maxLength } 文字以内でお願いします。現在の文字数:${ elem.value.length }`; } // エラーメッセージの span 要素に active クラスを追加 errorSpan.classList.add('active'); }
エラーの span 要素を JS で追加
エラーを表示する span 要素を JavaScript で追加することができます。
HTML ではエラーを表示する span 要素は記述せず、 JavaScript で追加します。
<form name="myForm" novalidate> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" required> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" required> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required minlength="8"> </div> <div> <p>選択してください</p> <input type="radio" name="color" value="blue" id="blue" required> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> </div> <div> <select name="season" id="season" required> <option value="">選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" maxlength="100" required rows="3" cols="50"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
前述の例との違いは、3〜23行目のエラーを表示する span 要素を生成して親要素に appendChild() を使って追加する部分のみです。
この場合、各コントロール要素は div 要素などの親要素で囲まれている必要があります。
ラジオボタンの場合は、その親要素から見て最初のラジオボタンに対して1つだけ追加するようにしています(そうしないとラジオボタンの項目の数だけ追加されてしまいます)。
const elements = document.myForm.elements; //エラーを表示する span 要素を生成して追加(次の for 文に含めることもできる) for(let i=0; i<elements.length-1; i++) { //span 要素を生成 const errorSpan = document.createElement('span'); //error クラスを設定 errorSpan.className = 'error'; //aria-live 属性を設定 errorSpan.setAttribute('aria-live', 'polite'); //ラジオボタン以外 if(elements[i].type !== 'radio') { //span 要素を親要素に追加 elements[i].parentNode.appendChild(errorSpan); }else{ //ラジオボタンの場合、ラジオボタンの親要素を取得 const parentElem = elements[i].parentElement; //親要素から見て最初のラジオボタンの要素を取得 const firstOfType = parentElem.querySelector('[type="radio"]'); //最初の要素の場合のみ親要素に span 要素を追加 if(elements[i] === firstOfType) elements[i].parentNode.appendChild(errorSpan); } } /* 以下は前述の例と同じ */ for(let i=0; i<elements.length-1; i++) { //input イベントが煩わしい場合は change イベントを使用 elements[i].addEventListener('input', (e) => { const parentDiv = e.currentTarget.parentElement; const errorSpan = parentDiv.querySelector('span.error'); errorSpan.textContent = ''; errorSpan.classList.remove('active'); if(!elements[i].validity.valid) { showError(e.currentTarget); } }); } document.myForm.addEventListener('submit', (e) => { if(!e.currentTarget.checkValidity()){ for(let i=0; i < elements.length-1; i++) { if(!elements[i].validity.valid) { showError(elements[i]); } } e.preventDefault(); } }); //エラーを表示し、span 要素に active クラスを追加する関数 const showError = (elem) => { const errorSpan = elem.parentElement.querySelector('span.error'); if(elem.validity.valueMissing) { errorSpan.textContent = '必須です' } else if(elem.validity.typeMismatch) { if(elem.type === 'email') { errorSpan.textContent = '正しいメールアドレスの形式でお願いします'; }else{ errorSpan.textContent = '値をお確かめください(タイプが合いません)'; } } else if(elem.validity.patternMismatch) { if(elem.name ==='name') { errorSpan.textContent = '2文字以上10文字以内でお願いします'; } else if(elem.name ==='tel') { errorSpan.textContent = '0から始まる半角数字で入力ください(ハイフンを含めることができます)'; } else if(elem.name ==='mail'){ errorSpan.textContent = '@ とドメイン名を含む正しいメールアドレスの形式でお願いします'; } } else if(elem.validity.tooShort) { errorSpan.textContent = `${ elem.minLength } 文字以上でお願いします。現在の文字数:${ elem.value.length }`; } else if(elem.validity.tooLong) { errorSpan.textContent = `${ elem.maxLength } 文字以内でお願いします。現在の文字数:${ elem.value.length }`; } errorSpan.classList.add('active'); }
クラス名で対象を限定
form 要素の elements プロパティを使う場合、全てのコントロールが対象になります。
検証対象ではないコントロールを除外したい場合、対象の要素を fieldset 要素で囲んで fieldset 要素の elements プロパティを使ったり、対象の要素にクラス属性を設定して getElementsByClassName() や querySelectorAll() などで対象の要素の集まりを取得するなどが考えられます。
以下は対象の要素に validate というクラスを指定して、検証の対象を限定する例です(エラーを表示する要素は JavaScript で追加しています)。
以下の例では全てを検証の対象にするため、全てのコントロールに検証属性と validate というクラスを指定していますが、検証が不要なコントロールには検証属性及びクラスを指定しないようにします。
※また、ラジオボタンでは、input イベントが機能するために全てのラジオボタンの項目(type="radio" の input 要素)に validate クラスを指定する必要があります。
<form name="myForm" novalidate><!-- novalidate 属性を指定 --> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" required class="validate"> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" required class="validate"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required minlength="8" class="validate"> </div> <div> <p>選択してください</p><!-- 全てのラジオボタンに class="validate" を指定 --> <input type="radio" name="color" value="blue" id="blue" required class="validate"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red" class="validate"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green" class="validate"> <label for="green"> 緑 </label> </div> <div> <select name="season" id="season" required class="validate"> <option value="">選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" maxlength="100" required rows="3" cols="50" class="validate"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required class="validate"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
前述の例との違いは、対象の要素を elements プロパティで取得する代わりに querySelectorAll('.validate') で取得しています。また、for 文の代わりに NodeList の forEach メソッドを使用しています。但し、NodeList の forEach メソッドは IE 未対応です(ポリフィル)。
//検証対象の(.validate を指定した)要素を取得 const elements = document.querySelectorAll('.validate'); elements.forEach((elem) => { //エラーを表示する span 要素を生成して対象の要素の親要素に追加 const errorSpan = document.createElement('span'); errorSpan.className = 'error'; errorSpan.setAttribute('aria-live', 'polite'); if(elem.type !== 'radio') { elem.parentNode.appendChild(errorSpan); }else{ const parentElem = elem.parentElement; const firstOfType = parentElem.querySelector('[type="radio"]'); if(elem === firstOfType) elem.parentNode.appendChild(errorSpan); } //対象の要素に input イベントのリスナーを登録 //input イベントが煩わしい場合は change イベントを使用 elem.addEventListener('input', (e) => { const parentDiv = e.currentTarget.parentElement; const errorSpan = parentDiv.querySelector('span.error'); errorSpan.textContent = ''; errorSpan.classList.remove('active'); if(!elem.validity.valid) { showError(e.currentTarget); } }); }); //form 要素に submit イベントのリスナーを設定 document.myForm.addEventListener('submit', (e) => { if(!e.currentTarget.checkValidity()){ elements.forEach( (elem) => { if(!elem.validity.valid) { showError(elem); } }) e.preventDefault(); } }); //エラーを表示し、span 要素に active クラスを追加する関数 const showError = (elem) => { const errorSpan = elem.parentElement.querySelector('span.error'); if(elem.validity.valueMissing) { errorSpan.textContent = '必須です' } else if(elem.validity.typeMismatch) { if(elem.type === 'email') { errorSpan.textContent = '正しいメールアドレスの形式でお願いします'; }else{ errorSpan.textContent = '値をお確かめください(タイプが合いません)'; } } else if(elem.validity.patternMismatch) { if(elem.name ==='name') { errorSpan.textContent = '2文字以上10文字以内でお願いします'; } else if(elem.name ==='tel') { errorSpan.textContent = '0から始まる半角数字で入力ください(ハイフンを含めることができます)'; } else if(elem.name ==='mail'){ errorSpan.textContent = '@ とドメイン名を含む正しいメールアドレスの形式でお願いします'; } } else if(elem.validity.tooShort) { errorSpan.textContent = `${ elem.minLength } 文字以上でお願いします。現在の文字数:${ elem.value.length }`; } else if(elem.validity.tooLong) { errorSpan.textContent = `${ elem.maxLength } 文字以内でお願いします。現在の文字数:${ elem.value.length }`; } errorSpan.classList.add('active'); }
data-* 属性でエラーをカスタマイズ
以下は必要に応じて data-* 属性(カスタムデータ属性)を使って、エラーメッセージをカスタマイズできるようにする例です。
data-* 属性を指定しない場合は、validationMessage プロパティを使って、システム(ブラウザ)のデフォルトのメッセージを表示します。
また、エラーメッセージを表示する span 要素は動的に追加・削除します。
指定する検証属性や type 属性により、以下のような data-* 属性を指定することでエラーメッセージをカスタマイズできます。
data-* 属性 | ValidityState | 説明(指定するエラーメッセージ) |
---|---|---|
data-error-required | valueMissing | required 属性が満たされていない(値がない)場合のエラーメッセージ |
data-error-type | typeMismatch | type 属性で指定されたタイプの構文に合っていない場合のエラーメッセージ |
data-error-pattern | patternMismatch | pattern 属性の指定と一致しない場合のエラーメッセージ |
data-error-minlength | tooShort | minlength 属性で指定された長さに満たない場合のエラーメッセージ |
data-error-maxlength | tooLong | maxlength 属性で指定された長さを超えている場合のエラーメッセージ |
data-error-min | rangeUnderflow | min 属性で指定された値に満たない場合のエラーメッセージ |
data-error-max | rangeOverflow | max 属性で指定された値を超えている場合のエラーメッセージ |
data-error-step | stepMismatch | step 属性で指定された規則に合致しない場合のエラーメッセージ |
data-error-badinput | badInput | 入力値をブラウザーが処理できない場合のエラーメッセージ |
前述までの例では、ValidityState プロパティと name 属性や type 属性の値を元にエラーメッセージを生成していたので、要素の name 属性の値に依存してしまいますが、この方法の場合、必要に応じて要素ごとにエラーメッセージを設定できるので汎用的に使えるかと思います。
以下の例では、全ての要素に data-* 属性を指定して、エラーメッセージをカスマイズしています。
また、以下のメールアドレスの入力では、type 属性に email を指定して、更に pattern 属性と required 属性を指定しています。pattern 属性と required 属性を満たさない場合は、カスタムエラーメッセージを表示するようにしています。data-error-type は指定していないので、type 属性のタイプの構文に合っていない場合のエラーはシステムのデフォルトのメッセージが表示されます。
<form name="myForm" novalidate> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" data-error-pattern="2文字以上10文字以内" required data-error-required="名前は必須です" class="validate"> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" data-error-pattern="0から始まる番号を半角数字のみまたはハイフンを付けて入力" required data-error-required="電話番号は必須です" class="validate"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" data-error-pattern="メールの形式が正しくないようです。@ と . 及びドメイン名が必要です。" required data-error-required="メールアドレスは必須です" minlength="8" class="validate"> </div> <div> <p>色を選択してください</p> <input type="radio" name="color" value="blue" id="blue" required data-error-required="色の選択は必須です" class="validate"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red" class="validate"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green" class="validate"> <label for="green"> 緑 </label> </div> <div> <select name="season" id="season" required data-error-required="季節の選択は必須です" class="validate"> <option value="">季節を選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" maxlength="100" minlength="10" data-error-minlength="10文字以上でお願いします" required data-error-required="お問い合わせ内容は必須です" rows="3" cols="50" class="validate"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required data-error-required="送信するにはチェックを入れてください" class="validate"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
前述の例では、予め生成しておいたエラー用の span 要素の textContent を使ってエラーの表示・非表示を行っていますが、以下ではエラー用の span 要素を生成してエラーを追加する関数を作成してエラーを表示しています。エラーを非表示にするには該当するエラー用の span 要素を削除しています。
生成されるエラー用の span 要素は error クラスの他に検証属性の値(以下のスクリプトでは validationType)を使ったクラス名が付与されます。例えば必須の要件を満たしていないエラーの場合は class="error required" が付与されます。
エラー用の span 要素に付与されるクラス名(error)は必要に応じて4行目で変更できます。
また、data-* 属性を指定しない場合は、その要素の validationMessage プロパティを使って、ブラウザのデフォルトのメッセージを設定しています。
//validate クラスを指定した要素(検証を行う要素)を全て取得 const elements = document.querySelectorAll('.validate'); //エラーを表示する span 要素に付与するクラス名 const errorClassName = 'error'; elements.forEach((elem) => { //対象の要素に input イベントのリスナーを登録 elem.addEventListener('input', (e) => { const parentDiv = e.currentTarget.parentElement; const errorSpans = parentDiv.querySelectorAll('span.' + errorClassName); if(errorSpans) { errorSpans.forEach((errorSpan) => { elem.parentNode.removeChild(errorSpan); }); } if(!elem.validity.valid) { showError(e.currentTarget); } }); }); //form 要素に submit イベントのリスナーを設定 document.myForm.addEventListener('submit', (e) => { if(!e.currentTarget.checkValidity()){ elements.forEach( (elem) => { if(!elem.validity.valid) { showError(elem); } }) e.preventDefault(); } }); /* エラーメッセージを表示する span 要素を生成して親要素に追加する関数 elem :対象の要素 validationType :対象の検証属性名または type(例:パターン検証なら pattern、type 属性の検証なら type など) */ const setupErrorMsg = (elem, validationType ) => { //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + validationType); //エラーを表示する span 要素が存在しなければ if(!errorSpan) { //エラーメッセージの変数にシステムのデフォルトエラーメッセージを代入 let errorMessage = elem.validationMessage; //要素に data-error-xxxx 属性が指定されていれば(xxxx は検証属性 または type 属性名) if(elem.hasAttribute('data-error-' + validationType)) { //data-error-xxxx 属性の値を取得 const dataError = elem.getAttribute('data-error-' + validationType); if(dataError) { //data-error-xxxx 属性の値をエラーメッセージとする errorMessage = dataError; } } //エラーメッセージ表示する span 要素を生成して追加 //span 要素を生成 const errorSpan = document.createElement('span'); //error 及び引数に指定されたクラスを追加(設定) errorSpan.classList.add(errorClassName, validationType); //aria-live 属性を設定 errorSpan.setAttribute('aria-live', 'polite'); //引数に指定されたエラーメッセージを設定 errorSpan.textContent = errorMessage; //elem の親要素の子要素として追加 elem.parentNode.appendChild(errorSpan); } } //ValidityStateのプロパティ(valueMissingなど)をチェックして対応するエラーを表示する関数 const showError = (elem) => { if(elem.validity.valueMissing) { //required 属性が満たされていない(値がない)場合 setupErrorMsg(elem, 'required'); } else if(elem.validity.typeMismatch) { //type 属性で指定されたタイプの構文に合っていない場合 setupErrorMsg(elem, 'type'); } else if(elem.validity.patternMismatch) { //pattern 属性の指定と一致しない場合 setupErrorMsg(elem, 'pattern'); } else if(elem.validity.tooShort) { //minlength 属性で指定された長さに満たない場合 setupErrorMsg(elem, 'minlength'); } else if(elem.validity.tooLong) { //maxlength 属性で指定された長さを超えている場合 setupErrorMsg(elem, 'maxlength'); } else if(elem.validity.rangeOverflow) { //max 属性で指定された最大値を超えている場合 setupErrorMsg(elem, 'max'); } else if(elem.validity.rangeUnderflow) { //min 属性で指定された最小値未満の場合 setupErrorMsg(elem, 'min'); } else if(elem.validity.stepMismatch) { //step 属性で指定された規則に合致しない場合 setupErrorMsg(elem, 'step'); } else if(elem.validity.badInput) { //入力値がブラウザーが処理できない(変換できない)場合 setupErrorMsg(elem, 'badinput'); } }
サンプル
以下はブラウザーの自動検証を無効にして、必要に応じて data-* 属性を指定して独自のエラーメッセージを span 要素に表示する例です。data-* 属性を指定しない場合はブラウザのデフォルトのエラーメッセージが表示されます(内容は前述の data-* 属性でエラーをカスタマイズ と同じです)。
このサンプルのスクリプトを使うには、form 要素に class="validationForm" と novalidate 属性を指定し、コントロール要素と label 要素(もしあれば)を div 要素で囲む必要があります。エラーメッセージはコントロール要素の親要素の子要素として追加されます。
また、検証を行うコントロール要素には validate クラス(class="validate")と検証属性を指定し、独自のエラーメッセージを表示するには data-error-xxxx 属性にメッセージを指定します。
<!-- form 要素に class="validationForm" と novalidate 属性を指定 --> <form name="myForm" class="validationForm" novalidate> <!-- div 要素でコントロール要素とラベル(もしあれば)を囲む --> <div> <label for="name">名前: </label> <!-- コントロール要素に validate クラスと検証属性を指定--> <input type="text" name="name" id="name" pattern=".{2,10}" data-error-pattern="2文字以上10文字以内" required data-error-required="名前は必須です" class="validate"> </div> <div> <label for="tel">電話番号: </label> <!-- 独自のエラーメッセージは data-error-xxxx 属性に指定--> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" data-error-pattern="0から始まる番号を半角数字のみまたはハイフンを付けて入力" required data-error-required="電話番号は必須です" class="validate"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" data-error-pattern="メールの形式が正しくないようです。@ と . 及びドメイン名が必要です。" required data-error-required="メールアドレスは必須です" minlength="8" class="validate"> </div> <div> <p>色を選択してください</p> <input type="radio" name="color" value="blue" id="blue" required data-error-required="色の選択は必須です" class="validate"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red" class="validate"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green" class="validate"> <label for="green"> 緑 </label> </div> ・・・ <button name="send">送信</button> </form>
以下は独自のエラーメッセージを表示する場合に指定できるカスタムデータ属性です。data-error-xxxx の xxxx の部分には対応する検証属性名または type 属性(type="email" など)では type を指定します。
data-error-xxxx 属性を指定していない場合は、システムのデフォルトのエラーメッセージを表示します。
data-* 属性 | 指定する属性 | 説明 |
---|---|---|
data-error-required | required 属性 | 値がない場合のエラーメッセージ |
data-error-pattern | pattern 属性 | パターンと一致しない場合のエラーメッセージ |
data-error-minlength | minlength 属性 | 最小文字数に満たない場合のエラーメッセージ |
data-error-maxlength | maxlength 属性 | 最大文字数を超えている場合のエラーメッセージ |
data-error-min | min 属性 | 最小値に満たない場合のエラーメッセージ |
data-error-max | max 属性 | 最大値を超えている場合のエラーメッセージ |
data-error-type | type 属性 | 構文に合っていない場合のエラーメッセージ |
data-error-step | step 属性 | step 属性の規則に合致しない場合のエラーメッセージ |
data-error-badinput | 入力値 | 入力値をブラウザーが処理できない場合のエラーメッセージ |
以下が上記サンプルの HTML です。JavaScript は別ファイルとして読み込んでいます。
<body> <div class="content"> <!-- form 要素に class="validationForm" と novalidate 属性を指定 --> <form name="myForm" class="validationForm" novalidate> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" required data-error-required="名前は必須です" class="validate"> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" data-error-required="電話番号は必須です" required class="validate"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required data-error-required="メールアドレスは必須です" minlength="8" class="validate"> </div> <div> <p>色を選択してください</p> <input type="radio" name="color" value="blue" id="blue" required class="validate"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red" class="validate"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green" class="validate"> <label for="green"> 緑 </label> </div> <div> <select name="season" id="season" required class="validate"> <option value="">季節を選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea name="inquiry" id="inquiry" maxlength="100" minlength="10" required rows="3" cols="50" class="validate"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required data-error-required="送信するにはチェックを入れてください" class="validate"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form> </div> <!-- 検証用の JavaScript(myConstraintValidation.js)の読み込み --> <script src="myConstraintValidation.js"></script> </body>
/* エラーメッセージのスタイル */ .error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color: red; box-sizing: border-box; }
JavaScript は前述の data-* 属性でエラーをカスタマイズ とほぼ同じですが、class="validationForm" を指定したフォームを対象にし、DOMContentLoaded イベントで読み込むようにしています。
エラーを表示する span 要素に付与するクラスは error としていますが、11行目で変更可能です。
また、初回の送信前にはエラー表示は行わず、送信時及び送信後に検証エラーを表示するようにしています。送信前の入力時にエラーを表示するには validateAfterFirstSubmit を false に変更します(9行目)。
制約検証 API を使わない方法
制約検証 API を使用せずに JavaScript を使ってフォームを検証することもできます。
組み込みの API(制約検証)ではできない検証を JavaScript を使って実装したり、古いブラウザーに対応するためなどに制約検証 API を使わずに JavaScript を使って独自の検証を実装することができます。
但し、以下のサンプルではアロー関数などの ES6 で追加された機能や addEventListener()、querySelectorAll() などを使用しているので古いブラウザ(IE9以前など)には対応していません。
また、NodeList のメソッド forEach() も IE には対応していません(ポリフィル)。
JavaScript を使った検証は様々な方法があると思いますが、以下は一例です。
どのような検証を実施するのか
仕組みとしては一定の構造を持つ HTML で、対象のコントロール要素(またはその親要素)に制限を意味するクラス属性(検証用クラス)を付与すると、検証用クラスが付与されているコントロール要素に対して、送信時及び値が変更される都度に検証を行います。
検証用クラスは複数の要素に付与される可能性があるので、基本的には document.querySelectorAll() で検証用クラスが付与された要素を全て取得して、それぞれの要素に対して処理を実行します。
制限 | 対象要素 | 指定するクラス | 検証用独自関数名 |
---|---|---|---|
必須(入力) | input(type 属性が text, tel, email) textarea | required | isValueMissing |
必須(選択) | select | required | isValueMissing |
必須(選択) | input(type 属性が checkbox) | requiredcb | isCheckMissing |
必須(選択) | input(type 属性が radio) | requiredrb | isRadioMissing |
パターン | input(type 属性が text, tel, email) textarea | pattern | isPatternMismatch |
最大(最小)文字数 | input(type 属性が text, tel, email) textarea | maxLength、minLength | isTooLong、isTooShort |
制限の種類によっては data-* 属性(カスタムデータ属性)を設定してカスタマイズするようにしています。
タイミングと検証を満たさない場合の動作
検証はフォームが送信される際と、入力または選択内容が変更される都度に検証用関数を実行します。
- 送信時:検証を満たさない場合はエラーメッセージを表示し、送信を中止(submit イベント)
- 変更時:検証を満たさない場合はエラーメッセージを表示(input または change イベント)
検証用関数
検証用の独自関数では、その時点での入力内容(値や選択の有無)を確認して、検証を満たさない場合はエラーメッセージを表示して true を返し、満たす場合はエラーメッセージをクリアして false を返します。
例えば、テキストが入力されているか空かを検証する関数は isValueMissing という名前にしてありますが、値がない場合は value is missing(値がないが真)なので true を返します(ValidityState のプロパティ名を参考に名前をつけましたが、もっと良い名前の付け方があるかと思います)。
検証用クラス名
制限の内容を意味するクラス名は検証属性と同じまたは似たような名前にしています。基本的には検証対象のコントロール要素にクラスを指定しますが、チェックボックス及びラジオボタンの選択を必須にする場合は、それらの親要素に指定するようにしています。
エラーメッセージ
デフォルトでは、検証用の関数ごとに既定のエラーメッセージを用意しますが、検証対象のコントロール要素に data-error-xxxx という独自属性を設定することでエラーメッセージをカスタマイズできるようにします(xxxx は対象の検証用クラス名)。詳細はエラーメッセージを生成する関数
また、エラーメッセージを表示する要素は JavaScript で生成し、エラーがなくなればその要素を削除するようにします。
JavaScript で生成されるエラーメッセージは span 要素で作成し、作成する際に error というクラスの他にどの検証のエラーなのかを特定するため検証用クラスも指定します。
<div> <label for="name">名前: </label> <input class="required" type="text" name="name" id="name"> <!-- 以下が生成されるエラーメッセージを表示する span 要素(必須の場合) --> <span class="error required" aria-live="polite">入力は必須です</span> </div>
また、以下が以降のサンプルでのエラーメッセージのスタイルです。
.error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color:red; box-sizing: border-box; }
HTML の構造
基本的には div 要素の中にコントロール要素と label 要素を配置します。
また、form 要素には novalidate 属性を指定してブラウザーの自動検証を無効にします。
<div> <label for="name">名前: </label> <input class="required" type="text" name="name" id="name"> </div>
テキストの検証
type 属性が text や email、tel などの input 要素や textarea 要素に入力されたテキスト(文字列)を検証するには、その値(value プロパティ)を取得して調べます。
入力を必須に
検証属性 の required のように、その要素に入力がない場合はエラーメッセージを表示して送信を中止することができます。
以下は required クラスを指定したテキストフィールドとテキストエリアの入力を必須とし、値が入力されていない場合や入力された値が空白文字のみの場合(全角スペースは UTF の場合のみ検出)はエラーを表示する例です。
この例では form 要素に novalidate を指定してブラウザーの自動検証を無効にしています。novalidate を指定しない場合、検証属性は指定していませんが、メールアドレスの入力欄では type 属性に email を指定してあるので type 属性による自動検証が実施されます 。
入力を必須とする要素には class="required" を指定します(以下のサンプルでは全てに指定)。
以下ではメールアドレスとお問い合わせ内容には data-error-required を指定してエラーメッセージをカスタマイズしています。
<form name="myForm" novalidate> <div> <label for="name">名前 </label> <input class="required" type="text" name="name" id="name"> </div> <div> <label for="tel">電話番号 </label> <input class="required" type="tel" name="tel" id="tel"> </div> <div> <label for="mail">メールアドレス </label> <input class="required" data-error-required="label" type="email" id="mail" name="mail"> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea class="required" data-error-required="お問い合わせ内容を入力してください" name="inquiry" id="inquiry" rows="3" cols="50"></textarea> </div> <button name="send">送信</button> </form>
required クラスが指定された要素を document.querySelectorAll() で取得し、送信時にはそれらの値が空かどうかを検証して検証を満たさない(値が空の)場合はエラーを表示して送信を中止します。
また、入力された値が変更されるたびに検証を行い、検証を満たさない場合はエラーを表示し、検証を満たせばエラーを削除します。
値を検証及びエラーを表示する関数 isValueMissing では値が空の場合は独自関数 addError() を使ってエラーを表示する要素を生成及び追加し、エラーメッセージを表示して true を返します。値が入力されていれば、エラーを表示する要素(もし存在すれば)を削除して false を返します。
値の前後の空白文字列を trim() で削除し、値が空かどうかは比較演算子(=== )を使い、値の文字列の長さが0かどうかで判定しています。
関数 isValueMissing は select 要素の選択を必須にする検証でも使用するので、対象の要素が select 要素の場合とそれ以外の場合でエラーメッセージの作成方法を分けています。select 要素かどうかは tagName プロパティで判定します(戻り値は大文字)。
required クラスが指定された全ての要素(requiredElems)に対して NodeList のメソッド forEach を使って input イベントを設定して値が変更されたら isValueMissing で検証及びエラーメッセージの表示・非表示を行います。
送信時の処理(submit イベント)では、required クラスが指定された全ての要素を isValueMissing で検証し、値が空(か空白文字のみ)の場合は true が返るので、preventDefault() で送信を中止します。
//required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) //elem :対象の要素 const isValueMissing = (elem) => { //エラーメッセージの要素に追加するクラス名 const className = 'required'; //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //要素の値(value)の前後の空白文字を削除 const elemValue = elem.value.trim(); //値が空の場合はエラーを表示して true を返す if(elemValue.length === 0) { //エラーを表示する span 要素が存在しなければ if(!errorSpan) { //select 要素の場合 if(elem.tagName === 'SELECT') { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '選択してください', 'を選択してください'); //select 要素以外の場合 }else{ //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ //値が空や空白文字のみのでない場合 //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証) requiredElems.forEach( (elem) => { elem.addEventListener('input', () => { //要素の値が変更されたら検証を実行 isValueMissing(elem); }) }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //検証対象の要素を検証し、要件を満たさない場合は送信を中止 requiredElems.forEach( (elem) => { //検証を満たさない(isValueMissing が true を返す)場合は送信中止 if(isValueMissing(elem)) { e.preventDefault(); } }); }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 //elem :対象の要素 //className :エラーメッセージの要素に追加するクラス名 //defaultMessage:デフォルトのエラーメッセージ //labelMessage:label 要素のテキストを使う場合のメッセージ(文字列) const addError = (elem, className, defaultMessage, labelMessage) => { //戻り値として返す変数 errorMessage にデフォルトのエラーメッセージを代入 let errorMessage = defaultMessage; //要素に data-error-xxxx 属性が指定されていれば(xxxx は第2引数の className) if(elem.hasAttribute('data-error-' + className)) { //data-error-xxxx 属性の値を取得 const dataError = elem.getAttribute('data-error-' + className); //data-error-xxxx 属性の値が label であれば if(dataError === 'label') { //label 要素が存在すれば(確認しないと label 要素が存在しない場合エラー) if(elem.parentElement.querySelector('label')) { //label 要素のテキストを取得 const label = elem.parentElement.querySelector('label').textContent; //テキストが空でなければ if(label) { //label 要素のテキストと引数 labelMessage でエラーメッセージを作成 errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) {// data-error-xxxx 属性の値が label 以外の場合 //data-error-xxxx 属性の値をエラーメッセージとする errorMessage = dataError; } } //span 要素を生成 const errorSpan = document.createElement('span'); //error 及び引数に指定されたクラスを追加(設定) errorSpan.classList.add('error', className); //aria-live 属性を設定 errorSpan.setAttribute('aria-live', 'polite'); //引数に指定されたエラーメッセージを設定 errorSpan.textContent = errorMessage; //elem の親要素の子要素として追加 elem.parentNode.appendChild(errorSpan); }
エラーメッセージを生成する関数
61行目からのエラーメッセージを表示する関数 addError() は、以降の全ての検証の関数で使用します。
この関数は検証用のそれぞれの関数の中で呼び出され、以下の引数を受け取ります。
- 第1引数 elem:対象の要素
- 第2引数 className:エラーメッセージの要素に追加するクラス名
- 第3引数 defaultMessage:デフォルトのエラーメッセージ
- 第4引数 labelMessage:label 要素のテキストを使う場合のメッセージ
※ 第2引数の className は検証の種類を特定するためのクラスで、基本的には検証用のクラス名を使いますが、例外もあります(チェックボックスとラジオボタンの場合)。
対象の要素に data-error-xxxx 属性が指定されていれば、その値によりエラーメッセージをカスタマイズします(xxxx 第2引数に指定したクラス名)。
data-error-xxxx 属性にはカスタムエラーメッセージの文字列を指定することができます。また、label を指定すると label 要素のテキストを使ってエラーメッセージを作成します。但し、チェックボックスやラジオボタンの場合は、それぞれの項目のラベルを使うのであまり有用ではありません。
第3引数及び第4引数はそれぞれの検証の内容に合わせて適切なエラーメッセージを生成するようなテキストを指定します。
パターンの検証
JavaScript の正規表現を使って入力された文字列が期待する形式(パターン)に合致するかを検証することができます。
パターンを検証する要素に pattern クラスを指定して、カスタムデータ属性 data-pattern にパターン文字列を指定して検証します。また、よく使うメールアドレスなどのパターンは data-pattern 属性に特定の文字列を指定することで、既定のパターンを使って検証するようにしています。
値 | 適用される正規表現パターン(スクリプト側に記述) |
---|---|
/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ui | |
tel | /^0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}$/ 以下は最初に0以外やカッコやドット、スペースも許容する例 /^\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}$/ |
zip | /^\d{3}-{0,1}\d{4}$/ |
hiragana | /^[\u3041-\u3096\u30FC]+$/ (ひらがな+長音記号) |
katakana | /^[\u30A1-\u30FC]+$/ (カタカナ) |
hankata | /^[\uFF61-\uFF9F]+$/ (半角カタカナ) |
パターン文字列 | 前後のスラッシュは不要(指定すると機能しません)。全体一致でチェックするため、先頭と末尾に^と$をつける必要はありません(検証属性の pattern 属性と同じ)。 |
※使用するパターンはサーバー側の検証に合わせて適宜変更します。
基本的には data-pattern 属性には検証する正規表現のパターン文字列を指定しますが、例えば data-pattern 属性に email や tel などの登録してある文字列を指定すればスクリプト側で用意したその文字列に対応するパターンを使って検証するようにしています。他にもよく使うパターンがあればスクリプト側に追加することで、data-pattern 属性にパターン文字列を指定しなくてもすみます。
以下の例では、電話番号1とメールアドレス1、郵便番号は pattern に加えて required クラスを指定して必須にして、data-pattern 属性にパターン文字列を指定しています。電話番号2とメールアドレス2では、data-pattern 属性に tel や email を指定して用意されているパターンで検証するようにしています。
また、電話番号2と郵便番号には data-error-pattern 属性に label を指定して、ラベルを使ったエラーを表示し、メールアドレス2 には data-error-pattern 属性にカスタムメッセージを指定してその値を表示するようにしています。
この例でも form 要素に novalidate を指定してブラウザーの自動検証を無効にしています。
<form name="myForm" novalidate> <div> <label for="tel1">電話番号1 </label> <input class="required pattern" data-pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" type="tel" name="tel1" id="tel1"> </div> <div> <label for="tel2">電話番号2 </label> <input class="pattern" data-pattern="tel" data-error-pattern="label" type="tel" name="tel2" id="tel2"> </div> <div> <label for="mail1">メールアドレス1 </label> <input class="required pattern" data-pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" type="email" id="mail1" name="mail1"> </div> <div> <label for="mail2">メールアドレス2 </label> <input class="required pattern" data-pattern="email" data-error-pattern="メールアドレスには @ やドメイン名が必要です" type="email" id="mail2" name="mail2"> </div> <div> <label for="zipcode">郵便番号 </label> <input class="pattern required" data-pattern="zip" data-error-pattern="label" type="tel" name="zipcode" id="zipcode"> </div> <button name="send">送信</button> </form>
基本的には入力を必須にする検証と同じで、検証用の関数を定義して送信時及び値が変更される都度に対象の要素を検証します。
パターンにマッチしているかを検証する関数 isPatternMismatch では、その要素の data-pattern 属性に指定されたパターン文字列から正規表現(オブジェクト)を生成します。文字列を正規表現にするには、正規表現オブジェクトのコンストラクタ関数(new RegExp)を使用します。
data-pattern 属性に email や tel などの文字列を指定すると対応する正規表現パターンで検証を行うようにします。正規表現パターンは後から変更したり、追加しやすいように先頭の方で定義しておきます。
この例では Map を使って data-pattern 属性に指定できる文字列とそれに対応するパターンを1つのエントリーとして patternMap に追加しています。必要に応じて、パターンを定義して、data-pattern 属性に指定する文字列を patternMap に追加すれば、その文字列を指定してパターンの検証ができるようになります(パターンの追加)。
関数 isPatternMismatch では、その要素の値が空ではない場合にのみ検証します。空の値に対してパターンを検証するとマッチしないため、この関数はエラーメッセージを生成して true を返します。そのため、値が空の場合に検証すると、必須でない入力は常にエラーメッセージが表示され送信できなくなります。
値がパターンにマッチするかは、正規表現の test() メソッドを使います。test() メソッドは、引数に指定されて文字列が正規表現とマッチすれば true を返し、マッチしなければ false を返します。
また、値が空で既にエラーメッセージを表示する要素が生成されている場合は、エラーメッセージの要素を削除してエラーをクリアします(途中まで入力してから値を削除した場合などエラーが残らないように)。
//pattern クラスを指定された要素の集まり const patternElems = document.querySelectorAll('.pattern'); //pattern 検証で data-pattern="email" を指定した場合に使用する正規表現パターン const emailRegExp = /^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ui; //pattern 検証で data-pattern="tel" を指定した場合に使用する正規表現パターン const telRegExp = /^0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}$/; //pattern 検証で data-pattern="zip" を指定した場合に使用する正規表現パターン const zipRegExp = /^\d{3}-{0,1}\d{4}$/; //pattern 検証で data-pattern="hiragana" を指定した場合に使用する正規表現パターン const hiraganaRegExp = /^[\u3041-\u3096\u30FC]+$/; //ひらがな+長音記号 //pattern 検証で data-pattern="katakana" を指定した場合に使用する正規表現パターン const katakanaRegExp = /^[\u30A1-\u30FC]+$/; //カタカナ //pattern 検証で data-pattern="hankata" を指定した場合に使用する正規表現パターン const hankataRegExp = /^[\uFF61-\uFF9F]+$/; //半角カタカナ //上記で定義した正規表現パターンと data-pattern 属性に指定できる文字列のマップ //各エントリーは ['data-pattern 属性に指定する文字列', 正規表現パターン] const patternMap = new Map([ ['email', emailRegExp], ['tel', telRegExp], ['zip', zipRegExp], ['hiragana', hiraganaRegExp], ['katakana', katakanaRegExp], ['hankata', hankataRegExp], ]); //指定されたパターンにマッチしているかを検証する関数(マッチしていない場合は true を返す) const isPatternMismatch = (elem) => { //対象のクラス名 const className = 'pattern'; //対象の data-xxxx 属性の名前 const attributeName = 'data-' + className; //検証するパターン(正規表現)を生成 let pattern = new RegExp('^' + elem.getAttribute(attributeName) + '$'); //先頭の方で定義されている data-pattern 属性に指定できる文字列と pattern のマップ(patternMap)から検証するパターンを設定 patternMap.forEach((value, key) => { if(elem.getAttribute(attributeName) === key) { pattern = value; } }); //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //要素の値(value)の前後の空白文字を削除 const elemValue = elem.value.trim(); //値が空でなければ if(elemValue !=='') { //値がパターンにマッチするかを test() メソッドで判定 if(!pattern.test(elemValue)) { //エラーを表示する span 要素が存在しなければ if(!errorSpan) { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '入力された値が正しくないようです', 'の形式が正しくないようです'); } return true; //マッチしない場合は true を返す }else{ //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } //値が空でエラーを表示する要素が存在すれば削除 }else if(elemValue ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } //pattern クラスを指定された要素に input イベントを設定(値が変更される都度に検証) patternElems.forEach( (elem) => { elem.addEventListener('input', () => { //要素の値が変更されたら検証を実行 isPatternMismatch(elem); }) }); //送信時の処理(検証対象の要素を検証し、要件を満たさない場合は送信を中止) document.myForm.addEventListener('submit', (e) => { //必須の検証 requiredElems.forEach( (elem) => { if(isValueMissing(elem)) { e.preventDefault(); } }); //パターンの検証 patternElems.forEach( (elem) => { if(isPatternMismatch(elem)) { e.preventDefault(); } }); }); /********* 以降は前述の必須の検証と同じ内容 *********/ //required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) const isValueMissing = (elem) => { const className = 'required'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const elemValue = elem.value.trim(); if(elemValue.length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, '選択してください', 'を選択してください'); }else{ addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証) requiredElems.forEach( (elem) => { elem.addEventListener('input', () => { isValueMissing(elem); }) }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
パターンの追加
以下は検証に使用する正規表現パターンを追加する例です。
正規表現パターンを定義して追加します(7行目)。
data-pattern 属性に指定する文字列('username')と定義した正規表現パターン(usernameRegExp)をマップに追加します(17行目)。
//正規表現パターンの定義の部分 const emailRegExp = /^([a-z0-9\+_\-]+)...中略...+[a-z]{2,6}$/ui; ・・・中略・・・ const hankataRegExp = /^[\uFF61-\uFF9F]+$/; //新たに検証に使用する正規表現パターンを追加 const usernameRegExp = /^[a-zA-Z0-9!@#$%&]{8,15}$/; //data-pattern 属性に指定する文字列と上記で定義した正規表現パターンを追加 const patternMap = new Map([ ['email', emailRegExp], ['tel', telRegExp], ['zip', zipRegExp], ['hiragana', hiraganaRegExp], ['katakana', katakanaRegExp], ['hankata', hankataRegExp], ['username', usernameRegExp], //新たにエントリーを追加 ]);
HTML では検証する要素に data-pattern="username" を追加します。
<div> <label for="user">ユーザー名 </label> <input class="pattern" data-pattern="username" type="text" id="user" name="user"> </div>
文字数の制限
文字数の制限は前項のパターンの検証を使っても可能ですが、以下では指定されたクラス名により最小及び最大文字数の検証を実装する例です。
最小文字数を検証する要素には minlength クラスを指定し、data-minlength 属性に最小文字数を、最大文字数を検証するには maxlength クラスを指定し、data-maxlength 属性に最大文字数を指定します。
data-* 属性(カスタムデータ属性)の * の部分は大文字は使えず小文字を使う必要があります。
オプションで、maxlength クラスと data-maxlength 属性を指定した要素に追加のクラス showCount を指定すると入力された文字数と data-maxlength 属性に指定した最大文字数を表示します。
この例では以下のような検証を指定しています。
- 名前:minlength クラスと data-minlength 属性を指定して最小文字数を指定(required も指定)
- ユーザー名:pattern クラスと data-pattern 属性を指定し、パターン検証で半角英数字(3文字〜10文字)を指定
- お問い合わせ内容:maxlength クラスと data-maxlength 属性を指定して最題文字数を指定。オプションで showCount クラスを指定して入力文字数を表示(required も指定)
<form name="myForm" novalidate> <div> <label for="name">名前: </label> <input class="required minlength" data-minlength="4" data-error-minlength="label" type="text" name="name" id="name"> </div> <div> <label for="user">ユーザー名: </label> <input class="pattern" data-pattern="[a-zA-Z0-9]{3,10}" data-error-pattern="半角英数字(3〜10文字)で入力ください" type="text" name="user" id="user"> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea class="required maxlength showCount" data-maxlength="100" name="inquiry" id="inquiry" rows="4" cols="50"></textarea> </div> <button name="send">送信</button> </form>
検証する関数では入力された値(value)の文字数を取得して比較します。
内容的にはパターン検証とほぼ同じで、パターンを検証する代わりに文字数を検証します。
この例では、入力された値の文字数は、絵文字などの4バイト文字も1文字としてカウントするようにしています(文字列を分割・カウント)。
data-maxlength 属性を指定した要素に showCount クラスが指定されていれば data-maxlength 属性から最大文字数を取得し、その値が数値であれば入力文字数を表示する p 要素を生成しコンテンツに指定した span 要素に input イベントで取得した文字数(カウント)を出力します。そして入力文字数が最大文字数を超えればカウントの文字色を赤に変更し、最大文字数より小さくなれば文字色を戻します(または span 要素にスタイル設定用の overMaxCount クラスを追加することもできます)。
スタイル用に入力文字数を表示する p 要素に countSpanWrapper クラスを指定しています。
値が同じかの検証
2つの input 要素に入力された値が同じかどうかを検証する例です。
検証する input 要素の2つ目の要素に equal-to クラスを指定し、data-equal-to 属性に比較対象の要素の id の値を指定します。
デフォルトのエラーメッセージは「入力された値が異なります」ですが、data-error-equal-to 属性に独自のエラーメッセージを指定することができます。
この例では以下のような検証を指定しています。
- メールアドレス:pattern クラスと data-pattern 属性を指定して正しい形式のメールアドレスかを検証。required も指定して必須に。
- メールアドレス 確認用:equal-to クラスと data-equal-to 属性に mail1(1つ目のメールアドレスの id 属性)を指定して2つのメールアドレスが一致するかを検証。また、data-error-equal-to を指定して値が一致しない場合のエラーメッセージを指定。required も指定して必須に。
<form name="myForm" novalidate> <div> <label for="mail1">メールアドレス(必須) </label> <input class="required pattern" data-pattern="email" type="email" id="mail1" name="mail1" size="30"> </div> <div> <label for="mail2">メールアドレス 確認用(必須)</label> <input class="required equal-to" data-equal-to="mail1" data-error-equal-to="入力されたメールアドレスが異なります" type="email" id="mail2" name="mail2" size="30"> </div> <button name="send">送信</button> </form>
検証する関数 isNotEqualTo では data-equal-to 属性に指定された id の要素を取得して、その値と入力された値を比較して値が異なればエラーメッセージを表示します。
また、data-equal-to 属性に指定された id の要素にも input イベントを設定し、比較の対象の入力値が変更された場合も値が一致するかを検証するようにしています(36〜43行目)。
パスワードの表示・非表示
type="password" を指定したパスワード入力欄では入力されたパスワードは伏せ字になっているので、表示・非表示を切り替えられるようにする例です。
以下は前述の例と同様、入力された2つのパスワードの値が同じかどうかや値(英数字7文字以上10文字以内)も検証します。
前述の例と同様、検証する input 要素の2つ目の要素に equal-to クラスを指定し、data-equal-to 属性に比較対象の要素の id の値を指定します。
また、form 要素には autocomplete="off" を、input 要素には autocomplete="new-password" を指定してフォームの自動補完や自動入力を抑止するようにしていますが、ブラウザーはキャッシュされたログイン情報の使用や保存、更新を尋ねてきてしまいます(MDN フォームの自動補完を無効にするには)。
この例ではパスワード入力欄の右側の「表示」のチェックボックスにチェックを入れるとパスワードの入力文字を表示します。
<form name="myForm" autocomplete="off" novalidate> <div> <label for="pw1">パスワード(必須) </label> <input class="required pattern" type="password" data-pattern="[a-zA-Z0-9!@#$%&*]{7,10}" id="pw1" name="pw1" size="30" autocomplete="new-password"> <input type="checkbox" class="toggle-pw" id="toggle-pw1"> <label for="toggle-pw1"> 表示</label> </div> <div> <label for="pw2">パスワード 確認用(必須)</label> <input class="required equal-to" data-equal-to="pw1" data-error-pattern="パスワードが一致しません" type="password" id="pw2" name="pw2" size="30" autocomplete="new-password"> <input type="checkbox" class="toggle-pw" id="toggle-pw2"> <label for="toggle-pw2"> 表示</label> </div> </form>
以下はチェックボックスにチェックを入れるとパスワード入力欄の type 属性を text に変更して、入力文字を表示する JavaScript の例です。
//チェックボックスの要素を取得 const togglePW1 = document.getElementById('toggle-pw1'); //パスワード入力欄の要素を取得 const pwElem1 = document.getElementById('pw1'); //チェックボックスの要素に change イベントを設定 togglePW1.addEventListener('change', (e) => { if( e.currentTarget.checked ) { //チェックされればパスワード入力欄の type 属性を text に pwElem1.setAttribute('type', 'text'); }else{ //チェックが外れればパスワード入力欄の type 属性を password に pwElem1.setAttribute('type', 'password'); } }); //チェックボックスの要素を取得(上記と同じ) const togglePW2 = document.getElementById('toggle-pw2'); const pwElem2 = document.getElementById('pw2'); togglePW2.addEventListener('change', (e) => { if( e.currentTarget.checked ) { pwElem2.setAttribute('type', 'text'); }else{ pwElem2.setAttribute('type', 'password'); } });
上記の例では getElementById() で個々のチェックボックスの要素を取得していますが、この例の場合、以下のように記述することもできます。
また、この例では以下のような検証も指定しています。
- パスワード:pattern クラスと data-pattern 属性を指定して入力値が7文字以上10文字以内の半角英数字及び !@#$% のいずれかであることを検証。required も指定して必須に。
- パスワード 確認用:equal-to クラスと data-equal-to 属性に pw1(1つ目のパスワードの id 属性)を指定して2つのパスワードが一致するかを検証。また、data-error-equal-to を指定して値が一致しない場合のエラーメッセージを指定。required も指定して必須に。
セレクトボックス
select 要素の value プロパティは選択されている option 要素があれば、選択されている最初の option 要素の value プロパティの値を返し、選択されている option 要素がなければ、空文字列を返します。
そのため、セレクトボックスの値が選択されていることを必須にする検証は、テキストの入力を必須にする検証で対応可能です。
但し、セレクトボックスには以下のような特徴があります。
単一選択型(プルダウンリスト)のセレクトボックスの場合、option 要素に selected 属性を指定していない場合は、最初の option 要素が選択状態になります。
複数選択型のリストボックスの場合は、option 要素に selected 属性を指定していなければ選択状態にはなりません。
関連項目:セレクトボックス
例えば、以下の単一選択型のセレクトボックスの場合、初期状態(ユーザーが何も操作していない状態)では最初の option 要素の New York が選択されていることになり、select 要素の value プロパティの値は newyork になります。
<select> <option value="newyork">New York</option> <option value="tokyo">Tokyo</option> <option value="paris">Paris</option> <option value="london">London</option> </select>
単一選択型のセレクトボックスで初期状態でどの項目も選択されていない状態にする1つの方法は、以下のように最初の option 要素の value 属性の値を空("")にします。
<select> <option value="">選択してください</option><!-- 最初の option の value を ""(空)に --> <option value="newyork">New York</option> <option value="tokyo">Tokyo</option> <option value="paris">Paris</option> <option value="london">London</option> </select>
入力を必須にする検証で定義した関数 isValueMissing をそのまま使うことができます。
以下は関数 isValueMissing の記述です。select 要素かどうかは tagName プロパティの値(SELECT)で判定してエラーメッセージの作成方法を分岐しています。
//値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) const isValueMissing = (elem) => { //エラーメッセージの要素に追加するクラス名 const className = 'required'; //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //要素の値(value)の前後の空白文字を削除 const elemValue = elem.value.trim(); //値が空の場合はエラーを表示して true を返す if(elemValue.length === 0) { //エラーを表示する span 要素が存在しなければ if(!errorSpan) { //select 要素の場合 if(elem.tagName === 'SELECT') { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '選択してください', 'を選択してください'); //select 要素以外の場合 }else{ //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ //値が空や空白文字のみのでない場合 //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }
以下は select 要素に required クラスを指定して、選択を必須にする例です。最後の名前の入力欄は select 要素以外の場合のエラーメッセージを比較・確認するために追加しています。
何も選択しない状態(初期状態)で送信ボタンをクリックすると、最初の3つのセレクトボックスはエラーメッセージが表示されます。
最初の2つの単一選択型セレクトボックスでは初期状態で選択状態になる option 要素の value 属性を空("")にしていて、3つ目の複数選択型セレクトボックスの場合は、option 要素に selected 属性を指定していないので、これらは何も選択されていないと判定されます。
但し、4つ目の単一選択型セレクトボックスでは初期状態で選択状態になる最初の option 要素の value 属性の値(manhattan)は空ではないので選択されていると判定されます。
<form name="myForm" novalidate> <div> <label for="season">四季</label> <select class="required" data-error-required="季節を選択してください" name="season" id="season"> <!-- 初期状態で選択状態になる option の value を ""(空)に --> <option value="">選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="color">色</label> <select class="required" data-error-required="label" name="color" id="color"> <option value="red">Red</option> <option value="blue">Blue</option> <option value="green">Green</option> <option value="yellow">Yellow</option> <!-- 初期状態で選択状態になる option の value を ""(空)に --> <option value="" selected>特になし</option> </select> </div> <div> <select class="required" name="hobby" id="hobby" size="4"> <option value="sport">スポーツ</option> <option value="music">音楽</option> <option value="reading">読書</option> <option value="walking">散歩</option> </select> </div> <div> <label for="borough">地区</label> <select class="required" name="borough" id="borough"> <option value="manhattan">Manhattan</option> <option value="brooklyn">Brooklyn</option> <option value="queens">Queens</option> <option value="bronx">Bronx</option> <option value="staten">Staten Island</option> </select> </div> <div> <label for="name">名前 </label> <input class="required" type="text" name="name" id="name"> </div> <button name="send">送信</button> </form>
//required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) const isValueMissing = (elem) => { const className = 'required'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const elemValue = elem.value.trim(); if(elemValue.length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, '選択してください', 'を選択してください'); }else{ addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証) requiredElems.forEach( (elem) => { elem.addEventListener('input', () => { isValueMissing(elem); }) }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //必須の検証 requiredElems.forEach( (elem) => { if(isValueMissing(elem)) { e.preventDefault(); } }); }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
チェックボックス
複数項目があるチェックボックスのいずれかをチェックすることを必須とする検証を JavaScript を使って実装する例です(複数項目のチェックボックス)。
以下のフォームでは、チェックボックスの項目がチェックされていない状態で送信したり、チェックした後に解除していずれもチェックされていない場合はエラーメッセージを表示します。
グループ内の項目の少なくとも1つをチェックすると送信できるようになります。
チェックボックスの場合、いずれかの項目の選択を必須にするには同じグループの先頭(最初)のチェックボックスの要素に required というクラスを指定します。
カスタムエラーメッセージを指定する場合は、先頭または全てのチェックボックスの要素に data-error-required-checkbox 属性を指定します。先頭のみに data-error-required-checkbox 属性を指定した場合は、その他の項目のチェックを外した際のエラーはデフォルトのエラーが表示されます。
また、チェックボックスの場合、data-error-required-checkbox 属性に label を指定するとその項目のラベルが使われるので、チェックボックスが1つの場合以外は使えません。
<form name="myForm" novalidate> <div> <p>連絡方法を選択してください(複数選択可)</p> <input class="required" type="checkbox" name="contact[]" id="byEmail" value="Email"> <label for="byEmail"> メール</label> <input type="checkbox" name="contact[]" id="byTel" value="Telephone"> <label for="byTel"> 電話</label> <input type="checkbox" name="contact[]" id="byMail" value="Mail"> <label for="byMail"> 郵便 </label> </div> <div> <p>興味のある項目を選択してください(複数選択可)</p> <input class="required" data-error-required-checkbox="いずれかを選択してください" type="checkbox" name="hobby[]" id="sports" value="Sports"> <label for="sports"> スポーツ</label> <input type="checkbox" name="hobby[]" id="music" value="Music"> <label for="music"> 音楽</label> <input type="checkbox" name="hobby[]" id="book" value="Book"> <label for="book"> 読書 </label> </div> <div> <input class="required" data-error-required-checkbox="label" type="checkbox" name="agreement" id="agreement" value="agree"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
テキストフィールドやテキストエリア、セレクトボックスを必須とする検証を行う関数 isValueMissing() にチェックボックス用の検証を追加します。
最初にタグ名と type 属性の値を調べてその要素がチェックボックスの場合、9〜29行目が実行されます(それ以降は今までの isValueMissing() と同じです)。
テキストフィールドやテキストエリア、セレクトボックスの場合、エラーメッセージの要素に追加するクラス名は required ですが、チェックボックスの場合は required-checkbox としています。
親要素のメソッドとして querySelector() の引数に :checked 擬似クラスを使ったセレクタを指定して選択されている先頭のチェックボックス要素を取得して変数 checkedCheckbox に代入しています。
変数 checkedCheckbox の値が null であれば、いずれのチェックボックスも選択されていないので(すでにエラーメッセージが存在しなければ)エラーメッセージを生成して true を返します。
変数 checkedCheckbox の値が null でなければ、いずれかのチェックボックスが選択されているので、すでにエラーメッセージが存在していれば削除して false を返します。
そのグループの全てのチェックボックスの選択状態をチェックするため、チェックボックスの親要素を基点に全てのチェックボックス要素を取得し、取得した全てのチェックボックス要素に change イベントを設定しています(66〜73行目)。
//required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) //elem :対象の要素 const isValueMissing = (elem) => { //その要素がチェックボックスの場合 if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') { //エラーメッセージの要素に追加するクラス名(data-error-xxxx の xxxx) const className = 'required-checkbox'; //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //選択状態の最初のチェックボックス要素を取得 const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked'); //選択状態のチェックボックス要素を取得できない場合 if(checkedCheckbox === null) { if(!errorSpan) { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; }else{ //いずれかのチェックボックスが選択されている場合 //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else{ //エラーメッセージの要素に追加するクラス名(data-error-xxxx の xxxx) const className = 'required'; //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //要素の値(value)の前後の空白文字を削除 const elemValue = elem.value.trim(); //値が空の場合はエラーを表示して true を返す if(elemValue.length === 0) { //エラーを表示する span 要素が存在しなければ if(!errorSpan) { //select 要素の場合 if(elem.tagName === 'SELECT') { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '選択してください', 'を選択してください'); //select 要素以外の場合 }else{ //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ //値が空や空白文字のみのでない場合 //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } //required クラスを指定された要素に input/change イベントを設定 requiredElems.forEach( (elem) => { //チェックボックスの場合 if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox' ){ //親要素を基点に全てのチェックボックス要素を取得 const elems = elem.parentElement.querySelectorAll(elem.tagName); //取得した全てのチェックボックス要素に change イベントを設定 elems.forEach( (elemsChild) => { elemsChild.addEventListener('change', () => { //それぞれのチェックボックス要素の選択状態が変更されたら検証を実行 isValueMissing(elemsChild); }); }); }else{ elem.addEventListener('input', () => { //要素の値が変更されたら検証を実行 isValueMissing(elem); }); } }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //検証対象の要素を検証し、要件を満たさない場合は送信を中止 requiredElems.forEach( (elem) => { //検証を満たさない(isValueMissing が true を返す)場合は送信中止 if(isValueMissing(elem)) { e.preventDefault(); } }); }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
関連項目:チェックボックス
チェックすると要素を表示
以下は「その他」のチェックボックスをチェックすると、テキストフィールドの入力欄を表示する例です。
前述の例と同様、チェックボックスの先頭の要素に .required を指定したチェックボックスでは項目を少なくとも1つ選択することを必須としています(以下のサンプルでは全てのチェックボックスの先頭の要素に .required を指定しています)。
また、チェックボックスをチェックした際に表示されるテキストフィールドにも .required を指定すれば必須入力とできます。以下の例では最初の「その他」のテキストフィールドは必須にしています。
仕組みとしては .toggler を指定したチェックボックスをチェックすると、その要素の data-togglerTarget 属性で指定された値の id 属性を持つ input 要素を表示し、チェックを外すと非表示にします。
言い換えると、.toggler を指定したチェックボックスにはチェックされた際に表示する input 要素の id を data-togglerTarge に指定する必要があります。
<form name="myForm" novalidate> <div> <p>連絡方法を選択してください(複数選択可)</p> <input class="required" type="checkbox" name="contact[]" id="byEmail" value="Email"> <label for="byEmail"> メール</label> <input type="checkbox" name="contact[]" id="byTel" value="Telephone"> <label for="byTel"> 電話</label> <input type="checkbox" name="contact[]" id="byMail" value="Mail"> <label for="byMail"> 郵便 </label> <input type="checkbox" name="contact[]" id="other1" value="Other1" class="toggler" data-togglerTarget="otherMethod"> <label for="other1"> その他 </label> <div> <label for="otherMethod">その他</label> <input type="text" name="otherMethod" id="otherMethod" class="required"> </div> </div> <div> <p>興味のある項目を選択してください(複数選択可)</p> <input class="required" type="checkbox" name="hobby[]" id="sports" value="Sports"> <label for="sports"> スポーツ</label> <input type="checkbox" name="hobby[]" id="music" value="Music"> <label for="music"> 音楽</label> <input type="checkbox" name="hobby[]" id="book" value="Book"> <label for="book"> 読書 </label> <input type="checkbox" name="hobby[]" id="other2" value="Other2" class="toggler" data-togglerTarget="otherHobby"> <label for="other2"> その他 </label> <div> <label for="otherHobby">その他</label> <input type="text" name="otherHobby" id="otherHobby"> </div> </div> <div> <input class="required" data-error-required-checkbox="送信するには同意が必要です" type="checkbox" name="agreement" id="agreement" value="agree"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form>
toggler クラスを指定した要素を全て取得して変数 togglers に格納し、チェックボックスをチェックした際に表示される要素を格納する配列 togglerTargets を用意します。
togglers の各要素の data-togglerTarget 属性の値から表示対象の要素を取得してその要素の親要素を初期状態で非表示にして、各要素を配列 togglerTargets に追加します(7〜14行目)。
toggleTarget() はチェックボックスの選択状態により、対象の input 要素(の親要素)を表示・非表示にする関数で、.toggler を指定したチェックボックスの change イベントのリスナーに指定します。
また、toggleTarget() はラジオボタンでも機能するように、ラジオボタンの場合は、ラジオボタンの各要素に change イベントのリスナーを登録して、変更があった際に toggler クラスを指定された要素以外が選択された場合は、対象の input 要素(の親要素)を非表示にするようにしています(ラジオボタンの場合、その要素の選択が解除されたという change イベントが発生しないため)。
送信時の処理(submit イベント)では、.required を指定した要素の検証で、その親要素が非表示になっているものは検証の対象から外しています(チェックボックスにチェックを入れて表示される input 要素に required が指定されていても非表示の場合は検証しないようにしています:65行目)。
また、togglerTargets の各要素(チェックボックスをチェックした際に表示される要素)の親要素が非表示になっている場合は、その要素の値を空にして非表示になっているテキストフィールドの値を送信しないようにしています(70〜76行目)。
//チェックすると input 要素(テキストフィールド)を表示するチェックボックス要素の集まり const togglers = document.querySelectorAll('.toggler'); //チェックボックスをチェックした際に表示される要素を格納する配列 const togglerTargets = []; //.toggler を指定したチェックボックスの各要素について実行 togglers.forEach((elem) => { //.togglerを指定した要素により表示される要素(data-togglerTarget属性に指定されている要素) const togglerTarget = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); //初期状態ではその親要素を非表示に togglerTarget.parentElement.style.setProperty('display', 'none'); //配列 togglerTargets に追加 togglerTargets.push(togglerTarget); }); //.toggler を指定した要素を引数に取り、選択状態により親要素を表示・非表示にする関数 const toggleTarget = (elem) => { //data-togglerTarget 属性に指定されている要素を取得 const target = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); //チェックボックスの場合 if(elem.type === 'checkbox') { if(elem.checked === true) { //チェックされれば target の親要素を表示 target.parentElement.style.removeProperty('display'); }else{ //チェックが外されたら target の親要素を非表示に target.parentElement.style.setProperty('display', 'none'); } //ラジオボタンの場合 }else if(elem.type === 'radio') { //チェックされれば target の親要素を表示 target.parentElement.style.removeProperty('display'); //同じ親要素を持つラジオボタンを取得 const radios = elem.parentElement.querySelectorAll('[type="radio"]'); //同じ親要素を持つラジオボタンにイベントリスナーを登録 radios.forEach((elem) => { //選択状態が代わったら elem.addEventListener('change', (e) => { // toggler クラスが指定されていなければ if(elem.className !== 'toggler') { //target の親要素が非表示でなければ if(target.parentElement.style.getPropertyValue('display') !== 'none') { //target の親要素を非表示に target.parentElement.style.setProperty('display', 'none'); } } }, {once: true}); //リスナーの呼び出しを一回のみとする }); } } //.toggler を指定した各要素に change イベントのリスナーを登録(上記関数を指定) togglers.forEach((elem) => { elem.addEventListener('change', (e) => { //e.currentTarget はイベントを登録した要素(elem:.toggler を指定した要素) toggleTarget(e.currentTarget); //または toggleTarget(elem); }); }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //必須の検証 requiredElems.forEach( (elem) => { //★★★ その要素の親要素が非表示(display:none)の場合は対象外 ★★★ if(isValueMissing(elem) && elem.parentElement.style.getPropertyValue('display') !=='none') { e.preventDefault(); } }); //input 要素の親要素が非表示であればその値は送らない togglerTargets.forEach( (elem) => { //チェックボックスにチェックを入れて表示される input 要素の親要素が非表示であれば if(elem.parentElement.style.getPropertyValue('display') ==='none') { //値があればクリア(送信時にチェックされていない場合は値をサーバーに送らない) elem.value = ''; } }); }); /********* 以降は前述の検証と同じ内容 *********/ //required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) const isValueMissing = (elem) => { if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') { const className = 'required-checkbox'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked'); if(checkedCheckbox === null) { if(!errorSpan) { addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else{ const className = 'required'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const elemValue = elem.value.trim(); if(elemValue.length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, '選択してください', 'を選択してください'); }else{ addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } //required クラスを指定された要素に input/change イベントを設定 requiredElems.forEach( (elem) => { if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox' ){ const elems = elem.parentElement.querySelectorAll(elem.tagName); elems.forEach( (elemsChild) => { elemsChild.addEventListener('change', () => { isValueMissing(elemsChild); }); }); }else{ elem.addEventListener('input', () => { isValueMissing(elem); }); } }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
ラジオボタン
ラジオボタンの選択を必須にするには、同じ name 属性の input 要素(type 属性が radio)のどれか1つに検証属性の required 属性を設定するのが簡単ですが、独自の JavaScript で検証する場合は選択状態のラジオボタンがあるかどうかを調べます。
関連項目:選択されたラジオボタンを取得
以下はラジオボタンの選択を必須とする検証を JavaScript を使って実装する例です(チェックボックスの場合とほぼ同じです)。
チェックボックスの場合と同様、同じグループの先頭(最初)のラジオボタンの要素に required というクラスを指定指定すると、そのグループのラジオボタンの選択を必須にします。
カスタムエラーメッセージを指定する場合は、先頭のラジオボタンの要素に data-error-required-radio 属性を指定します。
また、ラジオボタンの場合、data-error-required-radio 属性に label を指定するとその項目のラベルが使われるのであまり役に立ちません。
<form name="myForm" novalidate> <div> <p>色を選択してください</p> <input class="required" type="radio" name="color" value="Blue" id="blue"> <label for="blue"> 青 </label> <input type="radio" name="color" value="Red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="Green" id="green"> <label for="green"> 緑 </label> </div> <div> <p>サイズを選択してください</p> <input class="required" data-error-required-radio="サイズを1つお選びください" type="radio" name="size" value="Small" id="small"> <label for="small"> スモール </label> <input type="radio" name="size" value="Medium" id="medium"> <label for="medium"> ミディアム </label> <input type="radio" name="size" value="Large" id="large"> <label for="large"> ラージ </label> </div> <button name="send">送信</button> </form>
チェックボックスの検証で作成した関数 isValueMissing() にラジオボタン用の検証を追加します。内容的にはチェックボックスの場合とほぼ同じです。
ラジオボタンの親要素のメソッドとして querySelector() の引数に :checked 擬似クラスを使ったセレクタを指定して選択されているラジオボタン要素を取得して変数 checkedRadio に代入しています。
変数 checkedRadio の値が null であれば、ラジオボタンは選択されていないので(すでにエラーメッセージが存在しなければ)エラーメッセージを生成して true を返します。
変数 checkedRadio の値が null でなければ、ラジオボタンが選択されているので、すでにエラーメッセージが存在していれば削除して false を返します。
そのグループの全てのラジオボタンの選択状態をチェックするため、チェックボックスの親要素を基点に全てのラジオボタン要素を取得し、取得した全てのラジオボタン要素に change イベントを設定しています(71〜79行目)。
//required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) //elem :対象の要素 const isValueMissing = (elem) => { //ラジオボタンの場合 if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'radio') { //エラーメッセージの要素に追加するクラス名(data-error-xxxx の xxxx) const className = 'required-radio'; //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る) const errorSpan = elem.parentElement.querySelector('.error.' + className); //選択状態の最初のラジオボタン要素を取得 const checkedRadio = elem.parentElement.querySelector('input[type="radio"]:checked'); //選択状態のラジオボタン要素を取得できない場合 if(checkedRadio === null) { if(!errorSpan) { //addError() を使ってエラーメッセージ表示する span 要素を生成して追加 addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; } else{ //いずれかのラジオボタンが選択されている場合 //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') { //チェックボックスの場合 const className = 'required-checkbox'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked'); if(checkedCheckbox === null) { if(!errorSpan) { addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else{ //テキストフィールドやテキストエリア、セレクトボックスの場合 const className = 'required'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const elemValue = elem.value.trim(); if(elemValue.length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, '選択してください', 'を選択してください'); }else{ addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証) requiredElems.forEach( (elem) => { //ラジオボタンまたはチェックボックスの場合 if(elem.tagName === 'INPUT' && (elem.getAttribute('type') === 'radio' || elem.getAttribute('type') === 'checkbox' )){ //親要素を基点に全てのラジオボタンまたはチェックボックス要素を取得 const elems = elem.parentElement.querySelectorAll(elem.tagName); //取得した全ての要素に change イベントを設定 elems.forEach( (elemsChild) => { elemsChild.addEventListener('change', () => { //それぞれの要素の選択状態が変更されたら検証を実行 isValueMissing(elemsChild); }); }); }else{ elem.addEventListener('input', () => { //要素の値が変更されたら検証を実行 isValueMissing(elem); }); } }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //検証対象の要素を検証し、要件を満たさない場合は送信を中止 requiredElems.forEach( (elem) => { //検証を満たさない(isValueMissing が true を返す)場合は送信中止 if(isValueMissing(elem)) { e.preventDefault(); } }); }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
選択すると要素を表示
以下は「その他」のラジオボタンを選択すると、テキストフィールドの入力欄を表示する例です。
チェックボックスの場合と同様、 .toggler を指定したラジオボタンを選択すると、その要素の data-togglerTarget 属性で指定された値の id 属性を持つ input 要素を表示し、他のラジオボタンが選択されると非表示にします。
<form name="myForm" novalidate> <div> <p>色を選択してください</p> <input class="required" type="radio" name="color" value="Blue" id="blue"> <label for="blue"> 青 </label> <input type="radio" name="color" value="Red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="Green" id="green"> <label for="green"> 緑 </label> <input type="radio" name="color" value="Other" id="other" class="toggler" data-togglerTarget="otherColor"> <label for="other"> その他 </label> <div> <label for="otherColor">その他</label> <input type="text" name="otherColor" id="otherColor" class="required"> </div> </div> <div> <p>サイズを選択してください</p> <input class="required" type="radio" name="size" value="Small" id="small"> <label for="small"> スモール </label> <input type="radio" name="size" value="Medium" id="medium"> <label for="medium"> ミディアム </label> <input type="radio" name="size" value="Large" id="large"> <label for="large"> ラージ </label> <input type="radio" name="size" value="Other" id="other2" class="toggler" data-togglerTarget="otherSize"> <label for="other2"> その他 </label> <div> <label for="otherSize">その他</label> <input type="text" name="otherSize" id="otherSize" class="required"> </div> </div> <button name="send">送信</button> </form>
.toggler を指定した要素を選択すると、その要素の data-togglerTarget 属性で指定された値の id 属性を持つ input 要素を表示する仕組みは チェックボックスの「チェックすると要素を表示」と同じです。
//required クラスを指定された要素の集まり const requiredElems = document.querySelectorAll('.required'); //requiredrb クラスが指定されている div 要素(ラジオボタンの親要素)の集まり const togglers = document.querySelectorAll('.toggler'); //チェックボックスをチェックした際に表示される要素を格納する配列 const togglerTargets = []; //.toggler を指定したチェックボックスの各要素について実行 togglers.forEach((elem) => { //.toggler を指定した要素により表示される要素(data-togglerTarget 属性に指定されている要素) const togglerTarget = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); //初期状態ではその親要素を非表示に togglerTarget.parentElement.style.setProperty('display', 'none'); //配列 togglerTargets に追加 togglerTargets.push(togglerTarget); }); //.toggler を指定した要素を引数に取り、選択状態により親要素を表示・非表示にする関数 const toggleTarget = (elem) => { //data-togglerTarget 属性に指定されている要素を取得 const target = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); //チェックボックスの場合 if(elem.type === 'checkbox') { if(elem.checked === true) { //チェックされれば target の親要素を表示 target.parentElement.style.removeProperty('display'); }else{ //チェックが外されたら target の親要素を非表示に target.parentElement.style.setProperty('display', 'none'); } //ラジオボタンの場合 }else if(elem.type === 'radio') { //チェックされれば target の親要素を表示 target.parentElement.style.removeProperty('display'); //同じ親要素を持つラジオボタンを取得 const radios = elem.parentElement.querySelectorAll('[type="radio"]'); //同じ親要素を持つラジオボタンにイベントリスナーを登録 radios.forEach((elem) => { //選択状態が代わったら elem.addEventListener('change', (e) => { // toggler クラスが指定されていなければ if(elem.className !== 'toggler') { //target の親要素が非表示でなければ if(target.parentElement.style.getPropertyValue('display') !== 'none') { //target の親要素を非表示に target.parentElement.style.setProperty('display', 'none'); } } }, {once: true}); //リスナーの呼び出しを一回のみとする }); } } //.toggler を指定した各要素に change イベントのリスナーを登録(上記関数を指定) togglers.forEach((elem) => { elem.addEventListener('change', (e) => { //e.currentTarget はイベントを登録した要素(elem:.toggler を指定した要素) toggleTarget(e.currentTarget); //または toggleTarget(elem); }); }); //送信時の処理 document.myForm.addEventListener('submit', (e) => { //必須の検証 requiredElems.forEach( (elem) => { //★★★ その要素の親要素が非表示(display:none)の場合は対象外 ★★★ if(isValueMissing(elem) && elem.parentElement.style.getPropertyValue('display') !=='none') { e.preventDefault(); } }); //input 要素の親要素が非表示であればその値は送らない togglerTargets.forEach( (elem) => { //チェックボックスにチェックを入れて表示される input 要素の親要素が非表示であれば if(elem.parentElement.style.getPropertyValue('display') ==='none') { //値があればクリア(送信時にチェックされていない場合は値をサーバーに送らない) elem.value = ''; } }); }); //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す) const isValueMissing = (elem) => { if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'radio') { const className = 'required-radio'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const checkedRadio = elem.parentElement.querySelector('input[type="radio"]:checked'); if(checkedRadio === null) { if(!errorSpan) { addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; } else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') { const className = 'required-checkbox'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked'); if(checkedCheckbox === null) { if(!errorSpan) { addError(elem, className, '選択は必須です', 'の選択は必須です'); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else{ const className = 'required'; const errorSpan = elem.parentElement.querySelector('.error.' + className); const elemValue = elem.value.trim(); if(elemValue.length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, '選択してください', 'を選択してください'); }else{ addError(elem, className, '入力は必須です', 'は必須です'); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証) requiredElems.forEach( (elem) => { if(elem.tagName === 'INPUT' && (elem.getAttribute('type') === 'radio' || elem.getAttribute('type') === 'checkbox' )){ const elems = elem.parentElement.querySelectorAll(elem.tagName); elems.forEach( (elemsChild) => { elemsChild.addEventListener('change', () => { isValueMissing(elemsChild); }); }); }else{ elem.addEventListener('input', () => { isValueMissing(elem); }); } }); //エラーメッセージを表示する span 要素を生成して親要素に追加する関数 const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = '「' + label + '」' + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } const errorSpan = document.createElement('span'); errorSpan.classList.add('error', className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); }
サンプル
制約検証 API を使わずに、独自の JavaScript で検証を行う例(サンプル)です。
以下が設定してある制約とその制約を使用する場合に指定するクラス名や属性です。※ 指定するクラスによっては「必須属性」を追加で指定する必要があります。
制限 | 対象要素 | input 要素 対象 type 属性 |
指定するクラス | 必須属性 |
---|---|---|---|---|
必須(入力) | input, textarea | text, tel, email | required | |
必須(選択) | select | required | ||
必須(選択) | input | checkbox, radio | required | |
パターン | input, textarea | text, tel, email | pattern | data-pattern |
最大文字数 最小文字数 |
input, textarea | text, tel, email | maxlength minlength |
data-minlength data-maxlength |
最大値 最小値 |
input | number, range | max min |
data-min data-max |
値の一致 | input, textarea | text, email 等 | equal-to | data-equal-to |
パターンの検証
pattern クラスを指定して、data-pattern 属性にパターン文字列または email、tel、zip を指定(詳細)
最大文字数と最小文字数
maxlength(最大文字数)、minlength(最小文字数)クラスを指定して、それぞれ data-minlength、data-maxlength に文字数を指定(詳細)
最大値と最小値
max(最大値)、min(最小値)クラスを指定して、それぞれ data-min、data-max に値を指定。但し、指定できるのは type 属性が number または range の場合で、数値のみが指定可能。
値が一致するかの検証
検証する input 要素の2つ目の要素に equal-to クラスを指定し、data-equal-to 属性に比較対象の要素の id の値を指定(詳細)
エラーメッセージ
エラーメッセージは特に指定をしなければ、デフォルトのエラーメッセージが表示されます。カスタムエラーメッセージを表示するには以下のエラーメッセージ用の属性を指定します。
以下はそれぞれの検証でカスタムエラーメッセージを表示する場合に指定するエラーメッセージ用の属性とデフォルトのエラーメッセージ、及び属性に label を指定した場合のメッセージです。
エラーメッセージ用の属性に label 以外の文字列を指定すると、その文字列がカスタムエラーメッセージとして表示されます。
クラス | 属性 | デフォルト | label 指定時 |
---|---|---|---|
required | data-error-required | 入力は必須です | 「xxxx」は必須です |
required select 要素 |
data-error-required | 選択してください | 「xxxx」を選択してください |
required チェックボックス |
data-error-required-checkbox | 選択は必須です | 「xxxx」の選択は必須です |
required ラジオボタン |
data-error-required-radio | 選択は必須です | 「xxxx」の選択は必須です |
pattern | data-error-pattern | 入力された値が正しくないようです | 「xxxx」の形式が正しくないようです |
minlength | data-error-minlength | n 文字以上で入力ください | 「xxxx」は n 文字以上で入力ください |
maxlength | data-error-maxlength | n 文字以内で入力ください | 「xxxx」は n 文字以内で入力ください |
min | data-error-min | n 以上の値を入力ください | 「xxxx」は n 以上の値を入力ください |
max | data-error-max | n 以下の値を入力ください | 「xxxx」は n 以下の値を入力ください |
equal-to | data-error-equal-to | 入力された値が異なります | 「xxxx」に入力された値が異なります |
上記表の xxxx は label 要素のテキストを、n は属性に指定された文字数を表します。
デフォルトのエラーメッセージや label 要素のテキストを囲む文字"「" 及び "」"は、スクリプト(validateMyForm.js)の先頭で変数に代入しているので編集することもできます。
オプション
以下のクラスを指定するとオプションの機能を有効にします。
クラス | 説明 |
---|---|
showCount | maxlength クラスと data-maxlength 属性を指定した要素に追加すると入力された文字数と data-maxlength 属性に指定した最大文字数を表示。スタイル用に入力文字数を表示する p 要素に countSpanWrapper クラスを指定しています。 |
toggler | チェックボックスまたはラジオボタンの input 要素に指定して、その要素の data-togglerTarget 属性で指定する値の id 属性を持つ input 要素を記述すると選択状態により data-togglerTarget 属性で指定された要素を表示・非表示に |
HTML の構造
この検証のサンプルのスクリプトを使用するには、form 要素に class="validationForm" と novalidate 属性を指定し、コントロール要素と label 要素(もしあれば)を div 要素で囲む必要があります(name 属性は何でもかまいません)。
そして検証を行う要素に required や pattern などの検証用クラス(及び検証用属性)を指定します。検証用クラスを指定しなければ検証は行われません。
<!-- form 要素に class="validationForm" と novalidate 属性を指定 --> <form name="myForm" class="validationForm" novalidate> <!-- div 要素でコントロールとラベルを囲む --> <div> <label for="name">名前 </label> <!-- 検証用クラスや属性はコントロール要素に指定 --> <input type="text" class="required" name="name" id="name"> </div> <div> <label for="user">ユーザー名: </label> <input class="pattern" data-pattern="[a-zA-Z0-9]{4,10}" type="text" name="user" id="user"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" class="required pattern" data-pattern="email" id="mail" name="mail"> </div> <div data-error-requiredcb="必ず1つは選択ください"> <p>色を選択してください(必須)</p> <input class="required" type="radio" name="color" value="blue" id="blue"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> </div> <button name="send">送信</button> </form>
以下がサンプルのフォームです。
<!-- form 要素に class="validationForm" と novalidate 属性を指定 --> <form name="myForm" class="validationForm" novalidate> <div> <label for="name">名前 </label> <input type="text" class="required maxlength" data-error-required="label" data-maxlength="30" name="name" id="name" placeholder="必須"> </div> <div> <label for="user">ユーザー名: </label> <input class="pattern" data-pattern="[a-zA-Z0-9]{4,10}" data-error-pattern="半角英数字(4〜10文字)で入力ください" type="text" name="user" id="user"> </div> <div> <label for="tel">電話番号 </label> <input type="tel" class="pattern" data-pattern="tel" data-error-pattern="電話番号の形式が正しくないようです" name="tel" id="tel"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" class="required pattern" data-pattern="email" data-error-required="label" data-error-pattern="メールアドレスには @ やドメイン名が必要です" id="mail" name="mail" size="30" placeholder="必須"> </div> <div> <label for="mail2">メールアドレス 再入力(確認用)</label> <input class="required equal-to" data-equal-to="mail" data-error-equal-to="入力されたメールアドレスが異なります" type="email" id="mail2" name="mail2" size="30" placeholder="必須"> </div> <div> <p>色を選択してください(必須)</p> <input class="required" data-error-required-radio="いずれかを選択してください" type="radio" name="color" value="blue" id="blue"> <label for="blue"> 青 </label> <input data-error-requiredRadio="いずれかを選択してください" type="radio" name="color" value="red" id="red"> <label for="red"> 赤 </label> <input data-error-requiredRadio="いずれかを選択してください" type="radio" name="color" value="green" id="green"> <label for="green"> 緑 </label> </div> <div> <p>連絡方法を選択してください(必須:複数選択可)</p> <input class="required" type="checkbox" name="contact[]" id="byEmail" value="Email"> <label for="byEmail"> メール</label> <input type="checkbox" name="contact[]" id="byTel" value="Telephone"> <label for="byTel"> 電話</label> <input type="checkbox" name="contact[]" id="byMail" value="Mail"> <label for="byMail"> 郵便 </label> <input type="checkbox" name="contact[]" id="other1" value="Other1" class="toggler" data-togglerTarget="otherMethod"> <label for="other1"> その他 </label> <div class="mt-10"> <label for="otherMethod">その他</label> <input type="text" name="otherMethod" id="otherMethod" class="required" data-error-required="その他を選択した場合は入力ください"> </div> </div> <div> <select class="required" name="season" id="season"> <!-- 初期状態で選択されている項目のvalue を空に --> <option value="">選択してください(必須)</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <textarea class="required maxlength showCount" data-maxlength="100" name="inquiry" id="inquiry" rows="3" cols="50" placeholder="必須"></textarea> </div> <div> <input class="required" data-error-required-checkbox="送信するにはチェックを入れてください" type="checkbox" name="agreement" id="agreement" value="agree"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form> <!-- 検証用の JavaScript(validateMyForm.js)の読み込み --> <script src="validateMyForm.js"></script>
.error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color: red; box-sizing: border-box; } input[type="radio"]:not(:first-of-type), input[type="checkbox"]:not(:first-of-type) { margin-left: 10px; } input[type="radio"], input[type="checkbox"] { margin-right: 5px; } .countSpanWrapper { color: #999; } .mt-10 { margin-top: 10px; }
検証用 JavaScript(validateMyForm.js)
以下のスクリプトでは、class="validationForm" と novalidate 属性を指定した form 要素を独自に検証します。検証対象のフォームがそのドキュメントに1つのみであることを前提にしています。
また、初回の送信前にはエラー表示はせず、送信時及び送信後にエラーを表示するようにしています。初回送信前の入力時にエラーを表示するには validateAfterFirstSubmit を false に変更します(7行目)。
また、エラーを表示する span 要素に付与するクラスは error としていますが、9行目で変更可能です。
//class="validationForm" と novalidate 属性を指定した form 要素を独自に検証 document.addEventListener('DOMContentLoaded', () => { const validationForm = document.getElementsByClassName('validationForm')[0]; if(validationForm) { let validateAfterFirstSubmit = true; const errorClassName = 'error'; const preLabel = '「'; const postLabel = '」'; const requiredMsg = '入力は必須です'; const requiredMsg_L = 'は必須です'; const requiredSelectMsg = '選択してください'; const requiredSelectMsg_L = 'を選択してください'; const requiredCheckboxMsg = '選択は必須です'; const requiredCheckboxMsg_L = 'の選択は必須です'; const requiredRadioMsg = '選択は必須です'; const requiredRadioMsg_L = 'の選択は必須です'; const patternMsg = '入力された値が正しくないようです'; const patternMsg_L = 'の形式が正しくないようです'; const minlengthMsg = '文字以上で入力ください'; const minlengthMsg_L1 = 'は' ; const minlengthMsg_L2 = '文字以上で入力ください'; const maxlengthMsg = '文字以内で入力ください'; const maxlengthMsg_L1 = 'は' ; const maxlengthMsg_L2 = '文字以内で入力ください'; const minMsg = '以上の値を入力ください'; const minMsg_L1 = 'は' ; const minMsg_L2 = '以上の値を入力ください'; const maxMsg = '以下の値を入力ください'; const maxMsg_L1 = 'は' ; const maxMsg_L2 = '以下の値を入力ください'; const equalToMsg = '入力された値が異なります'; const equalToMsg_L = 'に入力された値が異なります'; const emailRegExp = /^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ui; const telRegExp = /^\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}$/; const zipRegExp = /^\d{3}-{0,1}\d{4}$/; const hiraganaRegExp = /^[\u3041-\u3096\u30FC]+$/; const katakanaRegExp = /^[\u30A1-\u30FC]+$/; const hankataRegExp = /^[\uFF61-\uFF9F]+$/; const patternMap = new Map([ ['email', emailRegExp], ['tel', telRegExp], ['zip', zipRegExp], ['hiragana', hiraganaRegExp], ['katakana', katakanaRegExp], ['hankata', hankataRegExp], ]); const requiredElems = document.querySelectorAll('.required'); const patternElems = document.querySelectorAll('.pattern'); const minlengthElems = document.querySelectorAll('.minlength'); const maxlengthElems = document.querySelectorAll('.maxlength'); const minElems = document.querySelectorAll('.min'); const maxElems = document.querySelectorAll('.max'); const showCountElems = document.querySelectorAll('.showCount'); const togglers = document.querySelectorAll('.toggler'); const togglerTargets = []; const equalToElems = document.querySelectorAll('.equal-to'); const addError = (elem, className, defaultMessage, labelMessage) => { let errorMessage = defaultMessage; if(elem.hasAttribute('data-error-' + className)) { const dataError = elem.getAttribute('data-error-' + className); if(dataError === 'label') { if(elem.parentElement.querySelector('label')) { const label = elem.parentElement.querySelector('label').textContent; if(label) { errorMessage = preLabel + label + postLabel + labelMessage; } } }else if(dataError) { errorMessage = dataError; } } if(!validateAfterFirstSubmit) { const errorSpan = document.createElement('span'); errorSpan.classList.add(errorClassName, className); errorSpan.setAttribute('aria-live', 'polite'); errorSpan.textContent = errorMessage; elem.parentNode.appendChild(errorSpan); } } const isValueMissing = (elem) => { if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'radio') { const className = 'required-radio'; const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); const checkedRadio = elem.parentElement.querySelector('input[type="radio"]:checked'); if(checkedRadio === null) { if(!errorSpan) { addError(elem, className, requiredRadioMsg, requiredRadioMsg_L); } return true; } else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') { const className = 'required-checkbox'; const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked'); if(checkedCheckbox === null) { if(!errorSpan) { addError(elem, className, requiredCheckboxMsg, requiredCheckboxMsg_L); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else{ const className = 'required'; const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value.trim().length === 0) { if(!errorSpan) { if(elem.tagName === 'SELECT') { addError(elem, className, requiredSelectMsg, requiredSelectMsg_L); }else{ addError(elem, className, requiredMsg, requiredMsg_L); } } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } requiredElems.forEach( (elem) => { if(elem.tagName === 'INPUT' && (elem.getAttribute('type') === 'radio' || elem.getAttribute('type') === 'checkbox' )){ const elems = elem.parentElement.querySelectorAll(elem.tagName); elems.forEach( (elemsChild) => { elemsChild.addEventListener('change', () => { isValueMissing(elemsChild); }); }); }else{ elem.addEventListener('input', () => { isValueMissing(elem); }); } }); const isPatternMismatch = (elem) => { const className = 'pattern'; const attributeName = 'data-' + className; let pattern = new RegExp('^' + elem.getAttribute(attributeName) + '$'); patternMap.forEach((value, key) => { if(elem.getAttribute(attributeName) === key) { pattern = value; } }); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value.trim() !=='') { if(!pattern.test(elem.value)) { if(!errorSpan) { addError(elem, className, patternMsg, patternMsg_L); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.value ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } patternElems.forEach( (elem) => { elem.addEventListener('input', () => { isPatternMismatch(elem); }) }); const getValueLength = (value) => { return (value.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []).length; } const isTooShort = (elem) => { const className = 'minlength'; const attributeName = 'data-' + className; const minlength = elem.getAttribute(attributeName); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value !=='') { const valueLength = getValueLength(elem.value); if(valueLength < minlength) { if(!errorSpan) { addError(elem, className, minlength + minlengthMsg, minlengthMsg_L1 + minlength + minlengthMsg_L2); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.value ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } minlengthElems.forEach( (elem) => { elem.addEventListener('input', () => { isTooShort(elem); }) }); const isTooLong = (elem) => { const className = 'maxlength'; const attributeName = 'data-' + className; const maxlength = elem.getAttribute(attributeName); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value !=='') { const valueLength = getValueLength(elem.value); if(valueLength > maxlength) { if(!errorSpan) { addError(elem, className, maxlength + maxlengthMsg, maxlengthMsg_L1 + maxlength + maxlengthMsg_L2); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.value ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } maxlengthElems.forEach( (elem) => { elem.addEventListener('input', () => { isTooLong(elem); }) }); const isRangeUnderflow = (elem) => { const className = 'min'; const attributeName = 'data-' + className; const min = parseFloat(elem.getAttribute(attributeName)); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value !=='') { const val = parseFloat(elem.value); if(val < min) { if(!errorSpan) { addError(elem, className, min + minMsg, minMsg_L1 + min + minMsg_L2); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.value ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } minElems.forEach( (elem) => { elem.addEventListener('input', () => { isRangeUnderflow(elem); }) }); const isRangeOverflow = (elem) => { const className = 'max'; const attributeName = 'data-' + className; const max = parseFloat(elem.getAttribute(attributeName)); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value !=='') { const val = parseFloat(elem.value); if(val > max) { if(!errorSpan) { addError(elem, className, max + maxMsg, maxMsg_L1 + max + maxMsg_L2); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } }else if(elem.value ==='' && errorSpan) { elem.parentNode.removeChild(errorSpan); } } maxElems.forEach( (elem) => { elem.addEventListener('input', () => { isRangeOverflow(elem); }) }); for(let i=0; i<showCountElems.length; i++) { const dataMaxlength = showCountElems[i].getAttribute('data-maxlength'); if(dataMaxlength && !isNaN(dataMaxlength)) { const countElem = document.createElement('p'); countElem.classList.add('countSpanWrapper'); countElem.innerHTML = '<span class="countSpan">0</span>/' + parseInt(dataMaxlength); showCountElems[i].parentNode.appendChild(countElem); } showCountElems[i].addEventListener('input', (e) => { const countSpan = showCountElems[i].parentElement.querySelector('.countSpan'); if(countSpan) { const count = getValueLength(e.currentTarget.value); countSpan.textContent = count; if(count > dataMaxlength) { countSpan.style.setProperty('color', 'red'); }else{ countSpan.style.removeProperty('color'); } } }); } togglers.forEach((elem) => { const togglerTarget = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); togglerTarget.parentElement.style.setProperty('display', 'none'); togglerTargets.push(togglerTarget); }); const toggleTarget = (elem) => { const target = document.querySelector('#' + elem.getAttribute('data-togglerTarget')); if(elem.type === 'checkbox') { if(elem.checked === true) { target.parentElement.style.removeProperty('display'); }else{ target.parentElement.style.setProperty('display', 'none'); } }else if(elem.type === 'radio') { target.parentElement.style.removeProperty('display'); const radios = elem.parentElement.querySelectorAll('[type="radio"]'); radios.forEach((elem) => { elem.addEventListener('change', () => { if(elem.className !== 'toggler') { if(target.parentElement.style.getPropertyValue('display') !== 'none') { target.parentElement.style.setProperty('display', 'none'); } } }, {once: true}); }); } } togglers.forEach((elem) => { elem.addEventListener('change', (e) => { toggleTarget(e.currentTarget); }); }); const isNotEqualTo = (elem) => { const className = 'equal-to'; const attributeName = 'data-' + className; const equalTo = elem.getAttribute(attributeName); const equalToElem = document.getElementById(equalTo); const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className); if(elem.value.trim() !=='' && equalToElem.value.trim() !=='') { if(equalToElem.value !== elem.value) { if(!errorSpan) { addError(elem, className, equalToMsg, equalToMsg_L); } return true; }else{ if(errorSpan) { elem.parentNode.removeChild(errorSpan); } return false; } } } equalToElems.forEach( (elem) => { elem.addEventListener('input', () => { isNotEqualTo(elem); }); const compareTarget = document.getElementById(elem.getAttribute('data-equal-to')); if(compareTarget) { compareTarget.addEventListener('input', () => { isNotEqualTo(elem); }); } }); validationForm.addEventListener('submit', (e) => { validateAfterFirstSubmit = false; requiredElems.forEach( (elem) => { if(isValueMissing(elem) && elem.parentElement.style.getPropertyValue('display') !=='none') { e.preventDefault(); } }); patternElems.forEach( (elem) => { if(isPatternMismatch(elem)) { e.preventDefault(); } }); minlengthElems.forEach( (elem) => { if(isTooShort(elem)) { e.preventDefault(); } }); maxlengthElems.forEach( (elem) => { if(isTooLong(elem)) { e.preventDefault(); } }); minElems.forEach( (elem) => { if(isRangeUnderflow(elem)) { e.preventDefault(); } }); maxElems.forEach( (elem) => { if(isRangeOverflow(elem)) { e.preventDefault(); } }); togglerTargets.forEach( (elem) => { if(elem.parentElement.style.getPropertyValue('display') ==='none') { elem.value = ''; } }); equalToElems.forEach( (elem) => { if(isNotEqualTo(elem)) { e.preventDefault(); } }); const targetSelector = '.' + errorClassName + ':not([style*="display: none"]' + ' .' + errorClassName + ' )'; const errorElem = document.querySelector(targetSelector); if(errorElem) { const errorElemOffsetTop = errorElem.offsetTop; window.scrollTo({ top: errorElemOffsetTop - 40, behavior: 'smooth' }); } }); } });
送信時にエラーがあった場合にその位置までスクロール
エラーがあった場合、その該当の要素までスクロールするように以下の記述を submit イベントに追加しました。
「:not([style*="display: none"] .error」は、 display: none が指定されている div 要素の子要素の .error を除外するためのセレクタです(エラーを表示する span 要素のクラスがデフォルトの .error の場合)。
//.error の要素を取得(但し、display: none が指定されている要素の子要素は除外) const errorElem = document.querySelector('.error:not([style*="display: none"] .error)'); //.error の要素があれば(エラーがあれば) if(errorElem) { const errorElemOffsetTop = errorElem.offsetTop; //エラーの要素の位置へスクロール window.scrollTo({ top: errorElemOffsetTop - 40, //40px 上に //スムーススクロール behavior: 'smooth' }); }
制約検証との併用
前述のサンプルで使用した検証用の JavaScript(validateMyForm.js)は制約検証(Constraint Validation API)を使わずに記述してあり、要素を検証するにはクラス(.required など)を指定し、制約検証では、検証属性(required など)を指定することでその要素を検証します。
また、制約検証では自動検証を無効にして独自のエラーメッセージを任意の位置に表示できます。
検証するそれぞれの要素に対して、クラスを指定して検証するか、属性を指定して検証するかのどちらかにすれば2つの方法を併用することができるので、それぞれの検証を必要に応じて使い分けることができます。
以下は form 要素に novalidate 属性を指定してブラウザーの自動検証を無効にし、制約検証を使った検証と前述の独自の検証を併用する例です。
この例の制約検証を使った検証では検証対象の要素には validate というクラスを指定し、検証属性を指定することで要素を検証するようになっています。
制約検証を使った検証でも、必要に応じて data-* 属性を指定してカスタムエラーメッセージを表示することができます。但し、独自の検証と同じ属性名のものもありますが、異なる属性名もあります。以下が制約検証を使った検証で指定できる data-* 属性です。※独自の検証と制約検証を使った場合で、data-* 属性の名前が重複しないようにするのが良いかも知れません。
data-* 属性 | 指定する属性 | 説明 |
---|---|---|
data-error-required | required 属性 | 値がない場合のエラーメッセージ |
data-error-pattern | pattern 属性 | パターンと一致しない場合のエラーメッセージ |
data-error-minlength | minlength 属性 | 最小文字数に満たない場合のエラーメッセージ |
data-error-maxlength | maxlength 属性 | 最大文字数を超えている場合のエラーメッセージ |
data-error-min | min 属性 | 最小値に満たない場合のエラーメッセージ |
data-error-max | max 属性 | 最大値を超えている場合のエラーメッセージ |
data-error-type | type 属性 | 構文に合っていない場合のエラーメッセージ |
data-error-step | step 属性 | step 属性の規則に合致しない場合のエラーメッセージ |
data-error-badinput | 入力値 | 入力値をブラウザーが処理できない場合のエラーメッセージ |
data-error-xxxx 属性を指定していない場合は、システムのデフォルトのエラーメッセージを表示します。
以下が制約検証と独自の検証を併用サンプルの HTML です。対象の form 要素に class="validationForm" と novalidate 属性を指定します。
カスタムエラーメッセージを表示するコントロール要素には validate というクラスを指定して data-error-* 属性にエラーメッセージを設定します。必要に応じて独自検証用のクラスを指定することもできます。
<body> <div class="content"> <!-- form 要素に class="validationForm" と novalidate 属性を指定 --> <form name="myForm" class="validationForm" novalidate> <div> <label for="name">名前: </label> <input type="text" name="name" id="name" pattern=".{2,10}" data-error-pattern="2〜10文字で入力ください" required data-error-required="名前は必須です" class="validate"> </div> <div> <label for="tel">電話番号: </label> <input type="tel" name="tel" id="tel" pattern="0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{4}" required data-error-required="電話番号は必須です" class="validate"> </div> <div> <label for="mail">メールアドレス</label> <input type="email" id="mail" name="mail" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" required data-error-required="メールアドレスは必須です" minlength="8" class="validate"> </div> <div> <p>色を選択してください</p> <input type="radio" name="color" value="blue" id="blue" required class="validate"> <label for="blue"> 青 </label> <input type="radio" name="color" value="red" id="red" class="validate"> <label for="red"> 赤 </label> <input type="radio" name="color" value="green" id="green" class="validate"> <label for="green"> 緑 </label> </div> <!-- 検証属性は使わず独自検証用のクラスを指定 --> <div> <p>連絡方法を選択してください(必須:複数選択可)</p> <input class="required" type="checkbox" name="contact" id="byEmail" value="Email" data-error-required-checkbox="少なくとも1つを選択してください"> <label for="byEmail"> メール</label> <input type="checkbox" name="contact" id="byTel" value="Telephone" data-error-required-checkbox="少なくとも1つを選択してください"> <label for="byTel"> 電話</label> <input type="checkbox" name="contact" id="byMail" value="Mail" data-error-required-checkbox="少なくとも1つを選択してください"> <label for="byMail"> 郵便 </label> <input type="checkbox" name="contact" id="other1" value="Other1" class="toggler" data-togglerTarget="otherMethod" data-error-required-checkbox="少なくとも1つを選択してください"> <label for="other1"> その他 </label> <div class="mt-10"> <label for="otherMethod">その他</label> <!-- 検証属性は使わず独自検証用のクラスを指定 --> <input type="text" name="otherMethod" id="otherMethod" class="required" data-error-required="その他を選択した場合は入力ください"> </div> </div> <div> <select name="season" id="season" required data-error-required="季節の選択は必須です。" class="validate"> <option value="">季節を選択してください</option> <option value="spring">春</option> <option value="summer">夏</option> <option value="autumn">秋</option> <option value="winter">冬</option> </select> </div> <div> <label for="inquiry">お問い合わせ内容</label> <!-- 検証属性は使わず独自検証用のクラスを指定 --> <textarea name="inquiry" id="inquiry" class="required maxlength showCount" data-maxlength="100" rows="3" cols="50"></textarea> </div> <div> <input type="checkbox" name="agreement" id="agreement" value="agree" required data-error-required="送信するにはこのチェックボックスをオンにしてください" class="validate"> <label for="agreement"> 同意する </label> </div> <button name="send">送信</button> </form> </div> <!-- 制約検証を使った検証の JavaScript(myConstraintValidation.js)の読み込み --> <script src="myConstraintValidation.js"></script> <!-- 独自の検証の JavaScript(validateMyForm.js)の読み込み --> <script src="validateMyForm.js"></script> </body>
.error { width : 100%; padding: 0; display: inline-block; font-size: 80%; color: red; box-sizing: border-box; } input[type="radio"]:not(:first-of-type), input[type="checkbox"]:not(:first-of-type) { margin-left: 10px; } input[type="radio"], input[type="checkbox"] { margin-right: 5px; } .countSpanWrapper { color: #999; } .mt-10 { margin-top: 10px; }
制約検証を使った検証は myConstraintValidation.js(制約検証を使ったサンプルで使用した JavaScript)を、独自の検証は前述のサンプルで使った JavaScript(validateMyForm.js)を読み込んでいます。