JavaScript フォームの検証(制約検証 API)

HTML5 のフォームの検証機能を使えば JavaScript を使わずに簡単に検証機能(入力チェック)を実装できます。但し、表示方法やエラーメッセージをカスタマイズするには制約検証 API(Constraint Validation API)を使うか、独自に JavaScript で検証機能を実装する必要があります。

以下は HTML5 のフォーム検証機能とその基本的な使い方、制約検証 API を使った JavaScript での検証方法、独自のエラーメッセージの表示方法や自動検証を無効にして任意の位置にカスタマイズしたエラーメッセージを表示する方法、制約検証 API を使わずに独自の JavaScript で検証する方法や制約検証との併用の方法などについてサンプルを交えて解説しています。

作成日:2021年8月22日

参考サイト:

関連ページ:

フォームの検証

フォームに入力されたデータをサーバーへ送信する前(サーバー側での検証の前)にクライアント側で検証することにより、フォームのユーザビリティを高めることができます。

クライアント側での検証はサーバー側で行う検証と一貫性のある方法で行います。基本的にはクライアント側の検証をパスすればサーバー側の検証をパスするようにします。また、クライアント側の検証はセキュリティ対策にはならないため、クライアント側の検証を実施してもサーバー側の検証は必要です。

クライアント側での検証には以下のような方法があります。

  • 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 属性に以下の値を指定することで値が妥当かどうかをチェックする制約が自動的に作成され検証が適用されます。

input 要素の type 属性
type 属性 説明
email 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 属性の項目がある場合はラジオボタンのようにはならないようです。

HTML
<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 疑似クラスを使うことでそれぞれの状態の要素にスタイルを適用することができます。

検証関連の疑似クラスには以下のようなものがあります。

検証関連の CSS 疑似クラス
擬似クラス 説明
:valid 入力値がすべての検証要件を満たす場合に適用される擬似クラス
:invalid 入力値が検証要件を満たさない(検証に失敗した)場合に適用される擬似クラス
:required required 属性が設定された入力要素に適用される擬似クラス
:optional required 属性が設定されていない入力要素に適用される擬似クラス
:in-range 値が範囲内にある数値入力要素に適用される擬似クラス
:out-of-range 値が範囲外にある数値入力要素に適用される擬似クラス

以下は :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;
}

選択してください

: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: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 属性に指定した文字が表示されてフォームは送信されません。

10文字以内で入力ください

0から始まる半角数字で入力ください(ハイフンを含めることができます)

メールアドレスは必須です。@ やドメインを含む正しい形式で入力ください。

HTML5 のフォームの検証機能を使えば、上記のように CSS だけで比較的簡単に検証機能を実装できますが、表示されるエラーメッセージや表示方法はブラウザによって異なります(ブラウザに依存します)。カスタマイズするには次項の「制約検証 API」を使います。

Constraint Validation API(制約検証)

Constraint Validation API(制約検証)を使うと、ユーザーがフォームのコントロール要素に入力した値を、サーバーに送信する前に JavaScript を使って検証することができます。

制約検証を使えば、ブラウザのフォーム検証機能の既定のエラーメッセージをカスタマイズしたり、より複雑な制約などを設定することができます。

最近のブラウザは制約検証に対応しています(caniuse.com/constraint-validation)。

参考ページ:

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 のプロパティやメソッドを持つフォームコントロール要素(オブジェクト)です。

以下は上記の要素で利用できる制約検証 API のプロパティで、いずれも読取専用です。

プロパティ 説明
validity 要素の検証状態を表す ValidityState オブジェクトを返します。このオブジェクトのプロパティを調べることで各検証の状態を真偽値で取得できます。 オブジェクト
validationMessage そのコントロール要素が制約検証を満たさなかった場合、その内容を記述したメッセージを返します。コントロール要素が制約の検証の対象ではない場合 (willValidate が false) や要素の値が制約を満たしている(合格している場合)は空文字列を返します。この値は setCustomValidity() メソッドでカスタマイズできます。 文字列
willValidate その要素が制約検証の候補であるか(検証を設定できるか)どうかの真偽値を返します。要素が検証可能な場合は true を返し、そうでない場合は false を返します。例えば、type 属性が hidden や reset、 button の場合や disabled 属性が設定されいている要素では false を返します(その要素に検証属性が設定されているかどうかを判定するものではありません)。 真偽値

メソッド

以下はフォームコントロール要素で利用できる制約検証 API のメソッドです。

コントロール要素で利用できる制約検証 API のメソッド
メソッド 説明
checkValidity() 要素の値が制約検証を満たしている場合に true を返します。制約検証を満たしていない場合は false を返し、その制約検証を満たしていない要素で invalid イベントを発生させます。
reportValidity() checkValidity() メソッドを実行し、false が返された場合 (制約検証を満たしていない場合) は、フォームを送信する際と同様、入力が無効であることをユーザーに報告(エラーメッセージを表示)します。
setCustomValidity() 要素に独自の検証メッセージ(カスタム検証メッセージ)設定します。設定するメッセージは引数に指定します。カスタム検証メッセージを設定すると、要素が制約検証を満たしていない場合に設定したメッセージが表示されます。このメッセージが設定されていると(空の文字列ではない場合)、その要素は独自の検証エラーがある状態になり、検証に不合格になります。独自の検証エラーを解除するには setCustomValidity() に空文字列を指定します。

form 要素

以下は form 要素(HTMLFormElement)で利用できる制約検証 API のメソッドとプロパティです。

form 要素で利用できる制約検証 API のメソッド
メソッド 説明
checkValidity() このフォーム要素の子コントロールが制約検証の対象となっていて、それらの制約を満たしている場合は true を返します。制約を満たさないコントロールがある場合は false を返します。制約を満たさないコントロールに対して、invalid イベントを発生させます。イベントがキャンセルされない場合、そのようなコントロールは無効とみなされます。
reportValidity() このフォーム要素の子コントロールがその検証する制約を満たしている場合、true を返します。false が返された場合、無効な子要素それぞれにキャンセル可能な invalid イベントが発生し、検証の内容がユーザーに報告されます(エラーメッセージが表示されます)。
プロパティ 説明
noValidate フォームの novalidate 属性の値を反映し、フォームの検証を行わないかどうかを示す真偽値を返します。検証を行わない場合は true を返します。

ValidityState

ValidityState はフォームコントロール要素に設定された制約の検証状態を表すオブジェクト(インターフェイス)で、コントロール要素の読み取り専用のプロパティ validity で参照できます。

ValidityState のプロパティには以下のようなものがあり、いずれも真偽値(Boolean)を返します。

true は指定された検証が失敗したことを表しますが、valid プロパティだけは例外で、true は要素の値がすべての制約に適合している(valid:有効である)ことを表します。

ValidityState のプロパティ(全て読み取り専用で真偽値を返します)
プロパティ 説明
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 を指定します。

HTML
<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 を使ってデータが無効な場合のスタイルを設定しています。

CSS
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 のプロパティを使ってエラーの内容を判定しエラーメッセージを表示します。

JavaScript
//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');
});
送信ボタンを disabled に

以下は、初期状態で送信ボタンに disabled 属性を設定して送信できないようにして、入力された値が検証を満たしていれば送信ボタンの disabled 属性を削除して送信できるようにする例です。

HTML と CSS は前述の例と同じです。前述の例と異なる部分にはコメントを入れてあります。この場合、フォームの送信時の値のチェックは不要なので submit イベントの部分は削除しています。

const myForm  = document.forms.myForm;
const email = myForm.mail;
const send = myForm.send;
const errorText = document.querySelector('#mail + span.error');
  
//送信ボタンに disabled 属性を設定
send.disabled = true;
//または send.setAttribute('disabled', 'disabled');

email.addEventListener('input', () => {
  if (email.validity.valid) {
    errorText.textContent = ''; 
    errorText.className = 'error'; 
    //値が有効であれば送信ボタンの disabled 属性を削除
    send.disabled = false;
    //または send.removeAttribute('disabled');
  } else {
    showError();
    //値が無効であれば送信ボタンを disabled に
    send.disabled = true;
    //または send.setAttribute('disabled', 'disabled');
  }
});

const showError = ()  => { //前述の例と同じ
  if(email.validity.valueMissing) {
    errorText.textContent = 'メールアドレスは必須です。';
  } else if(email.validity.typeMismatch) {
    errorText.textContent = '正しい形式のメールアドレスを入力してください';
  } else if(email.validity.tooShort) {
    errorText.textContent = `メールアドレスは ${ email.minLength } 文字以上です。現在の文字数:${ email.value.length }`;
  }
  errorText.className = 'error active'; 
  //または errorText.classList.add('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 イベントを使用するほうが互換性が高いようです。

また、エラーを表示する関数 showError() は、このフォームの要素の name 属性や type 属性の値などを使ってエラーメッセージをカスタマイズしているので、フォームの構造が異なれば、それに合わせて書き換える必要があります(エラーメッセージのカスタマイズ)。

//document.forms を使って name="myForm" の form 要素を取得
const myForm  = document.forms.myForm;
//フォームのコントロール要素全てを取得
const elements = myForm.elements;

//コントロール要素(最後のコントロールのボタンを除く)に input イベントのリスナーを登録
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++) {
  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 メソッドを使用しています。

//検証対象の(.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 イベントのリスナーを登録
  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-* 属性
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" が付与されます。

また、data-* 属性を指定しない場合は、その要素の validationMessage プロパティを使って、ブラウザのデフォルトのメッセージを設定しています。

//validate クラスを指定した要素(検証を行う要素)を全て取得
const elements = document.querySelectorAll('.validate');

//対象の要素に input イベントのリスナーを登録
elements.forEach((elem) => {
  elem.addEventListener('input', (e) => {
    const parentDiv = e.currentTarget.parentElement;
    const errorSpans = parentDiv.querySelectorAll('span.error');
    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();
  }
});
  
//エラーメッセージを生成する関数(ブラウザのデフォルトエラーメッセージを利用)
//elem :対象の要素
//validationType :対象の検証属性名または type(例:パターン検証なら pattern、type 属性の検証なら type など)
const getErrorMsg = (elem, validationType) => {
  //戻り値として返す変数にシステムのデフォルトエラーメッセージを代入
  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;
    }
  }
  //作成したエラーメッセージを返す
  return errorMessage;
} 

//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
//elem :対象の要素
//validationType :対象の検証属性名または type(例:パターン検証なら pattern、type 属性の検証なら type など)
//errorMessage :表示するエラーメッセージ(上記 getErrorMsg 関数で生成)
const addErrorSpan = (elem, validationType, errorMessage) => {
  //span 要素を生成
  const errorSpan = document.createElement('span');
  //error 及び引数に指定されたクラスを追加(設定)
  errorSpan.classList.add('error', validationType);
  //aria-live 属性を設定
  errorSpan.setAttribute('aria-live', 'polite');
  //引数に指定されたエラーメッセージを設定
  errorSpan.textContent = errorMessage;
  //elem の親要素の子要素として追加
  elem.parentNode.appendChild(errorSpan);
}

const setupErrorMsg = (elem, validationType ) => {
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = elem.parentElement.querySelector('.error.' + validationType);
  //エラーを表示する span 要素が存在しなければ
  if(!errorSpan) {
    //getErrorMsg() を使ってエラーメッセージを作成
    const errorMessage = getErrorMsg(elem, validationType);
    //addErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
    addErrorSpan(elem, validationType, errorMessage);
  }
}

//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 属性にメッセージを指定します。

HTML(指定する属性や構造)
<!-- 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>
CSS
/* エラーメッセージのスタイル */
.error {
  width : 100%;
  padding: 0;
  display: inline-block;
  font-size: 80%;
  color: red;
  box-sizing: border-box;
}

JavaScript は前述の data-* 属性でエラーをカスタマイズ とほぼ同じですが、class="validationForm" を指定したフォームを対象にしています。

また、DOMContentLoaded イベントで読み込むようにしています。

myConstraintValidation.js
//class="validationForm" と novalidate 属性を指定した form 要素の制約検証をカスタマイズ
//data-error-* 属性の値を使ってエラーメッセージをカスタマイズ
document.addEventListener('DOMContentLoaded', () => {
  //.validationForm を指定した最初の form 要素を取得
  const validationForm = document.getElementsByClassName('validationForm')[0];
  
  if(validationForm) {
    //validate クラスを指定した要素(検証を行う要素)を全て取得
    const elements = document.querySelectorAll('.validate');

    //対象の要素に input イベントのリスナーを登録
    elements.forEach((elem) => {
      elem.addEventListener('input', (e) => {
        const parentDiv = e.currentTarget.parentElement;
        const errorSpans = parentDiv.querySelectorAll('span.error');
        if(errorSpans) {
          errorSpans.forEach((errorSpan) => {
            elem.parentNode.removeChild(errorSpan);
          });
        } 
        if(!elem.validity.valid) {
          showError(e.currentTarget);
        }
      });
    });

    //.validationForm を指定した form 要素に submit イベントのリスナーを設定
    validationForm.addEventListener('submit', (e)  => {
      if(!e.currentTarget.checkValidity()){
        elements.forEach( (elem) => {
          if(!elem.validity.valid) {
            showError(elem);
          }
        })
        e.preventDefault();
      }
    });

    //エラーメッセージを生成する関数(ブラウザのデフォルトエラーメッセージを利用)
    //elem :対象の要素
    //validationType :対象の検証属性名または type(例:パターン検証なら pattern、type 属性の検証なら type など)
    const getErrorMsg = (elem, validationType) => {
      //戻り値として返す変数にシステムのデフォルトエラーメッセージを代入
      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;
        }
      }
      //作成したエラーメッセージを返す
      return errorMessage;
    } 

    //エラーメッセージを表示する span 要素を生成して親要素に追加する関数
    //elem :対象の要素
    //validationType :対象の検証属性名または type(例:パターン検証なら pattern、type 属性の検証なら type など)
    //errorMessage :表示するエラーメッセージ(上記 getErrorMsg 関数で生成)
    const addErrorSpan = (elem, validationType, errorMessage) => {
      //span 要素を生成
      const errorSpan = document.createElement('span');
      //error 及び引数に指定されたクラスを追加(設定)
      errorSpan.classList.add('error', validationType);
      //aria-live 属性を設定
      errorSpan.setAttribute('aria-live', 'polite');
      //引数に指定されたエラーメッセージを設定
      errorSpan.textContent = errorMessage;
      //elem の親要素の子要素として追加
      elem.parentNode.appendChild(errorSpan);
    }

    const setupErrorMsg = (elem, validationType ) => {
      //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
      const errorSpan = elem.parentElement.querySelector('.error.' + validationType);
      //エラーを表示する span 要素が存在しなければ
      if(!errorSpan) {
        //getErrorMsg() を使ってエラーメッセージを作成
        const errorMessage = getErrorMsg(elem, validationType);
        //addErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
        addErrorSpan(elem, validationType, errorMessage);
      }
    }

    //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');
      } 
    }
  }
});

制約検証 API を使わない方法

制約検証 API を使用せずに JavaScript を使ってフォームを検証することもできます。

組み込みの API(制約検証)ではできない検証を JavaScript を使って実装したり、古いブラウザーに対応するためなどに制約検証 API を使わずに JavaScript を使って独自の検証を実装することができます。

但し、以下のサンプルではアロー関数などの ES6 で追加された機能addEventListener()querySelectorAll() などを使用しているので古いブラウザ(IE9以前など)には対応していません。

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 というクラスの他にどの検証のエラーなのかを特定するため検証用クラスも指定します。

JavaScript で生成されるエラーメッセージの span 要素の例(5行目)
<div>
  <label for="name">名前: </label>
  <input class="required" type="text" name="name" id="name">
  <!-- 以下が生成されるエラーメッセージを表示する span 要素(必須の場合) -->
  <span class="error required" aria-live="polite">入力は必須です</span>
</div>

また、以下が以降のサンプルでのエラーメッセージのスタイルです。

CSS
.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 では値が空の場合は独自関数(後述)を使ってエラーを表示する要素を生成及び追加し、エラーメッセージを表示して true を返します。値が入力されていればエラーを表示する要素(存在していれば)を削除して false を返します。

値が空かどうかの判定は比較演算子(=== )を使い、値の文字列の長さが0かどうかを比較しています。値が空(長さが0)の場合は true が返ります。

関数 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);
  //値が空の場合はエラーを表示して true を返す
  if(elem.value.length === 0) {
    //エラーを表示する span 要素が存在しなければ
    if(!errorSpan) {
      //select 要素の場合
      if(elem.tagName === 'SELECT') {
        //getErrorMessage() を使ってエラーメッセージを作成
        const errorMessage = getErrorMessage(elem, className, '選択してください', 'を選択してください');
        //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
        createErrorSpan(elem, className, errorMessage);
      //select 要素以外の場合
      }else{ 
        //getErrorMessage() を使ってエラーメッセージを作成
        const errorMessage = getErrorMessage(elem, className, '入力は必須です', 'は必須です');
        //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
        createErrorSpan(elem, className, errorMessage);
      } 
    }
    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 :対象の検証用クラス名(例:必須なら required)
//errorMessage :表示するエラーメッセージ
//isParent :この関数をコントロール要素の親要素で実行する場合は true を指定(省略可能)
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  //span 要素を生成
  const errorSpan = document.createElement('span');
  //error 及び引数に指定されたクラスを追加(設定)
  errorSpan.classList.add('error', className);
  //aria-live 属性を設定
  errorSpan.setAttribute('aria-live', 'polite');
  //引数に指定されたエラーメッセージを設定
  errorSpan.textContent = errorMessage;
  //isParent が true であれば elem の子要素に追加
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    //そうでなければ elem の親要素の子要素として追加
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
//elem :対象の要素
//className :対象の検証用クラス名(例:必須なら required)
//defaultMessage:デフォルトのエラーメッセージ
//labelMessage:label 要素のテキストを使う場合のメッセージ(文字列)
//isParent :この関数をコントロール要素の親要素で実行する場合は true を指定(省略可能)
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  //戻り値として返す変数 errorMessage にデフォルトのエラーメッセージを代入
  let errorMessage = defaultMessage;
  //要素に data-error-xxxx 属性が指定されていれば(xxxx は検証用クラス名)
  if(elem.hasAttribute('data-error-' + className)) { 
    //data-error-xxxx  属性の値を取得
    const dataError = elem.getAttribute('data-error-' + className);
    //data-error-xxxx  属性の値が label であれば
    if(dataError === 'label') {
      //isParent が指定されていない(チェックボックスやラジオボタン以外)の場合
      if(!isParent) {
        //label 要素が存在すれば(確認しないと label 要素が存在しない場合エラー)
        if(elem.parentElement.querySelector('label')) {
          //label 要素のテキストを取得
          const label = elem.parentElement.querySelector('label').textContent;
          //テキストが空でなければ
          if(label) {
            //label 要素のテキストと引数 labelMessage でエラーメッセージを作成
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      //チェックボックスやラジオボタンの場合は label を指定すると無効
      }else{
        //親要素で実行されている場合、親要素に対する label 要素は存在しない
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {// data-error-xxxx  属性の値が label 以外の場合
      //data-error-xxxx  属性の値をエラーメッセージとする
      errorMessage = dataError;
    }
  }
  //作成したエラーメッセージを返す
  return errorMessage;
} 
エラーメッセージを生成する関数

エラーメッセージを生成する関数は、以降の全ての検証の関数で使用するため、少し複雑になっています。

createErrorSpan(63〜79行目)はエラーメッセージを表示するための span 要素を生成して親要素に追加する関数、getErrorMessage(87〜120行目)はエラーメッセージを生成して返す関数です。

これらの関数は検証用のそれぞれの関数の中で使われ、検証の対象のコントロール要素を第1引数の elem に、対象の検証用クラス名を第2引数の className に受け取ります。

関数 createErrorSpan の第4引数及び getErrorMessage の第5引数 isParent はこの関数の対象がコントロール要素の親要素の場合(チェックボックスやラジオボタンの場合)に true を指定します。その他の場合は省略するか false を指定します。

第2引数 className は検証の種類を特定するためのクラスで、それぞれの検証用クラス名を指定します。第3引数の errorMessage は関数 getErrorMessage で作成したメッセージを指定します。

関数 getErrorMessage はそれぞれの検証におけるエラーメッセージを作成する関数で、対象の要素に data-error-xxxx 属性が指定されていれば、その値によりエラーメッセージをカスタマイズします(xxxx は対象のクラス名)。

data-error-xxxx 属性にはカスタムエラーメッセージの文字列を指定することができます。また、label を指定すると label 要素のテキストを使ってエラーメッセージを作成します。但し、isParent を true に指定するチェックボックスやラジオボタンの場合は、親要素(div 要素)に対する label 要素は存在しないので、label を指定しても無効になります。

getErrorMessage の第3引数及び第4引数はそれぞれの検証の内容に合わせて適切なエラーメッセージを生成するようなテキストを指定します。

空白文字のみの場合もエラーにする

必須の条件を空文字列の場合だけではなく、空白文字のみの場合もエラーにするには、空白文字のみまたは空の文字列を表す正規表現(/^\s*$|^$/ など)を使って関数 isValueMissing で test() メソッドなどを使って以下のように記述することができます。

必須以外の検証では、空の値を検証してもエラーになるので、値が入力されている場合に検証し、値が入力されているかは入力の値の長さが 0 かどうかで判定しています。

そのため、必須の条件を空白文字のみの場合もエラーにすると、例えば半角文字だけのパターンの検証と併用した場合、最初に空白文字が入力されると両方のエラーが表示されることになります。

const isValueMissing = (elem) => {
  //空白文字のみまたは空の文字列を表す正規表現
  const pattern = /^\s*$|^$/;
  const errorSpan = elem.parentElement.querySelector('.error.required');
  //値が空か空白文字のみかを test() メソッドを使って判定
  if(pattern.test(elem.value)) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      const errorMessage = getErrorMessage(elem, '入力は必須です', 'は必須です');
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      createErrorSpan(elem, 'required', errorMessage);
    }
    ・・・以下省略・・・
    
パターンの検証

JavaScript の正規表現を使って入力された文字列が期待する形式(パターン)に合致するかを検証することができます。

パターンを検証する要素に pattern クラスを指定して、カスタムデータ属性 data-pattern にパターン文字列を指定して検証します。また、よく使うメールアドレスなどのパターンは data-pattern 属性に特定の文字列を指定することで、既定のパターンを使って検証するようにしています。

data-pattern 属性に指定できる値(追加可能)
適用される正規表現パターン(スクリプト側に記述)
email /^([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}$/
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 を返します。

また、値が空で既にエラーメッセージを表示する要素が生成されている場合は、エラーメッセージの要素を削除してエラーをクリアします(途中まで入力してから値を削除した場合などエラーが残らないように)。

//required クラスを指定された要素の集まり  
const requiredElems = document.querySelectorAll('.required');
//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);
  //値が空でなければ
  if(elem.value !=='') {
    //値がパターンにマッチするかを test()  メソッドで判定
    if(!pattern.test(elem.value)) {
      //エラーを表示する span 要素が存在しなければ
      if(!errorSpan) {
        //getErrorMessage() を使ってエラーメッセージを作成
        const errorMessage = getErrorMessage(elem, className, '入力された値が正しくないようです', 'の形式が正しくないようです');
        //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
        createErrorSpan(elem, className, errorMessage);
      }
      return true; //マッチしない場合は true を返す
    }else{
      //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア
      if(errorSpan) {
        elem.parentNode.removeChild(errorSpan);
      }
      return false;
    }
  //値が空でエラーを表示する要素が存在すれば削除
  }else if(elem.value ==='' && 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();
    }
  });
});
  
/********* 以降は前述の必須の検証と同じ内容 *********/
  
//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  const errorSpan = document.createElement('span');
  errorSpan.classList.add('error', className);
  errorSpan.setAttribute('aria-live', 'polite');
  errorSpan.textContent = errorMessage;
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  let errorMessage = defaultMessage;
  if(elem.hasAttribute('data-error-' + className)) { 
    const dataError = elem.getAttribute('data-error-' + className);
    if(dataError === 'label') {
      if(!isParent) {
        if(elem.parentElement.querySelector('label')) {
          const label = elem.parentElement.querySelector('label').textContent;
          if(label) {
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      }else{
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {
      errorMessage = dataError;
    }
  }
  return errorMessage;
} 
  
//値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す)
const isValueMissing = (elem) => {
  const className = 'required';
  const errorSpan = elem.parentElement.querySelector('.error.' + className);
  if(elem.value.length === 0) {
    if(!errorSpan) {
      if(elem.tagName === 'SELECT') {
        const errorMessage = getErrorMessage(elem, className, '選択してください', 'を選択してください');
        createErrorSpan(elem, className, errorMessage);
      }else{ 
        const errorMessage = getErrorMessage(elem, className, '入力は必須です', 'は必須です');
        createErrorSpan(elem, className, errorMessage);
      } 
    }
    return true;
  }else{ 
    if(errorSpan) {
      elem.parentNode.removeChild(errorSpan);
    }
    return false;
  }
}

//required クラスを指定された要素に input イベントを設定
requiredElems.forEach( (elem) => {
  elem.addEventListener('input', () => {
    isValueMissing(elem);
  })
});  
文字数の制限

文字数の制限は前項のパターンの検証を使っても可能ですが、以下では指定されたクラス名により最小及び最大文字数の検証を実装する例です。

最小文字数を検証する要素には 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 イベントで取得した文字数(カウント)を出力します。そして入力文字数が最大文字数を超えればカウントの文字色を赤に変更し、最大文字数より小さくなれば文字色を戻します。

スタイル用に入力文字数を表示する p 要素に countSpanWrapper クラスを指定しています。

セレクトボックス

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';
  const errorSpan = elem.parentElement.querySelector('.error.' + className);
  //値(value プロパティ)が空の場合
  if(elem.value.length === 0) {
    if(!errorSpan) {
      //select 要素の場合(option 要素の value が空の場合)
      if(elem.tagName === 'SELECT') {
        const errorMessage = getErrorMessage(elem, className, '選択してください', 'を選択してください');
        createErrorSpan(elem, className, errorMessage);
      }else{ //それ以外
        const errorMessage = getErrorMessage(elem, className, '入力は必須です', 'は必須です');
        createErrorSpan(elem, className, errorMessage);
      } 
    }
    return true;
  }else{
    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>

チェックボックス

複数項目があるチェックボックスのいずれかをチェックすることを必須とする検証を JavaScript を使って実装する例です(複数項目のチェックボックス)。

以下のフォームでは、チェックボックスの項目がチェックされていない状態で送信したり、チェックした後に解除していずれもチェックされていない場合はエラーメッセージを表示します。

グループ内の項目の少なくとも1つをチェックすると送信できるようになります。

チェックボックスの場合、いずれかの項目の選択を必須にするにはその親要素(div 要素)に requiredcb というクラスを指定するようにしています。

また、カスタムエラーメッセージを指定する場合も、親要素(div 要素)に data-error-requiredcb 属性を指定します。

但し、チェックボックスの場合、グループ全体に対する label 要素はないので、他の検証のように値に label を指定することはできません(指定しても無効です)。この例では2つ目のチェックボックスの親要素に data-error-requiredcb="label" を指定していますが、デフォルトのエラーメッセージが表示されます。

<form name="myForm" novalidate>
  <div class="requiredcb">
    <p>連絡方法を選択してください(複数選択可)</p>
    <input 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 class="requiredcb" data-error-requiredcb="label"><!-- label は無効 -->
    <p>興味のある項目を選択してください(複数選択可)</p>
    <input 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 class="requiredcb" data-error-requiredcb="送信するには同意が必要です">
    <input type="checkbox" name="agreement" id="agreement" value="agree">
    <label for="agreement"> 同意する </label>
  </div>
  <button name="send">送信</button>
</form>

検証及びエラーを表示する関数 isCheckMissing では引数にチェックボックスの親要素を受け取り、その親要素のメソッドとして querySelector() の引数に :checked 擬似クラスを使ったセレクタを指定して選択されている先頭のチェックボックス要素を取得して変数 checked に代入しています。

変数 checked の値が null であれば、いずれのチェックボックスも選択されていないので(すでにエラーメッセージが存在しなければ)エラーメッセージを生成して true を返します。

変数 checked の値が null でなければ、いずれかのチェックボックスが選択されているので、すでにエラーメッセージが存在していれば削除して false を返します。

また、この関数は親要素を対象に実行するので、getErrorMessage() の第5引数及び createErrorSpan() の第4引数には true を指定する必要があります。

チェックボックスやラジオボタンの場合、label 要素を使ったエラーメッセージは作成できないので、getErrorMessage() の第4引数は指定しても使われないので空文字を指定しています。

change イベントのリスナーはチェックボックスの各要素ではなく、チェックボックスの親要素に登録して子要素で発生するイベントを捕捉(イベント移譲)するようにしています。

関連項目:イベントの移譲

//requiredcb クラスが指定されている div 要素(チェックボックスの親要素)の集まり
const requiredcbDivs = document.querySelectorAll('.requiredcb');
  
//親要素を引数に受け取りその子要素のチェックボックスを検証する関数
const isCheckMissing = (parentElem) => {
  //対象のクラス名
  const className = 'requiredcb';
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = parentElem.querySelector('.error.' + className);
  //選択状態の最初のチェックボックス要素を取得
  const checked = parentElem.querySelector('input[type="checkbox"]:checked');
  //いずれのチェックボックスも選択されていない場合
  if(checked === null) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      //※ 第5引数に true を指定しなければならない(第4引数は使用されないので空文字を指定)
      const errorMessage = getErrorMessage(parentElem, className, '選択してください', '', true);
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      createErrorSpan(parentElem, className, errorMessage, true);
    }
    return true;
  }else{
    if(errorSpan) {
      parentElem.removeChild(errorSpan);
    }
    return false;
  }
}

//チェックボックスの親要素に change イベントのリスナーを登録(イベントの移譲)
requiredcbDivs.forEach( (parentElem) => {
  parentElem.addEventListener('change', () => {
    isCheckMissing(parentElem);
  })
});
  
//送信時の処理(検証対象の要素を検証し、要件を満たさない場合は送信を中止)
document.myForm.addEventListener('submit', (e) => {
  //.requiredcb を指定した子要素のチェックボックスの検証
  requiredcbDivs.forEach( (parentElem) => {
    if(isCheckMissing(parentElem)) {
      e.preventDefault();
    }
  });
});
  
//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  const errorSpan = document.createElement('span');
  errorSpan.classList.add('error', className);
  errorSpan.setAttribute('aria-live', 'polite');
  errorSpan.textContent = errorMessage;
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  let errorMessage = defaultMessage;
  if(elem.hasAttribute('data-error-' + className)) {
    const dataError = elem.getAttribute('data-error-' + className);
    if(dataError === 'label') {
      if(!isParent) {
        if(elem.parentElement.querySelector('label')) {
          const label = elem.parentElement.querySelector('label').textContent;
          if(label) {
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      }else{
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {
      errorMessage = dataError;
    }
  }
  return errorMessage;
}

選択されたチェックボックスの取得

チェックボックスの選択されている項目を取得するには次のような方法があります。

  • :checked 擬似クラスを使ったセレクタで選択されているチェックボックス要素を取得
  • 各チェックボックスを調べてその checked プロパティが true のチェックボックス要素を取得

:checked 擬似クラスを使ったセレクタ(例 [type="checkbox"]:checked)を querySelectorAll() に指定すれば、その時点で選択されている全てのチェックボックス要素を取得することができます(必要に応じてセレクタに name 属性などを指定します)。

前述の例では :checked 擬似クラスを使ったセレクタを querySelector() に指定して、選択されている先頭のチェックボックス1つを取得しました。

2番目の方法は、対象となるチェックボックスの要素を全て取得して、それぞれの要素についてその checked プロパティを調べます。

以下は前述の例の関数 isCheckMissing を2番目の方法で(:checked を使わずに)書き換えたものです。

const isCheckMissing = (parentElem) => {
  //対象のクラス名
  const className = 'requiredcb';
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = parentElem.querySelector('.error.' + className);
  //選択状態の最初のチェックボックス要素を取得
  //いずれのチェックボックスがチェックされているかどうかのフラグ(変数)
  let isChecked = false;  //初期状態では選択されていないので false
  //querySelectorAll() で全てのチェックボックス要素を取得
  const checkboxes = parentElem.querySelectorAll('input[type="checkbox"]');
  for(let i=0; i<checkboxes.length; i++) {
  //checked プロパティが true であれば isChecked の値を true に
    if(checkboxes[i].checked === true) {
      isChecked = true;
    } 
  }
  //いずれのチェックボックスも選択されていない(isChecked が false)場合
  if(!isChecked) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      const errorMessage = getErrorMessage(parentElem, className, '選択してください', 'を選択してください', true);
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      createErrorSpan(parentElem, className, errorMessage, true);
    }
    return true;
  }else{
    if(errorSpan) {
      parentElem.removeChild(errorSpan);
    }
    return false;
  }
}

関連項目:選択されたチェックボックスを取得

チェックすると要素を表示

以下は「その他」のチェックボックスをチェックすると、テキストフィールドの入力欄を表示する例です。

前述の例と同様、チェックボックスの親要素に .requiredcb を指定したチェックボックスでは項目を少なくとも1つ選択することを必須としています(以下では全てのチェックチェックの親要素に .requiredcb を指定しています)。

また、チェックボックスをチェックした際に表示されるテキストフィールドには .required を指定すれば必須入力とできます。以下の例では最初の「その他」のテキストフィールドは必須にしています。

仕組みとしては .toggler を指定したチェックボックスをチェックすると、その要素の data-togglerTarget 属性で指定された値の id 属性を持つ input 要素を表示し、チェックを外すと非表示にします。

言い換えると、.toggler を指定したチェックボックスにはチェックされた際に表示する input 要素の id を data-togglerTarge に指定する必要があります。

<form name="myForm" novalidate>
  <div class="requiredcb" data-error-requiredcb="少なくとも1つを選択してください">
    <p>連絡方法を選択してください(複数選択可)</p>
    <input 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 class="requiredcb">
    <p>興味のある項目を選択してください(複数選択可)</p>
    <input 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 class="requiredcb"v>
    <input 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 に追加します(11〜18行目)。

toggleTarget() はチェックボックスの選択状態により、対象の input 要素(の親要素)を表示・非表示にする関数で、.toggler を指定したチェックボックスの change イベントのリスナーに指定します。

また、toggleTarget() はラジオボタンでも機能するように、ラジオボタンの場合は、ラジオボタンの各要素に change イベントのリスナーを登録して、変更があった際に toggler クラスを指定された要素以外が選択された場合は、対象の input 要素(の親要素)を非表示にするようにしています(ラジオボタンの場合、その要素の選択が解除されたという change イベントが発生しないため)。

送信時の処理(submit イベント)では、.required を指定した要素の検証で、その親要素が非表示になっているものは検証の対象から外しています(チェックボックスにチェックを入れて表示される input 要素に required が指定されていても非表示の場合は検証しないようにしています:69行目)。

また、togglerTargets の各要素(チェックボックスをチェックした際に表示される要素)の親要素が非表示になっている場合は、その要素の値を空にして非表示になっているテキストフィールドの値を送信しないようにしています(74〜80行目)。

//required クラスを指定された要素の集まり  
const requiredElems = document.querySelectorAll('.required'); 
//requiredcb クラスを指定された要素(チェックボックスの親要素)の集まり  
const requiredcbDivs = document.querySelectorAll('.requiredcb');
//チェックすると 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 = '';
    }
  });
  //.requiredcb を指定した子要素のチェックボックスの検証
  requiredcbDivs.forEach( (elem) => {
    if(isCheckMissing(elem)) {
      e.preventDefault();
    }
  });
}); 
  
//親要素を引数に受け取りその子要素のチェックボックスを検証する関数
const isCheckMissing = (parentElem) => {
  //対象のクラス名
  const className = 'requiredcb';
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = parentElem.querySelector('.error.' + className);
  //選択状態の最初のチェックボックス要素を取得
  const checked = parentElem.querySelector('input[type="checkbox"]:checked');
  //いずれのチェックボックスも選択されていない場合
  if(checked === null) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      const errorMessage = getErrorMessage(parentElem, className, '選択してください', '', true);
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      createErrorSpan(parentElem, className, errorMessage, true);
    }
    return true;
  }else{
    if(errorSpan) {
      parentElem.removeChild(errorSpan);
    }
    return false;
  }
}

//チェックボックスの親要素に change イベントのリスナーを登録(イベントの移譲)
requiredcbDivs.forEach( (parentElem) => {
  parentElem.addEventListener('change', () => {
    isCheckMissing(parentElem);
  })
});
  
//値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す)
const isValueMissing = (elem) => {
  const className = 'required';
  const errorSpan = elem.parentElement.querySelector('.error.' + className);
  if(elem.value.length === 0) {
    if(!errorSpan) {
      //select 要素の場合
      if(elem.tagName === 'SELECT') {
        const errorMessage = getErrorMessage(elem, className, '選択してください', 'を選択してください');
        createErrorSpan(elem, className, errorMessage);
      }else{ //それ以外
        const errorMessage = getErrorMessage(elem, className, '入力は必須です', 'は必須です');
        createErrorSpan(elem, className, errorMessage);
      }
    }
    return true;
  }else{
    if(errorSpan) {
      elem.parentNode.removeChild(errorSpan);
    }
    return false;
  }
}

//required クラスを指定された要素に input イベントを設定(値が変更される都度に検証)
requiredElems.forEach( (elem) => {
  elem.addEventListener('input', () => {
    isValueMissing(elem);
  })
}); 

//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  const errorSpan = document.createElement('span');
  errorSpan.classList.add('error', className);
  errorSpan.setAttribute('aria-live', 'polite');
  errorSpan.textContent = errorMessage;
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  let errorMessage = defaultMessage;
  if(elem.hasAttribute('data-error-' + className)) {
    const dataError = elem.getAttribute('data-error-' + className);
    if(dataError === 'label') {
      if(!isParent) {
        if(elem.parentElement.querySelector('label')) {
          const label = elem.parentElement.querySelector('label').textContent;
          if(label) {
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      }else{
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {
      errorMessage = dataError;
    }
  }
  return errorMessage;
} 

ラジオボタン

ラジオボタンの選択を必須にするには、同じ name 属性の input 要素(type 属性が radio)のどれか1つに検証属性の required 属性を設定するのが簡単ですが、独自の JavaScript で検証する場合は選択状態のラジオボタンがあるかどうかを調べます。

関連項目:選択されたラジオボタンを取得

以下はラジオボタンの選択を必須とする検証を JavaScript を使って実装する例です(チェックボックスの場合とほぼ同じです)。

チェックボックスの場合と同様、親要素に requiredrb というクラスを指定すると、その子要素のラジオボタンの選択を必須にします。

<form name="myForm" novalidate>
  <div class="requiredrb" data-error-requiredrb="お好きな色を1つお選びください">
    <p>色を選択してください</p>
    <input 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 class="requiredrb">
    <p>サイズを選択してください</p>
    <input 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>

isRadioMissing はラジオボタンの親要素を引数に受け取りその子要素のラジオボタンを検証する関数で、ラジオボタンの親要素のメソッドとして querySelector() の引数に :checked 擬似クラスを使ったセレクタを指定して選択されているラジオボタン要素を取得して変数 checked に代入しています。

変数 checked の値が null であれば、ラジオボタンは選択されていないので(すでにエラーメッセージが存在しなければ)エラーメッセージを生成して true を返します。

変数 checked の値が null でなければ、ラジオボタンが選択されているので、すでにエラーメッセージが存在していれば削除して false を返します。

change イベントのリスナーはラジオボタンの各要素ではなく、ラジオボタンの親要素に登録して子要素で発生するイベントを捕捉(イベントの移譲)するようにしています。

送信時の処理では、関数 isRadioMissing に .requiredrb を指定した各要素(ラジオボタンの親要素)を渡して実行し、戻り値が true の場合(ラジオボタンが選択されていない場合)は、preventDefault() で送信を中止します。

//requiredrb クラスが指定されている div 要素(ラジオボタンの親要素)の集まり
const requiredrbDivs = document.querySelectorAll('.requiredrb');  

//親要素を引数に受け取りその子要素のラジオボタンを検証する関数
const isRadioMissing = (parentElem) => {
  //対象のクラス名
  const className = 'requiredrb';
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = parentElem.querySelector('.error.' + className);
  //選択状態の最初のラジオボタン要素を取得
  const checked = parentElem.querySelector('input[type="radio"]:checked');
  //いずれのラジオボタンも選択されていない場合
  if(checked === null) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      //※ 第5引数に true を指定しなければならない(第4引数は使用されないので空文字を指定)
      const errorMessage = getErrorMessage(parentElem, className, '選択してください', '', true);
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      //※ 第4引数に true を指定しなければならない
      createErrorSpan(parentElem, className, errorMessage, true);
    }
    return true;
  }else{
    if(errorSpan) {
      parentElem.removeChild(errorSpan);
    }
    return false;
  }
}
 
//ラジオボタンの親要素に change イベントのリスナーを登録(イベントの移譲)
requiredrbDivs.forEach( (parentElem) => {
  parentElem.addEventListener('change', () => {
    isRadioMissing(parentElem);
  })
});
  
//送信時の処理(検証対象の要素を検証し、要件を満たさない場合は送信を中止)
document.myForm.addEventListener('submit', (e) => { 
  //.requiredrb を指定した要素の子要素のラジオボタンの検証
  requiredrbDivs.forEach( (parentElem) => {
    if(isRadioMissing(parentElem)) {
      e.preventDefault();
    }
  });
});
 
//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  const errorSpan = document.createElement('span');
  errorSpan.classList.add('error', className);
  errorSpan.setAttribute('aria-live', 'polite');
  errorSpan.textContent = errorMessage;
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  let errorMessage = defaultMessage;
  if(elem.hasAttribute('data-error-' + className)) {
    const dataError = elem.getAttribute('data-error-' + className);
    if(dataError === 'label') {
      if(!isParent) {
        if(elem.parentElement.querySelector('label')) {
          const label = elem.parentElement.querySelector('label').textContent;
          if(label) {
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      }else{
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {
      errorMessage = dataError;
    }
  }
  return errorMessage;
} 

選択すると要素を表示

以下は「その他」のラジオボタンを選択すると、テキストフィールドの入力欄を表示する例です。

チェックボックスの場合と同様、 .toggler を指定したラジオボタンを選択すると、その要素の data-togglerTarget 属性で指定された値の id 属性を持つ input 要素を表示し、他のラジオボタンが選択されると非表示にします。

<form name="myForm" novalidate>
  <div class="requiredrb" data-error-requiredrb="お好きな色を1つお選びください">
    <p>色を選択してください</p>
    <input 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 class="requiredrb">
    <p>サイズを選択してください</p>
    <input 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 requiredrbDivs = document.querySelectorAll('.requiredrb');  
//チェックすると 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); 
  });
});
  

//親要素を引数に受け取りその子要素のラジオボタンを検証する関数
const isRadioMissing = (parentElem) => {
  //対象のクラス名
  const className = 'requiredrb';
  //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
  const errorSpan = parentElem.querySelector('.error.' + className);
  //選択状態の最初のラジオボタン要素を取得
  const checked = parentElem.querySelector('input[type="radio"]:checked');
  //いずれのラジオボタンも選択されていない場合
  if(checked === null) {
    if(!errorSpan) {
      //getErrorMessage() を使ってエラーメッセージを作成
      //※ 第5引数に true を指定しなければならない(第4引数は使用されないので空文字を指定)
      const errorMessage = getErrorMessage(parentElem, className, '選択してください', '', true);
      //createErrorSpan() を使ってエラーメッセージ表示する span 要素を生成して追加
      //※ 第4引数に true を指定しなければならない
      createErrorSpan(parentElem, className, errorMessage, true);
    }
    return true;
  }else{
    if(errorSpan) {
      parentElem.removeChild(errorSpan);
    }
    return false;
  }
}
 
//ラジオボタンの親要素に change イベントのリスナーを登録(イベントの移譲)
requiredrbDivs.forEach( (parentElem) => {
  parentElem.addEventListener('change', () => {
    isRadioMissing(parentElem);
  })
});
  
//値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す)
const isValueMissing = (elem) => {
  const className = 'required';
  const errorSpan = elem.parentElement.querySelector('.error.' + className);
  if(elem.value.length === 0) {
    if(!errorSpan) {
      //select 要素の場合
      if(elem.tagName === 'SELECT') {
        const errorMessage = getErrorMessage(elem, className, '選択してください', 'を選択してください');
        createErrorSpan(elem, className, errorMessage);
      }else{ //それ以外
        const errorMessage = getErrorMessage(elem, className, '入力は必須です', 'は必須です');
        createErrorSpan(elem, className, errorMessage);
      }
    }
    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) => {
    //★★★ その要素の親要素が非表示(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 = '';
    }
  });
  //.requiredrb を指定した要素の子要素のラジオボタンの検証
  requiredrbDivs.forEach( (parentElem) => {
    if(isRadioMissing(parentElem)) {
      e.preventDefault();
    }
  });
});
 
//エラーメッセージを表示する span 要素を生成して親要素に追加する関数
const createErrorSpan = (elem, className, errorMessage, isParent) => {
  const errorSpan = document.createElement('span');
  errorSpan.classList.add('error', className);
  errorSpan.setAttribute('aria-live', 'polite');
  errorSpan.textContent = errorMessage;
  if(isParent) {
    elem.appendChild(errorSpan);
  }else{
    elem.parentNode.appendChild(errorSpan);
  }
}

//エラーメッセージを生成する関数 
const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
  let errorMessage = defaultMessage;
  if(elem.hasAttribute('data-error-' + className)) {
    const dataError = elem.getAttribute('data-error-' + className);
    if(dataError === 'label') {
      if(!isParent) {
        if(elem.parentElement.querySelector('label')) {
          const label = elem.parentElement.querySelector('label').textContent;
          if(label) {
            errorMessage = '「' + label + '」' + labelMessage;
          }
        }
      }else{
        console.log('data-error-' + elem.className + '="label" は無効です')
      }
    }else if(dataError) {
      errorMessage = dataError;
    }
  }
  return errorMessage;
} 

サンプル

制約検証 API を使わずに、独自の JavaScript で検証を行う例(サンプル)です。

以下が設定してある制約とその制約を使用する場合に指定するクラス名や属性です。※ 指定するクラスによっては「必須属性」を追加で指定する必要があります。

制限 対象要素 input 要素
対象 type 属性
指定するクラス 必須属性
必須(入力) input, textarea text, tel, email required
必須(選択) select required
必須(選択) input checkbox requiredcb(親要素に指定)
必須(選択) input radio requiredrb(親要素に指定)
パターン 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

パターンの検証

pattern クラスを指定して、data-pattern 属性にパターン文字列または email、tel、zip を指定(詳細

最大文字数と最小文字数

maxlength(最大文字数)、minlength(最小文字数)クラスを指定して、それぞれ data-minlength、data-maxlength に文字数を指定(詳細

最大値と最小値

max(最大値)、min(最小値)クラスを指定して、それぞれ data-min、data-max に値を指定。但し、指定できるのは type 属性が number または range の場合で、数値のみが指定可能。

エラーメッセージ

エラーメッセージは特に指定をしなければ、デフォルトのエラーメッセージが表示されます。カスタムエラーメッセージを表示するには以下のエラーメッセージ用の属性を指定します。

以下はそれぞれの検証でカスタムエラーメッセージを表示する場合に指定するエラーメッセージ用の属性とデフォルトのエラーメッセージ、及び属性に label を指定した場合のメッセージです。

エラーメッセージ用の属性に label 以外の文字列を指定すると、その文字列がカスタムエラーメッセージとして表示されます。

クラス 属性 デフォルト label 指定時
required data-error-required 入力は必須です 「xxxx」は必須です
required
select 要素
data-error-required 選択してください 「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 以下の値を入力ください
requiredcb data-error-requiredcb 選択してください なし
requiredrb data-error-requiredrb 選択してください なし

上記表の 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 属性は何でもかまいません)。

<!-- 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 class="requiredrb" data-error-requiredcb="必ず1つは選択ください">
    <p>色を選択してください(必須)</p>
    <input 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" placeholder="必須">
  </div>
  <div class="requiredrb">
    <p>色を選択してください(必須)</p>
    <input 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 class="requiredcb" data-error-requiredcb="少なくとも1つを選択してください">
    <p>連絡方法を選択してください(必須:複数選択可)</p>
    <input 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 class="requiredcb" data-error-requiredcb="送信するにはチェックを入れてください">
    <input 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つのみであることを前提にしています。

validateMyForm.js(コメントなし)
document.addEventListener('DOMContentLoaded', () => {
  const validationForm = document.getElementsByClassName('validationForm')[0];
  if(validationForm) {
    const preLabel = '「';
    const postLabel = '」';
    const requiredMsg = '入力は必須です';
    const requiredMsg_L = 'は必須です'; 
    const requiredSelect = '選択してください';
    const requiredSelect_L = 'を選択してください';
    const patternMsg = '入力された値が正しくないようです';
    const patternMsg_L = 'の形式が正しくないようです'; 
    const minlengthMsg = '文字以上で入力ください';
    const minlengthMsg_L1 = 'は' ;
    const minlengthMsg_L2 = '文字以上で入力ください';
    const maxlengthMsg = '文字以内で入力ください';
    const maxlengthMsg_L1 = 'は' ;
    const maxlengthMsg_L2 = '文字以内で入力ください';
    const requiredcbMsg = '選択してください';
    const requiredrbMsg = '選択してください';
    const minMsg = '以上の値を入力ください';
    const minMsg_L1 = 'は' ;
    const minMsg_L2 = '以上の値を入力ください';
    const maxMsg = '以下の値を入力ください';
    const maxMsg_L1 = 'は' ;
    const maxMsg_L2 = '以下の値を入力ください';
    const emailRegExp = /^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ui;
    const telRegExp = /^0{1}\d{1,4}-{0,1}\d{1,4}-{0,1}\d{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 requiredcbDivs = document.querySelectorAll('.requiredcb');
    const togglers = document.querySelectorAll('.toggler'); 
    const togglerTargets = [];
    const requiredrbDivs = document.querySelectorAll('.requiredrb'); 
    const createErrorSpan = (elem, className, errorMessage, isParent) => {
      const errorSpan = document.createElement('span');
      errorSpan.classList.add('error', className);
      errorSpan.setAttribute('aria-live', 'polite');
      errorSpan.textContent = errorMessage;
      if(isParent) {
        elem.appendChild(errorSpan);
      }else{
        elem.parentNode.appendChild(errorSpan);
      }
    }
    const getErrorMessage = (elem, className, defaultMessage, labelMessage, isParent) => {
      let errorMessage = defaultMessage;
      if(elem.hasAttribute('data-error-' + className)) { 
        const dataError = elem.getAttribute('data-error-' + className);
        if(dataError === 'label') {
          if(!isParent) {
            if(elem.parentElement.querySelector('label')) {
              const label = elem.parentElement.querySelector('label').textContent;
              if(label) {
                errorMessage = preLabel + label + postLabel + labelMessage;
              }
            }
          }else{
            window.console.log('data-error-' + elem.className + '="label" は無効です');
          }
        }else if(dataError) {
          errorMessage = dataError;
        }
      }
      return errorMessage;
    } 
    const isValueMissing = (elem) => {
      const className = 'required';
      const errorSpan = elem.parentElement.querySelector('.error.' + className);
      if(elem.value.length === 0) {
        if(!errorSpan) {
          if(elem.tagName === 'SELECT') {
            const errorMessage = getErrorMessage(elem, className, requiredSelect, requiredSelect_L);
            createErrorSpan(elem, className, errorMessage);
          }else{ 
            const errorMessage = getErrorMessage(elem, className, requiredMsg, requiredMsg_L);
            createErrorSpan(elem, className, errorMessage);
          } 
        }
        return true;
      }else{
        if(errorSpan) {
          elem.parentNode.removeChild(errorSpan);
        }
        return false;
      }
    }
    requiredElems.forEach( (elem) => {
      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('.error.' + className);
      if(elem.value !=='') {
        if(!pattern.test(elem.value)) {
          if(!errorSpan) {
            const errorMessage = getErrorMessage(elem, className, patternMsg, patternMsg_L);
            createErrorSpan(elem, className, errorMessage);
          }
          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('.error.' + className);
      if(elem.value !=='') {
        const valueLength = getValueLength(elem.value);
        if(valueLength < minlength) {
          if(!errorSpan) {
            const errorMessage = getErrorMessage(elem, className, minlength + minlengthMsg, minlengthMsg_L1 + minlength + minlengthMsg_L2);
            createErrorSpan(elem, className, errorMessage);
          }
          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('.error.' + className);
      if(elem.value !=='') {
        const valueLength = getValueLength(elem.value);
        if(valueLength > maxlength) {
          if(!errorSpan) {
            const errorMessage = getErrorMessage(elem, className, maxlength + maxlengthMsg, maxlengthMsg_L1 +  maxlength + maxlengthMsg_L2);
            createErrorSpan(elem, className, errorMessage);
          }
          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('.error.' + className);
      if(elem.value !=='') {
        const val = parseFloat(elem.value);
        if(val < min) {
          if(!errorSpan) {
            const errorMessage = getErrorMessage(elem, className, min + minMsg, minMsg_L1 + min + minMsg_L2);
            createErrorSpan(elem, className, errorMessage);
          }
          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('.error.' + className);
      if(elem.value !=='') {
        const val = parseFloat(elem.value); 
        if(val > max) {
          if(!errorSpan) {
            const errorMessage = getErrorMessage(elem, className, max + maxMsg, maxMsg_L1 + max + maxMsg_L2);
            createErrorSpan(elem, className, errorMessage);
          }
          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(!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');
        const count = getValueLength(e.currentTarget.value);
        countSpan.textContent = count;
        if(count > dataMaxLength) {
          countSpan.style.setProperty('color', 'red');
        }else{
          countSpan.style.removeProperty('color');
        }
      })
    }
    const isCheckMissing = (parentElem) => {
      const className = 'requiredcb';
      const errorSpan = parentElem.querySelector('.error.' + className);
      const checked = parentElem.querySelector('input[type="checkbox"]:checked');
      if(checked === null) {
        if(!errorSpan) {
          const errorMessage = getErrorMessage(parentElem, className, requiredcbMsg, '', true);
          createErrorSpan(parentElem, className, errorMessage, true);
        }
        return true;
      }else{
        if(errorSpan) {
          parentElem.removeChild(errorSpan);
        }
        return false;
      }
    }
    requiredcbDivs.forEach( (parentElem) => {
      parentElem.addEventListener('change', () => {
        isCheckMissing(parentElem);
      })
    });
    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 isRadioMissing = (parentElem) => {
      const className = 'requiredrb';
      const errorSpan = parentElem.querySelector('.error.' + className);
      const checked = parentElem.querySelector('input[type="radio"]:checked');
      if(checked === null) {
        if(!errorSpan) {
          const errorMessage = getErrorMessage(parentElem, className, requiredrbMsg, '', true);
          createErrorSpan(parentElem, className, errorMessage, true);
        }
        return true;
      }else{
        if(errorSpan) {
          parentElem.removeChild(errorSpan);
        }
        return false;
      }
    }
    requiredrbDivs.forEach( (parentElem) => {
      parentElem.addEventListener('change', () => {
        isRadioMissing(parentElem);
      })
    });
    validationForm.addEventListener('submit', (e) => {
      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 = '';
        }
      });
      requiredcbDivs.forEach( (elem) => {
        if(isCheckMissing(elem)) {
          e.preventDefault();
        }
      });
      requiredrbDivs.forEach( (parentElem) => {
        if(isRadioMissing(parentElem)) {
          e.preventDefault();
        }
      }); 
    });
  }
});
制約検証との併用

前述のサンプルで使用した検証用の 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 class="requiredcb" data-error-requiredcb="少なくとも1つを選択してください">
      <p>連絡方法を選択してください(必須:複数選択可)</p>
      <input 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 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)を読み込んでいます。