JavaScriptでバリデーションチェックを実装してみよう!【バリデーションチェック編(JavaScript)】

チュートリアル

はじめに

この記事は「お問い合わせフォームを作ってみよう」チュートリアルの一部であり、このチュートリアルのバリデーションチェック編(JavaScript)として書かれています。

この記事では、これまでで作成したお問い合わせフォームにJavaScriptでバリデーションチェックを実装していきます。バリデーションチェック編(HTML)で実装したHTMLのバリデーションチェックは不要なので、【CSSスタイリング編】終了時点でのindex.htmlを使用します。

この記事の対象者

  • ウェブ制作初心者の方
  • お問い合わせページを作りたい方
  • JavaScriptでのバリデーションチェックについて学びたい方

この記事で学べること

  • JavaScriptでテキストフィールドの入力を必須にする方法
  • 送信ボタンをクリックする前にバリデーションチェックを行う方法

バリデーションチェックの実装

JavaScriptでのバリデーションチェックの実装は一般的であり、ウェブフォームの仕様によっては複雑な制御を行う場合もあります。

この記事はウェブ制作初心者の方向けなので、あまり複雑にならない程度で実践的なバリデーションチェックを実装していきます。

バリデーションチェックの仕様と手順

今回は以下の条件でバリデーションチェックを実装します。

  • お名前、メールアドレス、お問い合わせ内容、個人情報保護方針を必須にする
  • メールアドレスのフォーマットが正しいかを確認する
  • 値の入力が終わったタイミングで対象のバリデーションチェックを行う
  • バリデーションに失敗したら値が変更される度にバリデーションチェックを行う

また、手順は以下のとおりです。

  1. エラーメッセージのスタイリング
  2. JavaSctiptファイルを作成してHTMLで読み込む
  3. 送信ボタンのクリックイベントを定義する
  4. バリデーションを定義する
  5. バリデーションチェックのタイミングを制御する

それでは始めましょう。

使用するサンプルコード

今回は第三章までのHTMLを使用します。HTMLの内容は以下です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>お問い合わせ</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1>LOGO</h1>
  </header>

  <main>
    <h2>お問い合わせ</h2>
    <form id="form" action="" method="GET">

      <div class="form-item">
        <label for="type" class="form-ttl">お問い合わせの種類</label>
        <div class="select-wrap">
          <select id="type" name="type">
            <option value="learn" selected>ウェブ制作の学び方について</option>
            <option value="article">記事の内容について</option>
            <option value="site">このサイトの使い方について</option>
            <option value="admin">このサイトの管理者について</option>
          </select>
        </div>
      </div>

      <div class="form-item">
        <label for="name" class="form-ttl">お名前<span class="required">必須</span></label>
        <p class="example-txt">例:山田 太郎</p>
        <input type="text" id="name" name="name" required>
      </div>

      <div class="form-item">
        <fieldset>
          <legend class="form-ttl">性別</legend>
          <label for="male"><input type="radio" id="male" name="gender" value="male">男性</label>
          <label for="female"><input type="radio" id="female" name="gender" value="female">女性</label>
        </fieldset>
      </div>

      <div class="form-item">
        <label for="email" class="form-ttl">メールアドレス<span class="required">必須</span></label>
        <p class="example-txt">例:hogehoge@example.com</p>
        <input type="mail" id="email" name="email" required>
      </div>

      <div class="form-item">
        <fieldset>
          <legend class="form-ttl">学習中の言語</legend>
          <label for="html"><input type="checkbox" id="html" name="learning" value="html" checked>HTML</label>
          <label for="css"><input type="checkbox" id="css" name="learning" value="css">CSS</label>
          <label for="javascript"><input type="checkbox" id="javascript" name="learning" value="javascript">JavaScript</label>
          <label for="php"><input type="checkbox" id="php" name="learning" value="php">PHP</label>
        </fieldset>
      </div>

      <div class="form-item">
        <label for="content" class="form-ttl">お問い合わせ内容<span class="required">必須</span></label>
        <textarea id="content" name="content" rows="20" required></textarea>
      </div>

      <div class="policy-check-wrap">
        <label for="policy-check">
          <input
            type="checkbox"
            id="policy-check"
            name="policy-check"
            required>個人情報保護方針に同意します。
        </label>
      </div>

      <div class="btn-wrap">
        <button type="submit" id="submit">お問い合わせ内容を送信</button>
      </div>

    </form>
  </main>

  <footer>&copy;copyright.</footer>
</body>
</html>

まずはエラーメッセージのスタイリングを行います。

エラーメッセージのスタイリング

エラーメッセージは、入力値に問題がある場合にユーザーに伝えるためのものです。エラーメッセージは通常、赤いテキストで表示され、問題があるフォーム部品の近くに配置されます。

エラーメッセージのスタイリングを行うために、まずはバリデーションチェックの対象となるフォーム部品にエラーメッセージを追加します。

<div class="form-item">
  <label for="name" class="form-ttl">お名前<span class="required">必須</span></label>
  <p class="example-txt">例:山田 太郎</p>
  <!-- 以下を追加 -->
  <p class="err-msg">お名前を入力してください</p>
  <input type="text" id="name" name="name" required>
</div>
<div class="form-item">
  <label for="email" class="form-ttl">メールアドレス<span class="required">必須</span></label>
  <p class="example-txt">例:hogehoge@example.com</p>
  <!-- 以下を追加 -->
  <p class="err-msg">メールアドレスを入力してください</p>
  <input type="email" id="email" name="email" required>
</div>
<div class="form-item">
  <label for="content" class="form-ttl">お問い合わせ内容<span class="required">必須</span></label>
  <!-- 以下を追加 -->
  <p class="err-msg">お問い合わせ内容を入力してください</p>
  <textarea id="content" name="content" rows="20" required></textarea>
</div>
<div class="policy-check-wrap">
  <label for="policy-check">
    <!-- 以下を追加 -->
    <p class="err-msg">個人情報保護方針に同意してください</p>
    <input type="checkbox" id="policy-check" name="policy-check" required>個人情報保護方針に同意します。
  </label>
</div>

ここでは、各フォーム部品の上に err-msg というクラス名を付けた p 要素を追加しています。

エラーメッセージのスタイリングは以下です。CSSに追記してください。

/* エラーメッセージのスタイル */
.form-item,
.policy-check-wrap {
  position: relative;
}
.err-msg {
  position: absolute;
  left: 2em;
  bottom: -0.375em;
  color: #f06060;
  font-size: 0.875em;
  font-weight: bold;
  border: 2px solid #f06060;
  border-radius: 0.25em;
  padding: 0.25em 1em;
  background: #fffafa;
  display: none;
}
.err-msg::before {
  content: "";
  position: absolute;
  top: -1em;
  left: 0.5em;
  border: 0.5em solid transparent;
  border-bottom: 0.5em solid #f06060;
}
.err-msg::after {
  content: "";
  position: absolute;
  top: -0.875em;
  left: 0.5em;
  border: 0.5em solid transparent;
  border-bottom: 0.5em solid #FFF;
}

.err-msg.show+input[type="text"],
.err-msg.show+input[type="email"],
.err-msg.show+textarea {
  border: 1px solid #f06060;
  background: #fffafa;
}

.err-msg.show+input[type="text"]:focus,
.err-msg.show+input[type="email"]:focus,
.err-msg.show+textarea:focus {
  border: 1px solid #f06060;
  outline: 1px solid #f06060;
  background: #fffcfc;
}

.policy-check-wrap .err-msg {
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  bottom: -3em;
}
.policy-check-wrap .err-msg::before,
.policy-check-wrap .err-msg::after {
  left: 50%;
  transform: translateX(-50%);
}

ブラウザで確認すると以下のように表示されます。 .err-msg 「 display: none; 」が指定してあるので、この記述をコメントアウトすることで確認できます。

.err-msg {
  …
  /* display: none; */
}

確認ができたら「 display: none; 」のコメントアウトは解除しておきましょう。

JavaSctiptファイルを作成してHTMLで読み込む

まずは index.html と同じ階層に validation.js というファイルを作成します。また、HTMLのheadタグ内に以下を追記します。

<head>
  …
  <script src="validation.js" defer></script>
</head>

head 要素内でJavaScriptファイルを読み込む場合、script 要素に defer 属性を指定しないとJavaScript側でHTMLの要素を取得できないので注意しましょう。

続いて、送信ボタンのクリックイベントを定義します。

送信ボタンのクリックイベントを定義する

基本的なバリデーションチェックのタイミングとして、送信ボタンがクリックされた時に処理を行うように実装することは最低限必要になります。

まずは送信ボタンのクリックイベントを定義し、後のバリデーションチェックの実行は送信ボタンのクリックイベント内で行います。このためには、送信ボタンをJavaScriptで取得する必要があります。

validation.js に以下を記述します。

const submitBtn = document.getElementById('submit');

/*
  ===========================
  送信ボタンのクリックイベント
  ===========================
*/
submitBtn.addEventListener('click', function (event) {
  // バリデーションチェックの実行はここに記述します。
});

送信ボタンには「 id=”submit” 」を指定しているので、「 document.getElementById(‘submit’); 」で送信ボタンの要素を取得することができます。また、取得した送信ボタンは変更する必要がないので「 const 」で定義して定数として扱います。

submitBtn に取得した送信ボタンを代入し、それに対して addEventListener でクリックイベントを定義しています。

続いて、バリデーションを定義していきます。

バリデーションを定義する

ここで実装するバリデーションチェックのルールは以下でした。

  • お名前、メールアドレス、お問い合わせ内容、個人情報保護方針を必須にする
  • メールアドレスのフォーマットが正しいかを確認する

順番に実装していきます。

お名前、メールアドレス、お問い合わせ内容、個人情報保護方針を必須にする

入力を必須にする項目は、バリデーションチェックを実行した際に入力値を確認する必要があります。これらの要素をJavaScriptで取得することで、各フォーム部品に入力された値を確認することができます。

ここでは以下の手順で行います。

  • 各フォーム部品を取得する
  • 要素と値を取得できているか確認する
  • バリデーションチェックを行うための関数を定義する
  • バリデーションチェックの関数を実行する
各フォーム部品を取得する

各フォーム部品には id 属性を指定しているので、送信ボタンと同じように取得することができます。

定数や変数は同じところにまとめて宣言したほうがコードが見やすいので、送信ボタンの取得の下に以下を追記しましょう。コメントもしておくとなお良いです。

/*
  ==============
  HTML要素の取得
  ==============
*/
const submitBtn = document.getElementById('submit');
// 以下を追記
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const contentInput = document.getElementById('content');
const policyCheckInput = document.getElementById('policy-check');

これで各フォーム部品の入力値を取得できます。

要素と値を取得できているか確認する

例としてお名前欄の入力値を確認してみます。確認方法はいろいろありますが、 alert メソッドを使用することが一番手軽なのでここでは alert メソッドで行います。

/*
  ===========================
  送信ボタンのクリックイベント
  ===========================
*/
submitBtn.addEventListener('click', function(event) {
  alert(nameInput.value);
});

送信ボタンのクリックイベントに「 alert(nameInput.value); 」と記述しました。これにより、送信ボタンをクリックすると window オブジェクトの alert メソッドが実行されます。

windowオブジェクトのメソッドやプロパティを使用する際、windowの部分は省略することができます。

したがって、「 alert(nameInput.value); 」は「 window.alert(nameInput.value); 」と同じ意味になります。

また、 alert メソッドの引数には「 nameInput.value 」を指定しています。 document.getElementById で取得した要素は HTMLElement として扱うことができ、 value プロパティを指定することで要素の値を取得できます。

送信ボタンをクリックすることで以下のように表示されます。

値を確認できたら「 alert(nameInput.value); 」の記述は削除するかコメントアウトしておきましょう。

続いて、各フォーム部品に対してどのようなバリデーションチェックを行うかを定義していきます。

バリデーションチェックを行うための関数を定義する

フォーム部品の入力値を取得し、取得した値に対して条件分岐を行うことでバリデーションチェックの実装ができます。

送信ボタンのクリックイベント内に記述するよりも関数化したほうが管理しやすいので、各フォーム部品のバリデーションチェック用の関数を定義します。

まずはお名前欄から行います。

/*
  ==========================
  バリデーションチェックの関数
  ==========================
*/
// お名前のバリデーションチェック
function validateName() {
  let name = nameInput.value;
  let errMsgElem = nameInput.previousElementSibling;

  if (!name) {
    errMsgElem.classList.add('show');
    nameInput.focus();
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

関数と変数の宣言から説明します。

function validateName() {
  let name = nameInput.value;
  let errMsgElem = nameInput.previousElementSibling;
  …
}

上記では、 validateName という名前の関数を宣言しています。また、この関数内で扱う変数として name 変数と errMsgElem 変数を定義しています。

name 変数には「 nameInput.value 」の値を代入しており、この変数を使用して条件分岐を行います。

errMsgElem 変数には「 nameInput.previousElementSibling 」でお名前入力欄の前の要素を代入しています。お名前入力欄の前の要素は err-msg クラスを指定した p 要素です。

<p class="err-msg">お名前を入力してください</p>
<input type="text" id="name" name="name" required>

変数を関数内で宣言することでローカルスコープとして扱われるため、関数の外からこの変数の値の取得や変更ができなくなります。

一見不便に感じるかもしれませんが、変数名の衝突を防ぐことができるのでグローバルスコープとして扱うよりも保守性が高く、予期せぬバグを防ぐ効果があります。

次に条件分岐の説明です。

if (!name) {
  …
  return false;
}
…
return true;

上記では、 name 変数の値を使用して条件分岐を行っています。条件式を「 (!name) 」とすることで、 name 変数に代入された値が空の文字列だった場合の処理を定義することができます。この条件分岐が必須項目実装の実体です。

ここでは、 validationName() 関数を実行した際、 name 変数の値が空の文字列だった場合に「 false 」を、何らかの値が代入されていた場合には「 true 」を戻り値として返すようにしています。

次に if 文内の処理を説明します。「 (!name) 」条件式が成立した場合、以下の処理が実行されます。

if (!name) {
  errMsgElem.classList.add('show');
  nameInput.focus();
  return false;
}

errMsgElem 変数に代入された要素は HTMLElement オブジェクトであり、 classList プロパティを介してクラス属性の追加・削除・確認などを行うことができます。「 errMsgElem.classList.add(‘show’); 」の実行により、 errMsgElem show クラスを追加しています。

errMsgElem にはもともと err-msg というクラスが設定されており、 err-msg 「 display: none; 」で非表示にしていました。エラーメッセージのスタイルは以下のように定義してあるため、 show クラスを追加することで display プロパティを動的に変更して表示させることができます。

.err-msg {
  …
  display: none;
}

.err-msg.show {
  display: block;
}

また、対象の入力フォームにフォーカスが当たるようにすると親切なので、以下ではそれを行っています。

nameInput.focus();

バリデーションに成功したらエラーメッセージを非表示にしたいので、 if 文の外の return 前で show クラスを外しています。

function validateName() {
  …
  if (!name) {
    …
  }
  errMsgElem.classList.remove('show');
  return true;
}

これでお名前入力欄の入力を必須にすることができました。

メールアドレス、お問い合わせ入力も同じ方法で実装していきます。

// メールアドレスのバリデーションチェック
function validateEmail() {
  let email = emailInput.value;
  let errMsgElem = emailInput.previousElementSibling;

  if (!email) {
    errMsgElem.classList.add('show');
    emailInput.focus();
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

// お問い合わせ内容のバリデーションチェック
function validateContent() {
  let content = contentInput.value;
  let errMsgElem = contentInput.previousElementSibling;

  if (!content) {
    errMsgElem.classList.add('show');
    contentInput.focus();
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

個人情報保護方針は少し書き換える必要があります。

// 個人情報保護方針のバリデーションチェック
function validatePolicyCheck() {
  // valueをcheckedに変更
  let policyCheck = policyCheckInput.checked;
  let errMsgElem = policyCheckInput.previousElementSibling;
  
  if (!policyCheck) {   
    errMsgElem.classList.add('show');

    return false;
  }

  errMsgElem.classList.remove('show');

  return true;
}

チェックボックスはチェックの有無にかかわらず value 属性の値が定義されているので、チェックが入っていなくても有効な値として処理されてしまいます。ここでは、 value プロパティではなく checked プロパティを取得しています。

checked プロパティの値はチェックボックスにチェックが入っていれば「true」、入っていなければ「false」になるので必須選択の条件として成立します。

これで各フォーム部品を必須項目にするための関数ができました。

現状では関数を定義しただけなので実行はされません。次はここで定義した関数を実行して、正しく動作するかを確認しましょう。

バリデーションチェックの関数を実行する

ここでは、送信ボタンのクリックイベント内でバリデーションチェックの関数を実行してみます。

クリックイベントに以下を記述します。

/*
  ===========================
  送信ボタンのクリックイベント
  ===========================
*/
submitBtn.addEventListener('click', function(event) {

  if (!validateName()) {
    event.preventDefault();
  } else if (!validateEmail()) {
    event.preventDefault();
  } else if (!validateContent()) {
    event.preventDefault();
  } else if (!validatePolicyCheck()) {
    event.preventDefault();
  }

});

送信ボタンのクリックイベントには関数を定義しており、 event オブジェクトを引数として受け取っています。

submitBtn.addEventListener('click', function(event) { … });

event オブジェクトはイベントハンドラ内で使用でき、イベントの制御などを行うためのプロパティやメソッドが定義されています。

「 event.preventDefault(); 」を実行することで、クリックイベントのデフォルトの動作を中断することができます。

ここではバリデーションの関数を if 文の条件式内で順番に実行しており、実行結果が false だった場合(値が入力されていない場合)に event.preventDefault(); を実行することで処理を中断しています。

処理を中断しなかった場合、以下のようにHTMLの require 属性によるアラートも表示されるので注意しましょう。

必須項目が入力されていればフォームが送信されます。ちなみにこのサンプルコードは form 要素の action 属性をGETにしているので、送信結果がクエリ文字列としてブラウザのアドレスバーに表示されます。

お問い合わせの内容:ウェブ制作の学び方について、お名前:山田 太郎、性別:男性、メールアドレス:hoge@example.comを指定した例(他の項目も入力していますが見切れているため割愛します)。

続いて、メールアドレスのフォーマットが正しいかを確認するためのバリデーションを定義します。

メールアドレスのフォーマットが正しいかを確認する

メールアドレスが意図した形式で入力されているかを確認するには、正規表現を使用することが一般的です。

メールアドレスの入力値が正規表現に合致しているかを確認するために、まずは定数として正規表現を定義します。メールアドレスのバリデーション関数に以下を追記しましょう。

// メールアドレスのバリデーションチェック
function validateEmail() {
  let email = emailInput.value;
  let errMsgElem = emailInput.previousElementSibling;

  // 以下を追記
  const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  …
}

emailPattern という定数を定義し、正規表現でメールアドレスのフォーマットを代入しています。

正規表現についての詳しい説明は割愛しますが、上記の正規表現はHTMLで「 type=”email” 」を指定した場合より少し厳密に定義しています。

例えば、「 type=”email” 」でのバリデーションチェックは「 hoge@example 」を許容します。しかし、上記の正規表現では@マークの後ろには何らかの半角英数字とそれに続く「 . 」が必要であり、さらに「 . 」の後ろには半角のアルファベット2文字以上である必要があるため「 hoge@example 」を許容しません。

バリデーションチェックに使用する正規表現はフォームの仕様やクライアントの要望などによって適切に定義する必要があり、ここの正規表現はあくまで例として紹介しています。

誤った正規表現を定義すると「 普段使っているメールアドレスが入力できない 」といった不具合が発生する可能性があるので、正規表現を使用したバリデーションチェックは十分なテストを行う必要があります。

続いてパターンに合致しない場合の処理を記述します。

エラーメッセージの表示などの処理は入力が空だった場合と同じなので、条件式を「 || 」で追加することでこのバリデーションにも適用できます。

if (!email || !emailPattern.text(email)) {
  errMsgElem.classList.add('show');
  emailInput.focus();
  return false;
}

条件式を「 (!email || !emailPattern.text(email)) 」に変更しました。

続いて、入力値が空の場合と同じエラーメッセージだと違和感を感じるのでこれを変更します。

if (!email || !emailPattern.text(email)) {
  errMsgElem.classList.add('show');
  emailInput.focus();

  // 以下を追加
  if (!emailPattern.test(email)) {
    errMsgElem.textContent = 'メールアドレスの形式が正しくありません';
  }

  return false;
}

if 文を入れ子にすることで更に条件分岐を行っています。 return の後ろに記述すると実行されないので注意しましょう。

メールアドレスのフォーマットを確認するためのバリデーションが実装できました。

次に、バリデーションチェックを行うタイミングを制御してみましょう。

バリデーションチェックのタイミングを制御する

現状では、処理を確認するために送信ボタンがクリックされた場合にのみバリデーションチェックを実行しています。これだけでは、ユーザーが送信ボタンをクリックするまで入力の誤りに気付けないという不便があります。

JavaScriptによるバリデーションチェックはユーザビリティを向上させる目的で実装されることが一般的なのでもう少し手を加えていきます。

ここでは、以下の仕様に基づいてバリデーションチェックを行うタイミングを制御します。

  • 値の入力が終わったタイミングで対象のバリデーションチェックを行う
  • バリデーションに失敗したら値が変更される度にバリデーションチェックを行う

それでは実装していきましょう。

値の入力が終わったタイミングで対象のバリデーションチェックを行う

値の入力が終わったタイミングを少し具体的に定義すると、「入力が終わって他の入力フォームにフォーカスが変わった場合」、「対象の入力フォーム以外の要素をクリックした場合」と考えることができます。

いずれも「対象の入力フォームからフォーカスが外れた場合」と解釈でき、これは blur イベントを定義することで実装できます。

/*
  ===============================================
  値の入力が終わった時にバリデーションチェックを実行
  ===============================================
*/
nameInput.addEventListener('blur', validateName);
emailInput.addEventListener('blur', validateEmail);
contentInput.addEventListener('blur', validateContent);

個人情報保護方針の同意はチェックボックスなのでフォーカスが当たるとは限りません。 blur イベントでは意図した挙動にならない場合が考えられるので、 blur ではなく change イベントで実装します。

policyCheckInput.addEventListener('change', validatePolicyCheck);

バリデーションに失敗したら値が変更される度にバリデーションチェックを行う

メールアドレスの入力欄はフォーマットが決まっているため、ユーザーが入力を失敗しやすいフォーム部品のひとつです。入力が正しいかどうかをユーザーが素早く知るためにも、一度入力に失敗した場合は文字を入力する度にバリデーションチェックを行うと親切かも知れません。

ここでは input イベントを定義して実装します( input イベントの登録は過剰と判断されるケースも考えられますが、この記事はJavaScriptの学習を目的としているのでこのように実装します)。

/*
  ============================================
  値が変更される度にバリデーションチェックを実行
  ============================================
*/
emailInput.addEventListener('input', function() {
  // 文字が入力された場合の処理
});

input イベントのコールバック関数に直接 validateEmail() 関数を指定すると、入力の失敗に関わらず文字が入力されたタイミングでバリデーションチェックが実行されてしまいます。

入力の途中にエラーが表示されるとユーザーは嫌な気分になるので、条件分岐で「入力に失敗した場合」のみにバリデーションチェックを行うようにします。

/*
  ============================================
  値が変更される度にバリデーションチェックを実行
  ============================================
*/
emailInput.addEventListener('input', function() {
  if (emailInput.errFlag) {
    validateEmail();
  }
});

if 文の条件式に「 (emailInput.errFlag) 」を記述していますが、 emailInput HTMLElement であり、 errFlag という名前のプロパティは存在しないのでこれを定義する必要があります。

validateEmail() 関数内に以下を追加します。

// メールアドレスのバリデーションチェック
function validateEmail() {
  let email = emailInput.value;
  let errMsgElem = emailInput.previousElementSibling;

  const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

  // 以下を追加
  emailInput.errFlag = false;

  if (!email || !emailPattern.text(email)) {
    // 以下を追加
    emailInput.errFlag = true;
    …
  }
  …
}

誤解を恐れずに言えば HTMLElement もただのオブジェクトなので、通常のオブジェクトと同じようにプロパティを追加できます。

emailInput.errFlag = false;

if (!email || !emailPattern.text(email)) {
  emailInput.errFlag = true;
  …
}

上記では emaiInput に対して errFlag という独自のプロパティを定義し、デフォルト値として「 false 」を代入しています。また、バリデーションに失敗したタイミングで errFlag 「 true 」としています。

errFlag emailInput に登録した input イベントのコールバック関数内で使用され、 effFlag の値によって validateEmail() 関数を実行するかどうかを制御しています。

if (emailInput.errFlag) {
  validateEmail();
}

これにより、一度バリデーションに失敗するまでは input イベント内の validateEmail() 関数は実行されなくなり、よりユーザビリティの高い実装を行うことができました。

以上でJavaScriptでのバリデーションチェックの実装は完了です。全体のコードは以下です。

サンプルコードの全体

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>お問い合わせ</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1>LOGO</h1>
  </header>

  <main>
    <h2>お問い合わせ</h2>
    <form id="form" action="" method="GET">

      <div class="form-item">
        <label for="type" class="form-ttl">お問い合わせの種類</label>
        <div class="select-wrap">
          <select id="type" name="type">
            <option value="learn" selected>ウェブ制作の学び方について</option>
            <option value="article">記事の内容について</option>
            <option value="site">このサイトの使い方について</option>
            <option value="admin">このサイトの管理者について</option>
          </select>
        </div>
      </div>

      <div class="form-item">
        <label for="name" class="form-ttl">お名前<span class="required">必須</span></label>
        <p class="example-txt">例:山田 太郎</p>
        <p class="err-msg">お名前を入力してください</p>
        <input type="text" id="name" name="name" required>
      </div>

      <div class="form-item">
        <fieldset>
          <legend class="form-ttl">性別</legend>
          <label for="male"><input type="radio" id="male" name="gender" value="male">男性</label>
          <label for="female"><input type="radio" id="female" name="gender" value="female">女性</label>
        </fieldset>
      </div>

      <div class="form-item">
        <label for="email" class="form-ttl">メールアドレス<span class="required">必須</span></label>
        <p class="example-txt">例:hogehoge@example.com</p>
        <p class="err-msg">メールアドレスを入力してください</p>
        <input type="mail" id="email" name="email" required>
      </div>

      <div class="form-item">
        <fieldset>
          <legend class="form-ttl">学習中の言語</legend>
          <label for="html"><input type="checkbox" id="html" name="learning" value="html" checked>HTML</label>
          <label for="css"><input type="checkbox" id="css" name="learning" value="css">CSS</label>
          <label for="javascript"><input type="checkbox" id="javascript" name="learning" value="javascript">JavaScript</label>
          <label for="php"><input type="checkbox" id="php" name="learning" value="php">PHP</label>
        </fieldset>
      </div>

      <div class="form-item">
        <label for="content" class="form-ttl">お問い合わせ内容<span class="required">必須</span></label>
        <p class="err-msg">お問い合わせ内容を入力してください</p>
        <textarea id="content" name="content" rows="20" required></textarea>
      </div>

      <div class="policy-check-wrap">
        <label for="policy-check">
        <p class="err-msg">個人情報保護方針に同意してください</p>
          <input
            type="checkbox"
            id="policy-check"
            name="policy-check"
            required>個人情報保護方針に同意します。
        </label>
      </div>

      <div class="btn-wrap">
        <button type="submit" id="submit">お問い合わせ内容を送信</button>
      </div>

    </form>
  </main>

  <footer>&copy;copyright.</footer>
</body>
</html>

style.css

@charset "UTF-8";

/* ================
基本設定
================ */
body {
  font-family: "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Arial", "Yu Gothic", "Meiryo", sans-serif;
  background-color: #fefeef;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
  padding: 0;
}

/* ================
ページ全体のスタイル
================ */
header {
  padding: .5em 1em;
  background-color: #fff;
  box-shadow: -5px 1px 5px rgba(0, 0, 0, 0.1);
}

main {
  width: calc(100% - 2em);
  max-width: 768px;
  margin: 0 auto 6em;
}

footer {
  text-align: center;
  padding: 1em 0;
  background-color: #fff;
  box-shadow: -5px -1px 5px rgba(0, 0, 0, 0.1);
}

h2 {
  text-align: center;
  padding: 2em 0;
}

form {
  background-color: white;
  box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
  padding: 2em 1em;
}

.form-ttl {
  font-weight: bold;
  display: block;
  width: fit-content;
  margin-bottom: 0.5em;
}

.form-item {
  padding: 1em 0.5em 2em;
}

.form-item {
  border-bottom: 1px solid #bba;
}

fieldset label {
  font-weight: 500;
}

.required {
  font-size: 0.75em;
  font-weight: bold;
  color: #f03030;
  margin-left: 1em;
}

.example-txt {
  color: #888;
  font-size: 0.875em;
  margin-bottom: .5em;
}

.policy-check-wrap {
  text-align: center;
  margin: 2em 0 3em;
  font-size: 0.875em;
}

.btn-wrap {
  text-align: center;
}

@media screen and (min-width: 768px) {
  main {
    width: calc(100% - 4em);
  }

  form {
    padding: 3em;
  }
  
  .form-ttl {
    font-size: 1.25em;
  }
  
  .form-item {
    padding: 1em 1em 2em;
  }
  
  .policy-check-wrap {
    font-size: 1em;
  }
}

/* ================
フォーム部品
================ */
/* フォーム部品のリセット */
fieldset,
legend,
button,
select,
input[type="text"],
input[type="email"],
input[type="checkbox"],
input[type="radio"],
textarea {
  border: none;
  outline: none;
  background: none;
  appearance: none;
}

button:focus,
select:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="checkbox"]:focus,
input[type="radio"]:focus,
textarea:focus {
  outline: none;
}

/* フォーム部品の基本設定 */
select,
option,
input,
textarea {
  font-family: "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Arial", "Yu Gothic", "Meiryo", sans-serif;
  font-size: 1em;
  font-weight: 500;
}

textarea {
  overflow: auto;
  resize: none;
}

label,
input[type="checkbox"],
input[type="radio"],
button {
  cursor: pointer;
}

fieldset {
  display: flex;
  flex-wrap: wrap;
  gap: 1em;
}

input[type="text"],
input[type="email"],
textarea {
  width: 100%;
}

/* ドロップダウンメニューのスタイル */
select {
  border: 1px solid #bba;
  border-radius: 0.25em;
  padding: 1em 2em 1em 1em;
}

select:focus {
  outline: 1px solid #bba;
}

.select-wrap {
  position: relative;
  width: fit-content;
}

.select-wrap::before {
  content: "▼";
  position: absolute;
  color: #bba;
  right: 0.5em;
  top: 50%;
  transform: translateY(-50%);
}

/* テキストフィールドのスタイル */
input[type="text"],
input[type="email"],
textarea {
  border: 1px solid #bba;
  border-radius: 0.25em;
  padding: 0.5em;
  background: #fefeef;
  box-shadow: inset 1px 1px 3px #c8c4aa;
}

input[type="text"]:focus,
input[type="email"]:focus,
textarea:focus {
  outline: 1px solid #c8c4aa;
  background: #fffff2;
}

/* ラジオボタン・チェックボックス
   共通ののスタイル           */
input[type="radio"],
input[type="checkbox"] {
  border: 1px solid #bba;
  width: 1.25em;
  height: 1.25em;
  position: relative;
  top: 0.25em;
  margin-right: 0.5em;
}

input[type="radio"]:focus,
input[type="checkbox"]:focus {
  outline: 1px solid #bba;
}

/* ラジオボタンのスタイル */
input[type="radio"] {
  border-radius: 50%;
}

input[type="radio"]:checked {
  border-width: 0.375em;
}

/* チェックボックスのスタイル */
input[type="checkbox"] {
  border-radius: 0.25em;
}

input[type="checkbox"]::before {
  content: "";
  display: block;
  position: absolute;
  top: -0.125em;
  left: 0.375em;
  width: 0.5em;
  height: 1em;
  border-bottom: 0.25em solid #bba;
  border-right: 0.125em solid #bba;
  border-radius: 0.25em;
  transform: rotate(45deg);
  visibility: hidden;
}

input[type="checkbox"]:checked::before {
  visibility: visible;
}

/* 送信ボタンのスタイル */
button {
  padding: 1em 2em;
  font-weight: bold;
  letter-spacing: 0.05em;
  border-radius: .5em;
  background-color: #446;
  box-shadow: 1px 1px 3px #446;
  color: #fff;
  transition:
    color .3s ease,
    background-color .3s ease,
    outline-color .3s ease;
}

button:hover {
  color: #446;
  background-color: #fff;
  outline: 2px solid #446;
}

/* エラーメッセージのスタイル */
.form-item,
.policy-check-wrap {
  position: relative;
}

.err-msg {
  position: absolute;
  left: 2em;
  bottom: -0.375em;
  color: #f06060;
  font-size: 0.875em;
  font-weight: bold;
  border: 2px solid #f06060;
  border-radius: 0.25em;
  padding: 0.25em 1em;
  background: #fffafa;
  display: none;
}

.err-msg::before {
  content: "";
  position: absolute;
  top: -1em;
  left: 0.5em;
  border: 0.5em solid transparent;
  border-bottom: 0.5em solid #f06060;
}

.err-msg::after {
  content: "";
  position: absolute;
  top: -0.875em;
  left: 0.5em;
  border: 0.5em solid transparent;
  border-bottom: 0.5em solid #FFF;
}

.err-msg.show+input[type="text"],
.err-msg.show+input[type="email"],
.err-msg.show+textarea {
  border: 1px solid #f06060;
  background: #fffafa;
}

.err-msg.show+input[type="text"]:focus,
.err-msg.show+input[type="email"]:focus,
.err-msg.show+textarea:focus {
  border: 1px solid #f06060;
  outline: 1px solid #f06060;
  background: #fffcfc;
}

.policy-check-wrap .err-msg {
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  bottom: -3em;
}

.policy-check-wrap .err-msg::before,
.policy-check-wrap .err-msg::after {
  left: 50%;
  transform: translateX(-50%);
}

validation.js

/*
  ==============
  HTML要素の取得
  ==============
*/
const submitBtn = document.getElementById('submit');
// 以下を追記
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const contentInput = document.getElementById('content');
const policyCheckInput = document.getElementById('policy-check');

/*
  ===========================
  送信ボタンのクリックイベント
  ===========================
*/
submitBtn.addEventListener('click', function (event) {

  if (!validateName()) {
    event.preventDefault();
  } else if (!validateEmail()) {
    event.preventDefault();
  } else if (!validateContent()) {
    event.preventDefault();
  } else if (!validatePolicyCheck()) {
    event.preventDefault();
  }

});

/*
  ==========================
  バリデーションチェックの関数
  ==========================
*/
// お名前のバリデーションチェック
function validateName() {
  let name = nameInput.value;
  let errMsgElem = nameInput.previousElementSibling;

  if (!name) {
    errMsgElem.classList.add('show');
    nameInput.focus();
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

// メールアドレスのバリデーションチェック
function validateEmail() {
  let email = emailInput.value;
  let errMsgElem = emailInput.previousElementSibling;

  const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

  emailInput.errFlag = false;

  if (!email || !emailPattern.text(email)) {
    emailInput.errFlag = true;
    errMsgElem.classList.add('show');
    emailInput.focus();

    if (!emailPattern.test(email)) {
      errMsgElem.textContent = 'メールアドレスの形式が正しくありません';
    }

    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

// お問い合わせ内容のバリデーションチェック
function validateContent() {
  let content = contentInput.value;
  let errMsgElem = contentInput.previousElementSibling;

  if (!content) {
    errMsgElem.classList.add('show');
    contentInput.focus();
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

// 個人情報保護方針のバリデーションチェック
function validatePolicyCheck() {
  let policyCheck = policyCheckInput.checked;
  let errMsgElem = policyCheckInput.previousElementSibling;
  
  if (!policyCheck) {   
    errMsgElem.classList.add('show');
    return false;
  }

  errMsgElem.classList.remove('show');
  return true;
}

/*
  ===============================================
  値の入力が終わった時にバリデーションチェックを実行
  ===============================================
*/
nameInput.addEventListener('blur', validateName);
emailInput.addEventListener('blur', validateEmail);
contentInput.addEventListener('blur', validateContent);
policyCheckInput.addEventListener('change', validatePolicyCheck);

/*
  ============================================
  値が変更される度にバリデーションチェックを実行
  ============================================
*/
emailInput.addEventListener('input', function() {
  if (emailInput.errFlag) {
    validateEmail();
  }
});

次のステップ

これまでで作成したお問い合わせフォームにJavaScriptでバリデーションチェックを実装しました。

この記事のコードはウェブ制作初心者の方が理解しやすいようにあえて冗長な記述になっています。

次のステップの「お問い合わせフォームを作ってみよう【リファクタリング編】」では、リファクタリングの基本的な考え方について学び、この記事で作成したJavaScriptコードを保守性や効率を意識して改修します。

リファクタリングを行うことはプログラミングの学習においても効率的なので、ぜひJavaScriptの理解に役立てていただけると幸いです。

タイトルとURLをコピーしました