JavaScript | 1 日 120 分 × 7 日アプリ学習:フォームバリデーションアプリ

JavaScript
スポンサーリンク

2日目のゴールと今日のテーマ

2日目のテーマは「入力中に動くバリデーション」と「エラー状態の設計」です。
1日目は「submit を押したときにまとめてチェック」でしたが、今日は一歩進めて、

  • 入力中にエラーメッセージを動的に出す
  • エラー状態を JavaScript の「状態(state)」として管理する
  • UX 的にうるさすぎないタイミングを考える

ここを丁寧にやっていきます。


状態としての「入力値」と「エラー」を設計する

state を導入して、頭の中を整理する

1日目は、submit の中でローカル変数として emailError などを扱っていました。
2日目からは、これを「状態(state)」として 1 カ所にまとめます。

const state = {
  email: "",
  password: "",
  errors: {
    email: "",
    password: "",
    form: "",
  },
  touched: {
    email: false,
    password: false,
  },
  isSubmitting: false,
};
JavaScript

ここでのポイントは 3 つあります。

1つ目は、入力値(email, password)を state に持つこと。
DOM から毎回読み取るのではなく、「真実は state にある」と決めると設計がスッキリします。

2つ目は、エラーも state の一部として持つこと。
errors.email に「今のメールのエラーメッセージ」が入るようにしておけば、
画面の更新は「state を読むだけ」で済みます。

3つ目は、touched というフラグ。
これは「一度でも触られた(フォーカスされた)かどうか」を表します。
UX 的に「まだ何も触っていないのに真っ赤なエラーを出さない」ための工夫です。

render 関数で「state → 画面」を一方向に描く

状態ができたら、それをもとに画面を更新する関数を作ります。

function render() {
  emailInputEl.value = state.email;
  passwordInputEl.value = state.password;

  emailErrorEl.textContent = state.errors.email;
  passwordErrorEl.textContent = state.errors.password;
  formMessageEl.textContent = state.errors.form;

  if (state.errors.email && state.touched.email) {
    emailInputEl.classList.add("input-error");
  } else {
    emailInputEl.classList.remove("input-error");
  }

  if (state.errors.password && state.touched.password) {
    passwordInputEl.classList.add("input-error");
  } else {
    passwordInputEl.classList.remove("input-error");
  }

  const hasFieldError =
    (state.errors.email && state.touched.email) ||
    (state.errors.password && state.touched.password);

  submitButtonEl.disabled = hasFieldError || state.isSubmitting;
}
JavaScript

ここで深掘りしたいのは、「touched とエラーの組み合わせ」です。

メールにエラーがあっても、まだ一度も触っていないなら、
state.touched.email は false なので、赤枠やエラーメッセージを出さない、という判断ができます。
これが UX 設計の一歩目です。


入力イベントで「状態を更新 → バリデーション → render」の流れを作る

メール入力の input / blur を使い分ける

2日目では、次のような動きを目指します。

メール欄については、

  • 初めてフォーカスが外れたとき(blur)にエラーを出す
  • それ以降は、入力中(input)でもエラーを更新する

これを実現するために、touched.email を使います。

emailInputEl.addEventListener("input", () => {
  state.email = emailInputEl.value;
  if (state.touched.email) {
    state.errors.email = validateEmail(state.email);
  }
  render();
});

emailInputEl.addEventListener("blur", () => {
  state.touched.email = true;
  state.errors.email = validateEmail(state.email);
  render();
});
JavaScript

ここでの重要ポイントは 2 つです。

1つ目は、「入力中は、すでに touched のときだけエラーを更新する」こと。
これにより、「まだ一度もフォーカスしていないのにエラーが出る」ことを防げます。

2つ目は、「blur で必ず touched を true にし、エラーを計算する」こと。
これで「一度でも触ったフィールドは、以降ずっとバリデーション対象」になります。

パスワードも同じパターンで扱う

パスワードも同じようにします。

passwordInputEl.addEventListener("input", () => {
  state.password = passwordInputEl.value;
  if (state.touched.password) {
    state.errors.password = validatePassword(state.password);
  }
  render();
});

passwordInputEl.addEventListener("blur", () => {
  state.touched.password = true;
  state.errors.password = validatePassword(state.password);
  render();
});
JavaScript

この「input と blur の組み合わせ」は、実務でもよく使われる UX パターンです。


正規表現を使ったバリデーション関数を「純粋関数」として定義する

メールの正規表現とバリデーション関数

1日目で軽く触れたメールの正規表現を、関数の外に定義しておきます。

const emailRegex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
JavaScript

そして、バリデーション関数を「入力値 → エラーメッセージ」の純粋関数として定義します。

function validateEmail(value) {
  if (!value) {
    return "メールアドレスを入力してください。";
  }
  if (!emailRegex.test(value)) {
    return "メールアドレスの形式が正しくありません。";
  }
  return "";
}
JavaScript

ここで大事なのは、「この関数は副作用を持たない」ということです。
DOM に触らず、state にも触らず、ただ文字列を受け取って文字列を返すだけ。
こういう関数はテストしやすく、バグりにくいです。

パスワードの正規表現とバリデーション関数

パスワードのルールを、例えば次のように決めます。

  • 8文字以上
  • 英字と数字を少なくとも1文字ずつ含む

正規表現はこう書けます。

const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
JavaScript

そしてバリデーション関数。

function validatePassword(value) {
  if (!value) {
    return "パスワードを入力してください。";
  }
  if (value.length < 8) {
    return "パスワードは8文字以上にしてください。";
  }
  if (!passwordRegex.test(value)) {
    return "英字と数字を少なくとも1文字ずつ含めてください。";
  }
  return "";
}
JavaScript

ここでも、「どの条件に引っかかったか」でメッセージを変えています。
ユーザーにとって「何がダメなのか」が明確になるので、UX 的に非常に重要です。


submit 制御を「最終チェック」として設計する

submit 時は「全フィールドを強制的に touched にする」

入力中のバリデーションがあっても、submit 時にはもう一度全体をチェックするのが安全です。
その際、「まだ touched になっていないフィールド」も含めてエラーを出す必要があります。

formEl.addEventListener("submit", (event) => {
  event.preventDefault();

  state.touched.email = true;
  state.touched.password = true;

  state.errors.email = validateEmail(state.email);
  state.errors.password = validatePassword(state.password);

  const hasError = Boolean(state.errors.email || state.errors.password);

  if (hasError) {
    state.errors.form = "入力内容を確認してください。";
    if (state.errors.email) {
      emailInputEl.focus();
    } else if (state.errors.password) {
      passwordInputEl.focus();
    }
    render();
    return;
  }

  state.errors.form = "";
  state.isSubmitting = true;
  render();

  setTimeout(() => {
    state.isSubmitting = false;
    state.errors.form = "ログインに成功しました!(ダミー)";
    render();
  }, 800);
});
JavaScript

ここでの重要ポイントは 3 つです。

1つ目は、「submit 時に touched を強制的に true にする」こと。
これにより、「まだ触っていないフィールド」もエラー表示の対象になります。

2つ目は、「エラーがある場合は最初のエラーのフィールドに focus を移す」こと。
ユーザーが「どこを直せばいいか」をすぐに理解できるようになります。

3つ目は、「isSubmitting フラグ」でボタンを無効化していること。
連打を防ぎ、「今処理中だよ」という状態を表現できます。


2日目のまとめと、明日へのつなぎ

2日目であなたがやったことは、かなり「中級っぽい」内容です。

  • 入力値とエラーを state として管理した
  • render 関数で「state → 画面」を一方向に描いた
  • input / blur / submit のタイミングを使い分けて、UX を意識したバリデーションを設計した
  • 正規表現を使ったバリデーション関数を「純粋関数」として定義した

明日以降は、ここからさらに、

  • エラーの種類を増やす(必須・形式・長さ・一致など)
  • フィールドを増やしたときに、どう設計を崩さずに拡張するか
  • 「フィールドごとの状態」と「フォーム全体の状態」を整理する

といった方向に広げていけます。

もし余裕があれば、2日目のコードで、

  • メールをわざと aaa@bbb のようにしてみる
  • パスワードを 7 文字にしてみる
  • blur する前と後で、エラー表示がどう変わるかを見る

など、「ユーザーとして触ってみる」時間を少し取ってみてください。
バリデーションは、コードを書くより「人の動きを想像する力」がものを言います。

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