第11章:Validationとエラーメッセージ(優しく案内)😌🚨
この章は「入力ミスがあってもアプリが落ちない✨」「ユーザーが次に何をすればいいか分かる💡」を作る回だよ〜!🌸 CampusTodo(課題メモ)に やさしい入力チェック を入れて、気持ちよく使えるUIにしよっ🫶✨
1) この章のゴール🎯✨
できるようになることはこの3つ!💪😊
- 入力チェックを2段構えで作れる(Viewの形式チェック+Modelのルールチェック)🧱🧱
- エラーが出ても画面が崩れず、入力内容が消えない🧯✨
- “やさしい日本語”でエラーメッセージを出せる(怖くない・責めない・次が分かる)🌷💬
2) バリデーションは「2段階」が勝ち🏆😎

✅ ① 形式チェック(View側)=「入力として成立してる?」📝
- 空欄かどうか
- 長さ(短すぎ/長すぎ)
- 日付の形式、など
ここは ブラウザ標準のフォーム検証(Constraint Validation API) が強いよ!💪
フォーム要素単体 or <form> 全体で検証できる仕組みが用意されてる🥳 (MDN Web Docs)
✅ ② ルールチェック(Model側)=「アプリの世界のルール守ってる?」🛡️
- タイトルは空白だけNG(
" "はダメ🙅♀️) - 期限が過去の日付はNG
- (必要なら)同じタイトル重複NG など
これは Model(またはDomain)側で保証 するのが正解✨ Viewに置くと、別画面・別入力手段が増えた瞬間に破綻しやすいの🥲
3) 今回のCampusTodoのチェック仕様📌✅
最低限で気持ちよくいこう!🍀(増やしたくなったら増やせる設計にするよ)
入力項目
title(課題名)dueDate(期限:任意)
ルール
- title:空欄NG、空白だけNG、最大50文字(例)
- dueDate:入れるなら「今日以降」📅✨
4) まずはViewで「形式チェック」🌼(HTMLの力を借りる)
HTMLの属性だけでも、けっこう守れるよ〜!✨ (ブラウザの検証は Constraint Validation API によるものだよ) (MDN Web Docs)
<form id="todoForm" novalidate>
<label>
課題名
<input
id="titleInput"
name="title"
type="text"
required
minlength="1"
maxlength="50"
placeholder="例)レポート提出"
/>
</label>
<label>
期限(任意)
<input id="dueDateInput" name="dueDate" type="date" />
</label>
<button type="submit">追加</button>
<p id="formError" role="alert" aria-live="polite"></p>
<ul id="fieldErrors"></ul>
</form>
🍀 novalidateって何?
今回は「ブラウザ任せのポップアップで終わり」じゃなくて、 自分たちのUI(エラー欄)にも出したいから、制御しやすくするために付けてるよ😊
5) Viewで「やさしいエラーメッセージ」に変える💬🌸

ブラウザ標準の文言って、ちょっと硬い時あるよね😇
そこで setCustomValidity() を使うと、エラーメッセージを自前にできる!✨ (MDN Web Docs)
さらに reportValidity() を呼ぶと、検証して(必要なら)ユーザーに問題を表示してくれるよ🧠✨ (MDN Web Docs)
function setFriendlyMessage(input: HTMLInputElement) {
const v = input.validity; // ValidityStateで理由が分かるよ :contentReference[oaicite:4]{index=4}
if (v.valueMissing) {
input.setCustomValidity("課題名が空っぽみたい…!1文字以上入れてね🌸");
return;
}
if (v.tooShort) {
input.setCustomValidity("もうちょいだけ長くしてほしいかも😊");
return;
}
if (v.tooLong) {
input.setCustomValidity("ちょっと長すぎるかも!50文字以内にしてね✂️");
return;
}
// OKなら空文字に戻すのが大事!
input.setCustomValidity("");
}
✅ submit時に動かす(形式チェック)
const form = document.getElementById("todoForm") as HTMLFormElement;
const titleInput = document.getElementById("titleInput") as HTMLInputElement;
form.addEventListener("submit", (e) => {
e.preventDefault();
setFriendlyMessage(titleInput);
// フォーム全体の制約検証(true/falseを返す) :contentReference[oaicite:5]{index=5}
if (!form.checkValidity()) {
// 問題を表示(ブラウザ表示も使うなら) :contentReference[oaicite:6]{index=6}
titleInput.reportValidity();
return;
}
// ここから先は「形式チェックOK」🎉
});
6) 次はModel側の「ルールチェック」🛡️✨(Domainの守護神)
ここからがMVCらしいところ!😎 「画面がどれだけ増えても」同じルールで守れるようにするよ💪
✅ エラーを“コード化”する(あとで表示文言を変えやすい)🏷️
export type TodoValidationError =
| { field: "title"; code: "TITLE_REQUIRED" }
| { field: "title"; code: "TITLE_TOO_LONG"; max: number }
| { field: "dueDate"; code: "DUE_DATE_IN_PAST" };
export type Result<T> =
| { ok: true; value: T }
| { ok: false; errors: TodoValidationError[] };
✅ Modelの生成時にチェックする(例:TodoItem.create)
export type TodoItem = {
id: string;
title: string;
done: boolean;
dueDate?: string; // "YYYY-MM-DD" 想定
};
export const TodoItemFactory = {
create(input: { title: string; dueDate?: string }): Result<TodoItem> {
const errors: TodoValidationError[] = [];
const title = input.title.trim();
const MAX = 50;
if (title.length === 0) {
errors.push({ field: "title", code: "TITLE_REQUIRED" });
} else if (title.length > MAX) {
errors.push({ field: "title", code: "TITLE_TOO_LONG", max: MAX });
}
if (input.dueDate) {
const today = new Date();
const yyyyMMdd = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
if (input.dueDate < yyyyMMdd(today)) {
errors.push({ field: "dueDate", code: "DUE_DATE_IN_PAST" });
}
}
if (errors.length > 0) return { ok: false, errors };
const item: TodoItem = {
id: crypto.randomUUID(),
title,
done: false,
dueDate: input.dueDate,
};
return { ok: true, value: item };
},
};
7) Controller:失敗しても“復帰”できる流れにする🔁🧯
Controllerは交通整理🚦
- 形式チェック(View)
- OKなら Service/Modelへ
- NGなら Viewへ「どう見せる?」を渡す
✅ エラー表示をViewに任せるためのインターフェイス(例)
type FieldErrorViewModel = {
field: "title" | "dueDate";
message: string;
};
function mapErrors(errors: TodoValidationError[]): FieldErrorViewModel[] {
return errors.map((e) => {
switch (e.code) {
case "TITLE_REQUIRED":
return { field: "title", message: "課題名が空っぽだよ〜!1文字以上入れてね🌸" };
case "TITLE_TOO_LONG":
return { field: "title", message: `ちょっと長いかも!${e.max}文字以内にしてね✂️` };
case "DUE_DATE_IN_PAST":
return { field: "dueDate", message: "期限が過去になってるみたい…!今日以降にしてね📅✨" };
}
});
}
8) View:エラーの出し方(怖くしないコツ)🌷✨
✅ “やさしいエラー”のテンプレ🫶
- ❌「不正です」 → ✅「ここを直すとOKだよ😊」
- ❌「入力しろ」 → ✅「〜してね🌸」
- ✅ できれば 理由+具体的な修正(例:50文字以内)
✅ 画面への出し方(おすすめ)
- フィールドの近くに出す(迷子になりにくい)🧭
- まとめ欄も一応出す(全体把握)📋
- 入力内容は消さない(心が折れる🥲)
const fieldErrorsUl = document.getElementById("fieldErrors") as HTMLUListElement;
const formErrorP = document.getElementById("formError") as HTMLParagraphElement;
function renderErrors(errors: FieldErrorViewModel[]) {
formErrorP.textContent = errors.length ? "入力をちょっとだけ見直してね😊" : "";
fieldErrorsUl.innerHTML = "";
for (const e of errors) {
const li = document.createElement("li");
li.textContent = `${e.field}: ${e.message}`;
fieldErrorsUl.appendChild(li);
}
}
function clearErrors() {
formErrorP.textContent = "";
fieldErrorsUl.innerHTML = "";
}
9) 形式チェック+ルールチェックを繋げた“完成形”🎉✨
form.addEventListener("submit", (e) => {
e.preventDefault();
clearErrors();
// ① View:形式チェック
setFriendlyMessage(titleInput);
if (!form.checkValidity()) {
titleInput.reportValidity();
return;
}
// ② Model:ルールチェック
const dueDateInput = document.getElementById("dueDateInput") as HTMLInputElement;
const result = TodoItemFactory.create({
title: titleInput.value,
dueDate: dueDateInput.value || undefined,
});
if (!result.ok) {
renderErrors(mapErrors(result.errors));
return;
}
// ③ OK:ここで「追加して再描画」へ進む(第7章の流れ🔁)
// todoStore.add(result.value);
// view.render(...)
});
10) AI活用🤖💡(この章での使いどころ)
✅ 優しい文言づくり(超おすすめ)💬✨
AIにこう頼むと、かなり良い感じになるよ〜!
- 「女子大生に向けて、やさしい口調で、責めないエラーメッセージを5案ください😊」
- 「TITLE_TOO_LONG のメッセージ、短くて分かりやすいのを3案✂️」
- 「“理由+次にやること”が入ってるかチェックして🧐」
コツ:エラーコード(TITLE_REQUIREDなど)を渡して、文言だけ作らせると設計がキレイに保てるよ🏷️✨
11) ミニ演習✍️🎀
- title が「空白だけ」でも弾けるか確認(
" ")🧼 - dueDate を「昨日」にして、やさしく案内できるか📅
- エラーが出た状態で、入力を直すとエラーが消えるようにする(
inputイベントでclearErrors)✨ - エラーが複数あるとき(例:title空+dueDate過去)に 両方表示できるようにする📋💡
12) 章のまとめ🧠✨(大事な3行)
- 形式チェックはView(ブラウザの検証機能も活用)🪄 (MDN Web Docs)
- ルールチェックはModel(どの画面から来ても守れる)🛡️
- エラーは“落とす”んじゃなく“返す”(ResultでUXが安定)🧯✨
次の第12章では、ここで増えてきた「追加処理まわり」を Service層に寄せてControllerをスリム化していくよ〜!🍔➡️🥗✨