コードをリファクタリングしてコードの品質を高めよう!【お問い合わせフォームを作ってみよう:リファクタリング編】

チュートリアル

はじめに

この記事は「お問い合わせフォームを作ってみよう」チュートリアルの一部であり、このチュートリアルのリファクタリング編として書かれています。

この記事では、リファクタリングの基本的な考え方について学び、【バリデーションチェック編(JavaScript)】で作成したJavaScriptコードを保守性や再利用性を意識して改修します。

リファクタリングは既存のコードを改善し、より効率的で読みやすい形に修正する作業です。リファクタリングを行うことでコードの品質を向上させ、バグの発生リスクの減少や保守性を高めることができます。

この記事の対象者

  • ウェブ制作初心者の方
  • リファクタリングの方法について学びたい方
  • プログラミングのスキルを高めたい方

この記事で学べること

  • リファクタリングの概要とメリット
  • リファクタリングの注意点
  • リファクタリングの方法

リファクタリングについて

リファクタリングは既存のコードを再構築して、その品質や保守性を向上させるためのプロセスです。

プログラミングは処理が複雑になるほどコードの記述量が増え、理解しづらくなることがよくあります。また、新しい要件に合わせてコードを修正しなければいけない場合もあり、このような状況で、リファクタリングはコードを「 清潔に保つ 」手段として役立ちます。

リファクタリングの主な目的は、コードの品質を向上させ、保守性を高めることです。コードをより読みやすく、バグを減少させ、新しい機能の追加や変更を行いやすくします。

具体的な改善点

リファクタリングでは、以下のような改善点を考えます。

  • 冗長なコードの削除:不要なコードやコメントを削除することで、コードをスッキリさせます。これにより、他の開発者がコードを理解しやすくなります。
  • 名前やコメントのわかりやすさの向上:変数、関数の名前、コメントが適切かを考え、必要であれば修正します。適切な名前を使うことで、コードの意図が明確になり保守性が向上します。
  • インデントやスペースの整形:適切なインデントとスペースを使うことで、コードの構造が視覚的に理解しやすくなります。インデントやスペースが適切でなければバグの原因を見つけるのが難しくなります。
  • 変数や関数の分割:一つの大きな関数を小さな関数に分割することで、各関数が単一の役割として機能し、理解がしやすくなります。
  • 再利用可能なコードの抽出:似たようなコードが複数の場所で使われている場合、それを関数化して重複を削減します。これにより、変更が必要な場合に一か所の修正で済むようになります。

これらの改善点を意識してリファクタリングを行うことで、メンテナンスしやすく、バグの発生リスクを抑え、コードの品質を向上させることができます。

リファクタリングの注意点

リファクタリングはコードの品質を向上させるための重要なプロセスですが、正常に動作しているコードを書き換えることになるので、慎重に行わないとかえってバグの発生要因にもなりえます。

リファクタリングを行う際の注意点は以下です。

  • 小さなステップで進める:大規模な変更を一度に行うのではなく、小さなステップで進めます。各ステップで動作に問題がないかを確認しながら修正を行うことで、仮に問題があっても原因の特定が容易になります。
  • 目的を明確にする:各ステップでの目的を明確にします。コードを修正していると他にも修正したいところに気付くことがありますが、それは別のタスクとして処理するほうが効率よくリファクタリングを行えます。
  • バックアップを取る:コードをリファクタリングする前にバックアップを取りましょう。バックアップがあれば、何か問題が発生した場合に元のコードに戻すことができます。また、Git等のバージョン管理システムを使用することで変更履歴を管理でき、必要な場合にロールバック(コードをある地点まで戻す)することができます。

チーム開発でのプロジェクトであればメンバーに通知したり、フィードバックをもらうことも重要です。個人開発においても上に挙げたポイントを意識してリファクタリングを行うことをオススメします。

リファクタリングを行うことでプログラミングの理解を深めることができ、プログラミングスキルの向上が期待できます。初心者の方でも少しずつ取り組むことで、より良いコードを書く能力を身に付けることができるでしょう。

リファクタリングをしてみよう

リファクタリングを行う前に既存のコードはコピーなどをしてバックアップを取っておきましょう。

前回で作成した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();
  }
});

このコードのリファクタリングを行っていきます。

リファクタリングの下準備

まずは下準備として、コードの問題点の抽出とリファクタリングの目的を明確にします。

問題点の抽出

まずは、このコードの問題点を抽出してどのような修正を行うかを考えます。主な問題点は以下です。

  • for 文などの繰り返し処理がないので記述の重複が多い
  • エラーメッセージがバリデーション関数内で記述されているため他の関数から参照できない
  • バリデーション関数の処理はほとんど同じなのに別々の関数として定義している

少し具体的に見ていきましょう。

for文などの繰り返し処理がないので記述の重複が多い

分かりやすいところだと以下の部分が挙げられます。

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

ほとんど同じ記述ですが、仮にバリデーションを行う要素を増やす場合には新たにイベントを定義する必要があります。

エラーメッセージがバリデーション関数内に記述されているため他の関数から参照できない

以下の部分です。

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

errMsgElem.textContent validatieEmail() 関数内で定義されており、条件分岐によってエラーメッセージを変更しています。また、エラーメッセージのデフォルト値はHTMLに記述しました。

エラーメッセージを他の用途で使用する可能性は低いのであまり問題にならないかもしれませんが、エラーメッセージは配列やオブジェクトとしてまとめて管理した方がメンテナンスはしやすいでしょう。

バリデーション関数の処理はほとんど同じなのに別々の関数として定義している

簡単に説明すると、各バリデーション関数は一部を除いて以下の「○○」以外はほぼ重複しています。

function validate○○() {
  let ○○ = ○○.value;
  let errMsgElem = ○○.previousElementSibling;

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

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

関数をフォーム部品ごとに定義しているので、バリデーションの対象が増えれば新たに関数を定義しなければならなくなり、その要素に対して更にイベントも追加しなければいけなくなります。

タスクが多いと修正漏れの可能性が高くなるので、これらの関数は一つにまとめた方が管理がしやすくなります。

問題点を抽出したので、次にリファクタリングの目的を考えます。

リファクタリングの目的

ここでのリファクタリングの目的は以下です。

  • 関数を汎用的にする
  • バリデーションの対象となる要素が増えた場合に対応しやすくする
  • バリデーションのパターンが増えたときに対応しやすくする

これらの目的を果たすには、バリデーションに関するデータを配列やオブジェクトとして扱うことが有効です。ここでは、バリデーションに関するデータをオブジェクトとして定義していきます。

データをオブジェクト化する

バリデーションに関するデータをオブジェクト化することでコードを整理することができ、保守性と可読性を向上させることができます。また、データを追加するだけで新しい要素に対するバリデーションを実装できるので再利用性も向上します。

バリデーションに関するデータは「お名前入力欄」、「メールアドレス入力欄」、「お問い合わせ内容入力欄」、「個人情報保護方針チェックボックス」に分類できます。まずはこれを定義します。

const validationData = {
  name: {},
  email: {},
  content: {},
  policyCheck: {}
}

validationData というオブジェクトを定義し、プロパティには更に、各フォーム部品の名前付けたオブジェクトを定義しています。

各フォーム部品はHTML要素・エラーメッセージ・バリデーションを実行するタイミングに関するデータを扱います。

また、エラーメッセージはエラーの条件とメッセージのテキストを扱います。「お名前入力欄」を例にすると以下のようになります。

const validationData = {
  name: {
    element: 'HTML要素を取得',
    errMsgs: {
      empty: {
        pattern: '正規表現を記述',
        errMsg: 'エラー文を記述'
      }
    },
    eventListeners: 'イベントハンドラを記述'
  },
  …
}

オブジェクト内の各プロパティは for 文などのループ処理で扱えるようになります。エラーメッセージに関するデータも同様の意図でオブジェクトとして定義しています。

以下は不要になるので削除しましょう。

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

かわりに、 validationData オブジェクト内でHTML要素を取得します。

const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {…},
    eventListeners: […]
  …
}

続いて、 errMsgs オブジェクトのデータを定義します。プロパティとしてエラーの種類をオブジェクトで定義し、そのオブジェクトのプロパティとして pattern プロパティには正規表現を、 errMsg プロパティにはエラーのテキストを記述します。

const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'お名前を入力してください'
      }
    },
    eventListeners: […]
  },
  …
}

上記では empty というエラーの種類を定義しています。また、このエラーを検証するための正規表現として、 pattern プロパティの値を「 /^.+$/ 」としています。

この正規表現は改行を除く1文字以上の文字列であることを検証します。これにより、入力がされていない時はこの正規表現にマッチしなくなるので、 empty の条件として成立させることができます。

次に eventListeners プロパティのデータを定義します。お名前入力欄は blur イベントと input イベント発生時にバリデーションを行いたいので、以下のように定義しておきます。

const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {…},
    eventListeners: ['blur', 'input']
  …
}

お名前入力欄用のバリデーションデータの定義は以上です。他のフォーム部品も同様に定義していきます。各フォーム部品の内容を書き換えると以下のようになります。

const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'お名前を入力してください'
      },
    eventListeners: ['blur', 'input']
    },

   email: {
    element: document.getElementById('email'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'メールアドレスを入力してください'
      },
      invalid: {
        pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
        errMsg: 'メールアドレスの形式が正しくありません'
      }
    },
    eventListeners: ['blur', 'input']
  },

  content: {
    element: document.getElementById('content'),
    errMsgs: {
      empty: {
        pattern: /^[\s\S]+$/,
        errMsg: 'お問い合わせ内容を入力してください'
      }
    },
    eventListeners: ['blur', 'input']
  },

  policyCheck: {
    element: document.getElementById('policy-check'),
    errMsgs: {
      unchecked: {
        pattern: /^checked$/,
        errMsg: '個人情報保護方針に同意してください'
      }
    },
    eventListeners: ['change']
  }
}

ここまで行ったら一度テストしておくと良いでしょう。このままだと nameInput などの定数が定義されていないというエラーが出るので、これらを以下のように書き換えます。

  • nameInput → validationData.name.element
  • emailInput → validationData.email.element
  • contentInput → validationData.content.element
  • policyCheckInput → validationData.policyCheck.element

validation.js 内の全ての箇所を書き換える必要がありますが、使っているテキストエディタがVS Codeであれば文字列を選択した状態で「Ctrl + D」を押すことでそのファイル内の同じ文字列を選択できるので、効率よく変更ができます。

正常に動作しない場合は開発ツールのコンソールでエラーを確認しましょう。

SyintaxErrorなら文法ミスなので扱えない文字列が含まれていないか、カッコやカンマなどが適切かなどを確認します。

TypeErrorならオブジェクトやプロパティの名前が間違っていないかを確認します。

開発ツールのコンソールにはエラーが何行目で発生しているかが表示されるのでそちらを参考にすると良いでしょう。

続いて、ここで定義した validationData オブジェクトを使用してバリデーション関数を汎用的に使えるように共通化します。

関数の共通化

各フォーム部品にはそれぞれ個別にバリデーション関数を定義していました。これらを共通化して一つの関数として扱えるようにすることで、バリデーションの対象となる要素が増えた場合でも対応が簡単になります。

関数を共通化するためのポイントは以下です。

  • 重複した処理を見極める
  • 対象となるデータを関数の引数として受け取る
  • 独自性のあるものは条件分岐で処理する

重複した処理を見極める

関数を共通化するには、まず重複した処理を見極める必要があります。各フォーム部品のバリデーション関数は主に以下の処理が重複しています。

  • 変数の定義
  • 入力値が無い場合の処理
  • バリデーションに成功した場合の処理

対象となるデータを関数の引数として受け取る

次に、重複した処理を共通化するためにどのような引数を受け取るかを考えます。

validationData のプロパティは name email content policyCheck ですが、各プロパティは更にオブジェクトが3層の入れ子構造になっているので、 validationData をそのまま関数の引数として受け取ると処理が複雑になります。

  • name
    • element
    • errMsgs
      • empty
        • pattern
        • errMsg
    • eventListeners

また、各フォーム部品の element プロパティを引数として受け取った場合は errMsgs プロパティを関数内で扱うことが難しくなります。ここでは、 element プロパティと errMsgs プロパティを持つ、 name email content policyCheck などを引数として受け取るようにします。

これらを踏まえて関数を定義すると以下のようになります。

/*
  ==========================
  バリデーションチェックの関数
  ==========================
*/
function validate(target) {
  let element = target.element;
  let errMsgs = target.errMsgs;
  let value = element.value;
  let errMsgElem = element.previousElementSibling;

  for (const key in errMsgs) {
    const {pattern, errMsg} = errMsgs[key];

    if (!pattern.test(value)) {
      errMsgElem.textContent = errMsg;
      errMsgElem.classList.add('show');
      element.focus();
      return false;
    } 
  }

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

まずは関数の宣言から解説します。

function validate(target) { … }

関数名はシンプルに validate とし、引数名は target としています。 target validationData オブジェクトのプロパティの値である、 name email といったオブジェクトが入ります。

続いて、変数は以下のように宣言しています。

function validate(target) {
  let element = target.element;
  let errMsgs = target.errMsgs;
  let value = element.value;
  let errMsgElem = element.previousElementSibling;
  …
}

element 変数と errMsgs 変数には、引数で受け取ったオブジェクトのプロパティの値を代入しています。

value 変数は element 変数の value プロパティの値を、 errMsgElem 変数には element 変数の前のHTML要素を代入しています。

続いて for 文です。

function validate(target) {
  …
  for (const key in errMsgs) {
    const  pattern, errMsg} = errMsgs[key];

    if (!pattern.test(value)) { … } 
  }
  …
}

「 for (const key in errMsgs) 」とすることで、 errMsgs オブジェクトの各プロパティを for 文で扱うことができます。 key は、 email を例にすると empty invalid といった errMsgs のプロパティを表します。

「 const {pattern, errMsg} = errMsgs[key]; 」の記述では、 empty invalid のオブジェクトのプロパティの値を取得しています。

const {pattern, errMsg} = errMsgs[key];

errMsgs[key] empty などを表します。 {pattern, errMsg} に代入(このような記法を分割代入といいます)することで、 empty invalid に定義されている pattern プロパティと errMsg プロパティの値を、 pattern 定数、 errMsg 定数として扱うことができます。

続いて if 文です。

for (const key in errMsgs) {
  const {pattern, errMsg} = errMsgs[key];

  if (!pattern.test(value)) {
    errMsgElem.textContent = errMsg;
    errMsgElem.classList.add('show');
    element.focus();

    return false;
  } 
}

pattern 定数の値は正規表現になるので、「 if (!patten.test(value)) 」とすることで入力値が正規表現にマッチしない場合の処理を分岐しています。

また、「 errMsgElem.textContent = errMsg; 」のところでは pattern にマッチしない場合のエラー文を代入しています。 email で例えると、 errMsgs は以下のように定義しました。

email: {
  …,
  errMsgs: {
    empty: {
      pattern: /^.+$/,
      errMsg: 'メールアドレスを入力してください'
    },
    invalid: {
      pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
      errMsg: 'メールアドレスの形式が正しくありません'
    }
  }
}

これにより、 errMsgElem.textContent の値は

  • 「 pattern: /^.+$/ 」にマッチしない場合は「メールアドレスを入力してください」
  • 「 pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ 」にマッチしない場合は「メールアドレスの形式が正しくありません」

という結果になります。

共通化する前では if 文の条件に「 (!value) 」を指定していましたが、エラーメッセージのデータをオブジェクト化し、条件の定義を正規表現に統一することで条件の追加が簡単になりました。

お名前入力欄を例にすると、 validationData name プロパティに以下のように追記するだけでバリデーションの条件を追加できます。

const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'お名前を入力してください'
      },
      spaceOnly: {
        pattern: /^(?!\s+$).+$/,
        errMsg: 'スペースのみの入力はできません'
      },
      noSeparated: {
        pattern: /^.+\s.+$/,
        errMsg: '姓と名の間にスペースを入れてください'
      },
      max: {
        pattern: /^.{0,30}$/,
        errMsg: '30文字以内で入力してください'
      },
      invalid: {
        pattern: /^[ぁ-んァ-ヶ一-龠a-zA-Z\s]+$/,
        errMsg: 'ひらがな、カタカナ、漢字、半角英字で入力してください'
      }
    }
  },
  …
}

バリデーション関数の定義は大体できました。

しかし、個人情報保護方針のバリデーション関数は value プロパティの値ではなく checked プロパティの値で条件分岐を行っていたので、このままでは正常に動作しません。次はこの対策を行います。

独自性のあるものは条件分岐で処理する

ここでは、関数内の value 変数の値を条件分岐で切り替えて個人情報保護方針のバリデーションに対応させます。 value 変数は以下のように定義しました。

let value = element.value;

また、個人情報保護方針のバリデーションチェックの条件は以下です。

policyCheck: {
  …
  errMsgs: {
    unchecked: {
      pattern: /^checked$/,
      errMsg: '個人情報保護方針に同意してください'
    }
  },
  …
}

pattern プロパティには正規表現で「 /^checked$/ 」と記述しており、これは文字列が「checked 」の場合にマッチするという意味になります。

「 policyCheckの場合 」として条件分岐を行うと汎用的ではないので、「 type属性がcheckboxの場合 」を条件にして value 変数の値を切り替えます。以下のように変更しましょう。

function validate(target) {
  …
  let value;
  if (element.type === 'checkbox') {
    value = element.checked ? 'checked' : '';
  } else {
    value = element.value;
  }
  …
}

element.type プロパティには該当する要素の type 属性の値が格納されています。条件式を「 (element.type === ‘checkbox’) 」とすることで、「 type属性がcheckboxの場合 」の処理を分岐しています。

さらに、 if 文内では element.checked プロパティの値が true であれば ‘checked’ を、 false であれば空の文字列を value 変数に代入しています。

value = element.checked ? 'checked' : '';

これで validate() 関数はおおむね完成です。

関数を共通化したら動作に問題がないかテストを行います。まずは、送信ボタンのクリックイベントなどで呼び出している関数を書き換えます。

/*
  ===========================
  送信ボタンのクリックイベント
  ===========================
*/
submitBtn.addEventListener('click', function(event) {
  for (const key in validationData) {
    if (!validate(validationData[key])) {
      event.preventDefault();
      return false;
    }
  }
});

for 文を使用している以外は変更前とほとんど同じなので、 validate() 関数の処理が理解できれば上記で動作する理由もわかるかと思います。

以下の記述はテストの邪魔になるので削除かコメントアウトします。

// お名前のバリデーションチェック
function validateName() {
  …
}

// メールアドレスのバリデーションチェック
function validateEmail() {
  …
}

// お問い合わせ内容のバリデーションチェック
function validateContent() {
  …
}

// 個人情報保護方針のバリデーションチェック
function validatePolicyCheck() {
  …
}
/*
  ===============================================
  値の入力が終わった時にバリデーションチェックを実行
  ===============================================
*/
validationData.name.element.addEventListener('blur', validateName);
validationData.email.element.addEventListener('blur', validateEmail);
validationData.content.element.addEventListener('blur', validateContent);
validationData.policyCheck.element.addEventListener('change', validatePolicyCheck);

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

続いて、各フォーム部品の blur change input イベントの登録処理も修正します。イベントの登録処理は、削除していなければ以下のようになっているかと思います。

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

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

各関数を削除したのでこれらの処理はエラーになります。とりあえず以下のように書き換えましょう。

/*
  ===================================
  イベントハンドラにvalidate関数を登録
  ===================================
*/
for (const key in validationData) {
  const target = validationData[key];
  const element = target.element;
  const eventListeners = target.eventListeners;
    
  for (const eventType in eventListeners) {
    element.addEventListener(eventListeners[eventType], function() {
      validate(target);
    });
  }
}

blur change イベントは上記の記述で問題ありませんが、このままだと input イベントを登録した要素は入力している最中に validate() 関数が実行されてしまいます。 input イベントは一度入力に失敗するまでは発生してほしくないので少し調整します。

まずは、 eventListeners[eventType] の値が ‘input’ の場合はイベントの登録を行わないようにします。

/*
  ===================================
  イベントハンドラにvalidate関数を登録
  ===================================
*/
for (const key in validationData) {
  …   
  for (const eventType in eventListeners) {
    if (eventListeners[eventType] === 'input') {
      continue;
    } else {
      element.addEventListener(eventListeners[eventType], function() {
        validate(target);
    });
  }
}

input イベントの登録はバリデーションに失敗したタイミングで行うようにします。 validate() 関数に以下を追記します。

function validate(target) {
  let element = target.element;
  let errMsgs = target.errMsgs;

  // 以下を追記
  let eventListeners = target.eventListeners;

  let value;
  if (element.type === 'checkbox') {
    value = element.checked ? 'checked' : '';
  } else {
    value = element.value;
  }
  let errMsgElem = element.previousElementSibling;

  for (const key in errMsgs) {
    const { pattern, errMsg } = errMsgs[key];

    if (!pattern.test(value)) {
      errMsgElem.textContent = errMsg;
      errMsgElem.classList.add('show');
      element.focus();

      // 以下を追記
      if (eventListeners.includes('input')) {
        if (!target.inputListener) {
          element.addEventListener('input', function () {
            validate(target);
          });
          target.inputListener = true;
        }
      }

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

  return true;
}

関数内で eventListeners という変数を定義し、引数として受け取ったフォーム部品の eventListeners プロパティの値を代入しています。

フォーム部品の eventListeners プロパティは配列として定義しているので、 includes() という配列用の便利なメソッドが使用できます。

if (eventListeners.includes('input')) {
  if (!target.inputListener) {
    element.addEventListener('input', function () {
      validate(target);
    });
    target.inputListener = true;
  }
}

includes() メソッドは、引数で受け取った値が配列に含まれていた場合に true を返します。上記の部分では、条件分岐で「 eventListeners変数に ‘input’ が含まれていた場合 」の処理を定義し、この中で input イベントの登録を行っています。

また、 addEentListener() メソッドを使用したイベントの登録は同じ処理でも重複して登録されてしまいます。対策を行わなければ、入力値が正しくなるまで文字を入力する度にコールバック関数が登録されることになります。

気持ち悪いので対策しています。以下の部分で inputListener というプロパティを定義して登録の重複を防いでいます。

if (!target.inputListener) {
  element.addEventListener('input', function () {
    validate(target);
  });
  target.inputListener = true;
}

validate() 関数のテストを行う準備ができたので動作の確認をしましょう。問題が無ければ次に進みます。

イベント内の処理を関数化

バリデーションチェックの関数を共通化することで汎用的に使えるようになりましたが、現状ではイベントハンドラの設定がバラバラのところに記述されているので可読性があまりよくありません。

イベント内の処理を関数として定義し、コードの記述位置を変更してこれらを整理していきます。

送信ボタンのクリックイベント

まずは送信ボタンのクリックイベントの処理を関数化します。送信ボタンのクリックイベントの処理は以下です。

submitBtn.addEventListener('click', function(event) {
  for (const key in validationData) {
    if (!validate(validationData[key])) {
      event.preventDefault();
      return false;
    }
  }
});

以下のように変更します。

/*
  ==========
  関数の定義
  ==========
*/
function execValidateAll(event) {
  for (const key in validationData) {
    if (!validate(validationData[key])) {
      event.preventDefault();
      return false;
    }
  }
  return true;
}

function validate(target) {…}
/*
  ===========================
  イベントハンドラの設定
  ===========================
*/
submitBtn.addEventListener('click', execValidateAll);

送信ボタンのクリックイベントの処理を execValdateAll() という関数にしました。引数として event オブジェクトを受け取ることで、関数内で event.preventDefault(); を実行することができます。また、コメントを記述して関数の定義とイベントハンドラの設定を分けています。

/*
  ==========
  関数の定義
  ==========
*/
function execValidateAll(event) {…}
function validate(target) {…}

イベントハンドラの登録処理

イベントハンドラの登録処理も同様に関数化します。以下のように修正します。

/*
  ==========
  関数の定義
  ==========
*/
function execValidateAll(event) { … }

// 以下を追加
function setValidateListeners(data) {
  for (const key in data) {
    const target = data[key];
    const element = target.element;
    const eventListeners = target.eventListeners;
    
    for (const eventType in eventListeners) {
      if (eventListeners[eventType] === 'input') {
        continue;
      } else {
        element.addEventListener(eventListeners[eventType], function() {
          validate(target);
      });
    }
  }
}

function validate(target) { … }

/*
  ===========================
  イベントハンドラの設定
  ===========================
*/
submitBtn.addEventListener('click', execValidateAll);
// 以下を追加
setValidateListeners(validationData);

setValidateListeners() という関数を定義しました。この関数は引数として data を受け取ります。関数の実行時に引数として validationData オブジェクトを渡すことで機能します。

イベント内の処理を関数として定義し、記述場所をまとめることで少しコードが見やすくなりました。些細な問題と思うかも知れませんが、コードの記述量が多くなるほど効果が顕著に現れるので普段から意識しておくと良いでしょう。

コメントで関数の説明を記述

最後に、関数にコメントを追加していきます。コメントは関数の使い方が分かる内容にします。関数のコメントは JSDocs という記法で書くと便利なので簡単にご紹介します。

以下は JSDocs で記述したコメントの例です。

/**
 * validationDataのプロパティに対してバリデーションを実行し、エラーがある場合にフォームの送信を防ぎます。
 * @param {Event} event - クリックイベント
 * @returns {boolean} - バリデーションが成功した場合は true、エラーがある場合は false
 */
function execValidateAll(event) { … }

JSDocs でコメントを書くことで、 VSCode では関数名にマウスカーソルをのせた時に JSDocs のコメントを表示させることができます。

上記のコメントを追加したうえで execValidateAll の部分にマウスカーソルをのせると以下のように表示されます。

JSDocsでコメントを書くときは、最初の行を「 /** 」とし、各行の先頭には「 * 」を記述します。 また、@param には関数の引数、 @returns には関数の戻り値を記述します。

JSDocs は関数の使い方や引数に指定できる値、戻り値などを示すことができ、ファイル内で関数の記述場所を探す手間を省けるのでオススメです。

setValidateListeners() 関数と validate() 関数のコメントについては以下のサンプルコードの全体を参照してください。

サンプルコードの全体

この記事では validation.js しか変更していないので、 index.html や style.css のサンプルコードは割愛します。

/*
  ==============
  HTML要素の取得
  ==============
*/
const submitBtn = document.getElementById('submit');

/**
 * バリデーションに使用するオブジェクトです。 
 */
const validationData = {
  name: {
    element: document.getElementById('name'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'お名前を入力してください'
      },
      spaceOnly: {
        pattern: /^(?!\s+$).+$/,
        errMsg: 'スペースのみの入力はできません'
      },
      noSeparated: {
        pattern: /^.+\s.+$/,
        errMsg: '姓と名の間にスペースを入れてください'
      },
      max: {
        pattern: /^.{0,30}$/,
        errMsg: '30文字以内で入力してください'
      },
      invalid: {
        pattern: /^[ぁ-んァ-ヶ一-龠a-zA-Z\s]+$/,
        errMsg: 'ひらがな、カタカナ、漢字、半角英字で入力してください'
      }
    },
    eventListeners: ['blur', 'input']
  },

  email: {
    element: document.getElementById('email'),
    errMsgs: {
      empty: {
        pattern: /^.+$/,
        errMsg: 'メールアドレスを入力してください'
      },
      invalid: {
        pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
        errMsg: 'メールアドレスの形式が正しくありません'
      }
    },
    eventListeners: ['blur', 'input']
  },

  content: {
    element: document.getElementById('content'),
    errMsgs: {
      empty: {
        pattern: /^[\s\S]+$/,
        errMsg: 'お問い合わせ内容を入力してください'
      }
    },
    eventListeners: ['blur', 'input']
  },

  policyCheck: {
    element: document.getElementById('policy-check'),
    errMsgs: {
      unchecked: {
        pattern: /^checked$/,
        errMsg: '個人情報保護方針に同意してください'
      }
    },
    eventListeners: ['change']
  }
}

/*
  ==========
  関数の定義
  ==========
*/
/**
 * validationDataのプロパティに対してバリデーションを実行し、エラーがある場合にフォームの送信を防ぎます。
 * @param {Event} event - クリックイベント
 * @returns {boolean} - バリデーションが成功した場合は true、エラーがある場合は false
 */
function execValidateAll(event) {
  for (const key in validationData) {
    if (!validate(validationData[key])) {
      event.preventDefault();
      return false;
    }
  }
  return true;
}

/**
 * validationDataのプロパティに対してvalidation関数を実行するためのイベントを設定します。
 * @param {Object} data - バリデーションデータオブジェクト
 */
function setValidateListeners(data) {
  for (const key in data) {
    const target = data[key];
    const element = target.element;
    const eventListeners = target.eventListeners;
    
    for (const eventType in eventListeners) {
      if (eventListeners[eventType] === 'input') {
        continue;
      } else {
        element.addEventListener(eventListeners[eventType], function() {
          validate(target);
      });
    }
  }
}

/**
 * 入力フィールドの値をバリデーションします。
 * @param {Object} target - validationDataのプロパティ
 * @returns {boolean} - バリデーションが成功した場合は true、エラーがある場合は false
 */
function validate(target) {
  let element = target.element;
  let errMsgs = target.errMsgs;
  let eventListeners = target.eventListeners;
  let value;
  if (element.type === 'checkbox') {
    value = element.checked ? 'checked' : '';
  } else {
    value = element.value;
  }
  let errMsgElem = element.previousElementSibling;

  for (const key in errMsgs) {
    const { pattern, errMsg } = errMsgs[key];

    if (!pattern.test(value)) {
      errMsgElem.textContent = errMsg;
      errMsgElem.classList.add('show');
      element.focus();

      if (eventListeners.includes('input')) {
        if (!target.inputListener) {
          element.addEventListener('input', function () {
            validate(target);
          });
          target.inputListener = true;
        }
      }
      return false;
    }
  }
  errMsgElem.classList.remove('show');
  return true;
}

/*
  ===========================
  イベントハンドラの設定
  ===========================
*/
submitBtn.addEventListener('click', execValidateAll);
setValidateListeners(validationData);

次のステップ

リファクタリングの基本的な考え方について学び、【バリデーションチェック編(JavaScript)】で作成したJavaScriptコードを保守性や効率を意識して改修しました。

リファクタリングを行ったことで、 validationData オブジェクトに新しいプロパティを追加するだけでバリデーションの対象となる要素を追加することができるようになり、エラーメッセージのパターンの追加も簡単になりました。

今回行ったリファクタリングはあくまで一例であり、目的に応じて他にもリファクタリングする余地がたくさんあります。また、 validationData オブジェクトに追加する要素によっては validation() 関数の修正が必要な場合もあります。

リファクタリングはプログラミングの理解を深めるための効率的な手段なので、JavaScriptを学習中の方は積極的にリファクタリングを行うと良いと思います。

次のステップの「お問い合わせフォームを作ってみよう【確認画面の実装編】」では、確認画面の必要性について学び、お問い合わせフォームの値を反映させた確認画面をモーダルウィンドウで実装します。

実際のお問い合わせフォームの制作では、送信する前に確認画面を表示させるといった実装が一般的です。こちらもJavaScriptの知識が必要になりますが、今回の記事の内容が理解できればそれほど難しくないのでぜひチャレンジしてみてください。

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