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 する前と後で、エラー表示がどう変わるかを見る
など、「ユーザーとして触ってみる」時間を少し取ってみてください。
バリデーションは、コードを書くより「人の動きを想像する力」がものを言います。


