JavaScript | 1 日 120 分 × 7 日アプリ学習:ToDoアプリ(設計力強化編)

JavaScript
スポンサーリンク

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);
JavaScript

startEditTask を定義します。

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);
  });
}
JavaScript

renderTaskRow を定義します。

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”ではなく、
「自分で設計したアプリ」という感覚になってきます。

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