4日目のゴールと今日のテーマ
4日目は「ToDoアプリの設計を“もう一段だけ大人にする日”」です。
1〜3日目で、すでにあなたの ToDo は
- 追加できる
- 完了/未完了を切り替えられる
- 削除できる
- フィルタで表示を切り替えられる
という“ちゃんと動くアプリ”になりました。
今日はここから一歩進めて、
- タスク編集(タイトル変更)の設計
- 空のときのメッセージ表示
- コードの「責務の分離」をもう少しだけ進める
という「設計力を磨く」方向に進みます。
機能としては小さいけれど、設計のセンスが一気に伸びるところです。
タスク編集機能を設計する
まずは「どういう UX にするか」を決める
タスク編集にはいくつかパターンがあります。
- タスク名をクリックしたら、入力欄に変わる
- 「編集」ボタンを押したら、編集モードになる
- 別の場所に「選択中タスクの編集フォーム」が出る
中級編としては、「行内編集(インライン編集)」が良い題材です。
つまり、
- 普段はテキストとして表示
- 編集ボタンを押すと、その行だけ input に変わる
- Enter で確定、Esc でキャンセル
という形を目指します。
ここで大事なのは、
「今どのタスクが編集モードか」も state で管理する
という発想です。
state に「編集中の id」を追加する
今の state はこうでした。
const state = {
tasks: [],
filter: "all",
};
JavaScriptここに、編集中のタスク id を追加します。
const state = {
tasks: [],
filter: "all",
editingTaskId: null, // いま編集中のタスクの id(なければ null)
};
JavaScriptこの 1 行で、「編集モード」という概念を
アプリ全体の状態として扱えるようになります。
編集モードの切り替えを設計する
編集開始:editingTaskId に id を入れる
タスク行に「編集」ボタンを追加します。
const editButton = document.createElement("button");
editButton.textContent = "編集";
editButton.addEventListener("click", () => {
startEditTask(task.id);
});
actionsEl.appendChild(editButton);
JavaScriptstartEditTask を定義します。
function startEditTask(id) {
state.editingTaskId = id;
render();
}
JavaScriptここでのポイントは、
- 「どのタスクを編集しているか」は editingTaskId が真実
- 行側は「自分の id と editingTaskId が一致しているか」で表示を変える
という構造にすることです。
編集終了:editingTaskId を null に戻す
編集を確定したとき、キャンセルしたときは、
editingTaskId を null に戻します。
function finishEditTask() {
state.editingTaskId = null;
render();
}
JavaScriptこの「モードを state で持つ」という感覚は、
フォームバリデーション編ともつながる、とても大事な設計の軸です。
タスク行の描画を「表示モード」と「編集モード」に分ける
1 行の描画を関数に切り出す
3日目までの renderTaskList の中で、
1 行分の DOM をその場で組み立てていました。
4日目では、1 行を描画する処理を関数に分けます。
function renderTaskList() {
tasksContainerEl.innerHTML = "";
const visibleTasks = getVisibleTasks();
visibleTasks.forEach((task) => {
const rowEl = renderTaskRow(task);
tasksContainerEl.appendChild(rowEl);
});
}
JavaScriptrenderTaskRow を定義します。
function renderTaskRow(task) {
const rowEl = document.createElement("div");
rowEl.className = "task-item";
const isEditing = state.editingTaskId === task.id;
if (isEditing) {
renderTaskRowEditing(rowEl, task);
} else {
renderTaskRowDisplay(rowEl, task);
}
return rowEl;
}
JavaScriptここでの重要ポイントは、
- 「この行は編集モードか?」を state から判断している
- 行の中身の描画を「表示用」と「編集用」に分けている
という構造です。
これで、1 行の中に if 文がベタッと書かれず、読みやすくなります。
表示モードの行を描画する
function renderTaskRowDisplay(rowEl, task) {
const titleEl = document.createElement("div");
titleEl.className = "task-title";
if (task.done) {
titleEl.classList.add("done");
}
titleEl.textContent = task.title;
const actionsEl = document.createElement("div");
actionsEl.className = "task-actions";
const toggleButton = document.createElement("button");
toggleButton.textContent = task.done ? "未完了に戻す" : "完了";
toggleButton.addEventListener("click", () => {
toggleTaskDone(task.id);
});
const editButton = document.createElement("button");
editButton.textContent = "編集";
editButton.addEventListener("click", () => {
startEditTask(task.id);
});
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
deleteButton.addEventListener("click", () => {
deleteTask(task.id);
});
actionsEl.appendChild(toggleButton);
actionsEl.appendChild(editButton);
actionsEl.appendChild(deleteButton);
rowEl.appendChild(titleEl);
rowEl.appendChild(actionsEl);
}
JavaScript編集モードの行を描画する
function renderTaskRowEditing(rowEl, task) {
const inputEl = document.createElement("input");
inputEl.type = "text";
inputEl.value = task.title;
inputEl.className = "task-title-edit";
const actionsEl = document.createElement("div");
actionsEl.className = "task-actions";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
saveButton.addEventListener("click", () => {
updateTaskTitle(task.id, inputEl.value);
});
cancelButton.addEventListener("click", () => {
finishEditTask();
});
inputEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
updateTaskTitle(task.id, inputEl.value);
} else if (event.key === "Escape") {
finishEditTask();
}
});
actionsEl.appendChild(saveButton);
actionsEl.appendChild(cancelButton);
rowEl.appendChild(inputEl);
rowEl.appendChild(actionsEl);
setTimeout(() => {
inputEl.focus();
inputEl.select();
}, 0);
}
JavaScriptここでの深掘りポイントは、
- 編集モードでは「タイトル表示」ではなく「input」を使っている
- 保存・キャンセル・Enter・Esc で自然な UX を作っている
setTimeout(..., 0)で描画後にフォーカスを当てている
というところです。
特に最後のフォーカスは、「ユーザーがすぐ編集できる」体験として大事です。
タスクタイトル更新のロジックを設計する
updateTaskTitle を state 更新専用にする
タイトル更新の関数を作ります。
function updateTaskTitle(id, newTitle) {
const trimmed = newTitle.trim();
if (!trimmed) {
finishEditTask();
return;
}
const task = state.tasks.find((t) => t.id === id);
if (!task) {
finishEditTask();
return;
}
task.title = trimmed;
finishEditTask();
}
JavaScriptここでの重要ポイントは、
- 空文字は許可しない(そのままキャンセル扱い)
- id でタスクを探し、見つかったら title を更新
- 最後に finishEditTask で編集モードを解除し、render を呼ぶ
という流れです。
finishEditTask の中で render を呼ぶようにしておけば、
updateTaskTitle からは render を直接呼ばなくて済みます。
function finishEditTask() {
state.editingTaskId = null;
render();
}
JavaScriptこうやって「状態を変える関数」と「画面を描く関数」の関係を
少しずつ整理していくと、コードがどんどん読みやすくなります。
空のときのメッセージ表示を追加する
タスクが 0 件のときに「何もない」だと寂しい
UX 的に、タスクが 1 件もないときに
「ただ真っ白」だと、ユーザーは不安になります。
そこで、「タスクがありません。まずは 1 件追加してみましょう。」
のようなメッセージを出してあげます。
renderTaskList に空チェックを入れる
function renderTaskList() {
tasksContainerEl.innerHTML = "";
const visibleTasks = getVisibleTasks();
if (visibleTasks.length === 0) {
const emptyEl = document.createElement("div");
emptyEl.textContent = "タスクがありません。新しいタスクを追加してみましょう。";
emptyEl.style.color = "#777";
emptyEl.style.fontSize = "12px";
tasksContainerEl.appendChild(emptyEl);
return;
}
visibleTasks.forEach((task) => {
const rowEl = renderTaskRow(task);
tasksContainerEl.appendChild(rowEl);
});
}
JavaScriptここでのポイントは、
- 「フィルタの結果が 0 件」のときにもメッセージが出る
(完了フィルタで完了タスクがない場合など) - 「何もない」のではなく「今どういう状態か」を伝える
という UX の配慮です。
4日目のまとめと、明日へのつなぎ
今日あなたがやったのは、「ToDoアプリを設計として一段引き上げる」作業でした。
- state に editingTaskId を追加し、「編集モード」を状態として扱った
- 1 行の描画を表示モード/編集モードに分けて整理した
- タスク編集(タイトル変更)を id ベースで実装した
- 保存・キャンセル・Enter・Esc で自然な編集 UX を作った
- 空のときのメッセージ表示で「状態を言葉で伝える」ようにした
明日(5日目)はここから、
- データ構造と state の設計をもう一度俯瞰して整理する
- 小さなリファクタリング(関数の分割・命名の見直し)
- 「この設計なら機能を増やしても壊れないか?」を考える
という、「設計を俯瞰して見る目」を育てていきます。
もし余力があれば、4日目のうちに
- 実際に編集モードにして、タイトルを変えまくってみる
- 編集中にフィルタを切り替えたらどうなるか試してみる
など、「状態」と「画面」の関係を自分の目で確かめてみてください。
そこが見えてくると、もう“ただの ToDo”ではなく、
「自分で設計したアプリ」という感覚になってきます。

