第6章:Controller入門①:入力(イベント)を受けて指示する🎮➡️🧠
この章は「Controller=交通整理🚦」を体に入れる回だよ〜!✨ ボタンやフォームの入力イベントを受け取って、Modelを更新して → Viewに“描画してね”と指示する流れを作ります🔁💕
この章のゴール🎯✨
- Controllerがやることを説明できる(交通整理🚦ってこういう意味!)
- Viewからイベントを受け取る(submit / click / change)🖱️⌨️
- Modelを更新して、Viewを再描画する基本ループが作れる🔁✅
- 「Viewにロジックを入れすぎない🙅♀️」を守れる
まず結論:Controllerって何者?👀✨
Controllerは一言でいうと、
- ユーザー操作(イベント)を受け取る
- どの処理を動かすか決める
- Modelを更新して
- Viewに再描画を頼む
…っていう、交通整理係🚦です!
✅ Model:データとルール(Todoの正体📦) ✅ View:表示(DOMを作る・更新する🎨) ✅ Controller:入力を受けて指示する(司令塔🎮)
2026/01時点の “今どき”メモ🧃✨(超さらっと)
- TypeScript の最新版は 5.9系(npm上の Latest は 5.9.3) (typescriptlang.org)
- Vite の Latest は 7.3.1 (NPM)
- Node.js は v24 が Active LTS(v25 は Current) (Node.js) ※ 2026-01-13 に Node v24.13.0 (LTS) のセキュリティリリースも出てるよ〜🔒 (Node.js)
(章の内容はバージョンに強く依存しないように作ってあるよ🙆♀️)
今日つくる動き(CampusTodo)🧠➡️✅
✅ 1) 追加(Add)
- 入力欄にタイトル入れる✍️
- 追加ボタン or Enter で追加➕
- 一覧に反映📋✨
✅ 2) 完了トグル(Toggle)
- チェックボックスを押す✅
- done が切り替わる🔁
- 一覧に反映📋✨
“責務まぜまぜ”を防ぐルール🍀🙅♀️
**Viewに書いていいのはコレだけ!**👇
- DOMを作る / 更新する
- イベントを拾って「外に通知する」(=Controllerに渡す)
**Viewに書いちゃダメ!**👇
- todos配列の更新
- id採番
- 追加・完了の業務ロジック
これをController側に寄せるよ〜🚚✨
実装:Controllerを入れてMVCを回す🌀✨
ここでは最小構成でいくよ! (あとで第12章でService層に分けるから、今はControllerが少し頑張る💪)
ファイル構成(例)📁✨
src/model/TodoTypes.tssrc/model/TodoStore.tssrc/view/TodoView.tssrc/controller/TodoController.tssrc/main.ts
1) Model:TodoStore(データ置き場)📦
// src/model/TodoTypes.ts
export type TodoId = string;
export type TodoItem = {
id: TodoId;
title: string;
done: boolean;
createdAt: number;
};
// src/model/TodoStore.ts
import type { TodoId, TodoItem } from "./TodoTypes";
export class TodoStore {
private todos: TodoItem[] = [];
getAll(): readonly TodoItem[] {
return this.todos;
}
add(title: string): TodoItem {
const trimmed = title.trim();
if (trimmed.length === 0) {
throw new Error("タイトルが空だよ😢");
}
const todo: TodoItem = {
id: crypto.randomUUID(),
title: trimmed,
done: false,
createdAt: Date.now(),
};
this.todos = [todo, ...this.todos];
return todo;
}
toggle(id: TodoId): void {
this.todos = this.todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
);
}
}
ここでは「最低限のルール」として 空タイトルNGだけ入れてるよ😌 (もっとルール増やすのは第9章で本格的にやる予定🛡️✨)
2) View:表示だけ + “イベントを外に渡す窓口”🎨📣
ポイントは「ViewがControllerを直接知らない」こと!
だから bindAddTodo() みたいな イベント登録メソッドを用意して、外(Controller)から関数を渡してもらう形にするよ🤝✨
// src/view/TodoView.ts
import type { TodoId, TodoItem } from "../model/TodoTypes";
export class TodoView {
private form: HTMLFormElement;
private input: HTMLInputElement;
private list: HTMLUListElement;
constructor(root: HTMLElement) {
this.form = root.querySelector<HTMLFormElement>("#todo-form")!;
this.input = root.querySelector<HTMLInputElement>("#todo-input")!;
this.list = root.querySelector<HTMLUListElement>("#todo-list")!;
}
render(todos: readonly TodoItem[]): void {
this.list.innerHTML = "";
for (const todo of todos) {
const li = document.createElement("li");
const label = document.createElement("label");
label.style.display = "flex";
label.style.gap = "8px";
label.style.alignItems = "center";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.done;
checkbox.dataset.todoId = todo.id;
const span = document.createElement("span");
span.textContent = todo.title;
if (todo.done) {
span.style.textDecoration = "line-through";
span.style.opacity = "0.6";
}
label.append(checkbox, span);
li.append(label);
this.list.append(li);
}
}
// ✅ 追加イベントを“外に通知”する窓口
bindAddTodo(handler: (title: string) => void): void {
this.form.addEventListener("submit", (e) => {
// submit するとページ遷移しちゃうので止めるよ🛑
// preventDefault の意味は MDN でも説明あり 👍 :contentReference[oaicite:4]{index=4}
e.preventDefault();
handler(this.input.value);
});
}
// ✅ 完了トグルイベントを“外に通知”する窓口(イベント委任✨)
bindToggleTodo(handler: (id: TodoId) => void): void {
this.list.addEventListener("change", (e) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) return;
if (target.type !== "checkbox") return;
const id = target.dataset.todoId;
if (!id) return;
handler(id);
});
}
clearInput(): void {
this.input.value = "";
this.input.focus();
}
showError(message: string): void {
alert(message); // いったん最小!あとで第11章で優しくする🌸
}
}
🌟 submitイベントの注意(地味にハマる)
submit は ボタンじゃなくて form 自体に発火するよ!📝
(MDNにも明記されてる) (MDN Web Docs)
3) Controller:交通整理係🚦(本日の主役)

Controllerは Viewのイベントを受ける → Model更新 → View再描画 をひたすら回すよ🔁✨
// src/controller/TodoController.ts
import { TodoStore } from "../model/TodoStore";
import { TodoView } from "../view/TodoView";
export class TodoController {
constructor(
private store: TodoStore,
private view: TodoView
) {}
init(): void {
// 最初の描画
this.view.render(this.store.getAll());
// Viewイベントを受けて指示する(交通整理🚦)
this.view.bindAddTodo((title) => this.handleAdd(title));
this.view.bindToggleTodo((id) => this.handleToggle(id));
}
private handleAdd(title: string): void {
try {
this.store.add(title);
this.view.render(this.store.getAll());
this.view.clearInput();
} catch (err) {
const message = err instanceof Error ? err.message : "追加に失敗したよ😢";
this.view.showError(message);
}
}
private handleToggle(id: string): void {
this.store.toggle(id);
this.view.render(this.store.getAll());
}
}
4) main.ts:全部つないで起動🔌✨
// src/main.ts
import "./style.css";
import { TodoStore } from "./model/TodoStore";
import { TodoView } from "./view/TodoView";
import { TodoController } from "./controller/TodoController";
const root = document.querySelector<HTMLElement>("#app")!;
// MVCを組み立てる🧱
const store = new TodoStore();
const view = new TodoView(root);
const controller = new TodoController(store, view);
// 起動🚀
controller.init();
5) HTML(例)🧁(idだけ合ってればOK)
<div id="app">
<h1>CampusTodo 📚✅</h1>
<form id="todo-form">
<input id="todo-input" type="text" placeholder="課題を入力してね✍️" />
<button type="submit">追加➕</button>
</form>
<ul id="todo-list"></ul>
</div>
動作チェック✅✨(できたら勝ち🎉)
- 入力して「追加➕」で増える
- Enterでも増える(form submit)
- 追加後、入力欄が空になってカーソル戻る
- チェックで取り消し線がつく(done切替)
- 空欄で追加するとエラーが出る
よくあるハマりポイント集🕳️😵💫(ここ大事!)
① 追加した瞬間ページがリロードされる😇
原因:フォームsubmitのデフォルト動作(ページ遷移)
対策:e.preventDefault() 🛑 (MDN Web Docs)
② submitイベントをボタンに付けて動かない🙃
submitは formに発火するよ〜! (MDN Web Docs)
なので form.addEventListener("submit", ...) が正解✅
③ Viewで todos を直接いじりたくなる誘惑🍰
いじりたくなるけど…ここは我慢!😂 Viewは「表示だけ」にすると、あとで機能追加がラクになるよ✨
ミニ演習(3つ)💪🎀
演習A:二重送信を防ぐ🛡️
- 空欄なら追加しない(今はError出るだけ)
- できれば「追加ボタンを一瞬 disabled」にしてもOK✨
演習B:件数表示を付ける📊
- 例:
未完了: 3 / 全部: 5を表示 - どこで数える? → Controllerで計算してViewに渡す が気持ちいいよ😌✨
演習C:イベント設計を整理🧾
- 追加:
AddTodo(title) - 切替:
ToggleTodo(id) - いまは関数で直渡ししてるけど、「操作名」を意識すると第8章がスムーズ🗺️✨
AI相棒の使い方🤖✨(この章向けプロンプト例)
コピペでどうぞ💌(そのまま採用じゃなくて、差分レビューね!👀)
- 「TodoViewに
bindAddTodoとbindToggleTodoを追加したい。Viewはロジックを持たず、イベントを外に通知するだけにしたい。最小実装例をTypeScriptで」 - 「Controllerで
handleAdd/handleToggleを作りたい。例外処理と再描画の責務をきれいに保ちたい」 - 「changeイベントのイベント委任の例を、checkbox + datasetで」
- 「submitでリロードされる問題の原因と対策を、初心者向けに説明して」
- 「MVCでControllerが太り始めるサインを箇条書きで(次章への予告にも使う)」
まとめ🌸✨
今日できたのはこれ!🎉
- イベント(入力) → Controllerが受け取る🎮
- Modelを更新 → Viewに描画指示🧠➡️🎨
- MVCが グルグル回り始めた🌀✅
次の章(第7章)は、この回転を「ルール化」して、さらにブレないMVCにしていくよ〜!🚀💕