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

JavaScript
スポンサーリンク

2日目のゴールと今日のテーマ

2日目は「ToDoアプリを“ちゃんと動くもの”に近づける日」です。
1日目で作ったのは、あくまで「設計の土台」と「表示の骨組み」でした。

今日はここまで持っていきます。

  • タスクを追加できる
  • タスクを完了/未完了に切り替えられる
  • タスクを削除できる
  • フィルタ(全件/未完了/完了)がちゃんと効く

そして、その裏側で大事なのは、

  • 状態(state)をどう更新するか
  • 配列+オブジェクトのデータ構造をどう扱うか
  • 再描画(render)をどうシンプルに保つか

この 3 つです。


状態管理の復習と、今日の拡張ポイント

1日目の state をもう一度確認する

昨日の時点で、state はこんな形でした。

const state = {
  tasks: [],
  filter: "all", // "all" | "active" | "completed"
};
JavaScript

そして、タスクはこういうオブジェクトでした。

{
  id: 1,
  title: "牛乳を買う",
  done: false,
}
JavaScript

今日やることは、この tasks 配列に対して

  • 追加(push)
  • 完了状態の切り替え(true / false の変更)
  • 削除(配列から取り除く)

を、きれいな形で実装することです。

ここで大事なのは、「どのタスクを操作するか」を
id で特定するという考え方です。


完了切り替えの設計:id でタスクを探して done を反転する

なぜ id が必要なのか

画面上にはタスクが縦に並んでいますが、
「上から何番目か」で判断すると、フィルタや削除でズレてしまいます。

だから、タスクごとに一意な id を持たせておき、
「この id のタスクを操作する」という形にします。

完了切り替え関数を作る

まずは、state を更新する関数から作ります。

function toggleTaskDone(id) {
  const task = state.tasks.find((t) => t.id === id);
  if (!task) return;

  task.done = !task.done;
  render();
}
JavaScript

ここでのポイントは二つです。

一つ目は、find で「id が一致するタスク」を探していること。
配列のインデックスではなく、id で探すことで、
順番が変わっても正しく動きます。

二つ目は、「状態を変えたら必ず render を呼ぶ」というルール。
画面は state の“結果”なので、state を変えたら再描画が必要です。


削除の設計:filter で「残すものだけ」を残す

削除は「消す」ではなく「残すものを選ぶ」

タスク削除は、こう考えるとシンプルです。

  • 「この id のタスクを消す」
    =「この id 以外のタスクだけを残す」

JavaScript の配列では、filter がぴったりです。

function deleteTask(id) {
  state.tasks = state.tasks.filter((t) => t.id !== id);
  render();
}
JavaScript

ここでの重要ポイントは、

  • filter は「条件を満たす要素だけを残した新しい配列」を返す
  • それをそのまま state.tasks に代入している

というところです。

「配列を直接いじる(splice など)」よりも、
“新しい配列を作って入れ替える”ほうがバグりにくいという感覚も、
少しずつ意識しておくと良いです。


DOM と state をつなぐ:完了ボタンと削除ボタンに id を渡す

render の中で「ボタンに id を持たせる」

1日目の render を、少しだけ拡張します。

function render() {
  tasksContainerEl.innerHTML = "";

  const visibleTasks = getVisibleTasks();

  visibleTasks.forEach((task) => {
    const rowEl = document.createElement("div");
    rowEl.className = "task-item";

    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 deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";
    deleteButton.addEventListener("click", () => {
      deleteTask(task.id);
    });

    actionsEl.appendChild(toggleButton);
    actionsEl.appendChild(deleteButton);

    rowEl.appendChild(titleEl);
    rowEl.appendChild(actionsEl);

    tasksContainerEl.appendChild(rowEl);
  });
}
JavaScript

ここで深掘りしたいのは、

  • render の中でイベントリスナーを付けている
  • そのときに「どのタスクか」を示すために task.id をクロージャで閉じ込めている

という点です。

「ボタンが押されたときに、どのタスクを操作するか」を
id でつなぐのが、設計としてとてもきれいです。


フィルタと完了状態の連動を体験する

filter の値で「見せるタスク」を変える

1日目で作った getVisibleTasks をもう一度見ます。

function getVisibleTasks() {
  if (state.filter === "all") {
    return state.tasks;
  }
  if (state.filter === "active") {
    return state.tasks.filter((task) => !task.done);
  }
  if (state.filter === "completed") {
    return state.tasks.filter((task) => task.done);
  }
  return state.tasks;
}
JavaScript

ここでのポイントは、

  • state.tasks は「全タスク」
  • filter に応じて「見せるタスク」を絞り込んでいるだけ

ということです。

完了ボタンを押して task.done が true になれば、
「未完了フィルタ」では見えなくなり、
「完了フィルタ」では見えるようになります。

つまり、

  • データ(state.tasks)は 1 つ
  • 見せ方(filter)は複数

という構造になっています。


タスク追加の UX を少しだけ良くする

Enter キーでも追加できるようにする

ボタンだけでなく、Enter キーでも追加できると気持ちいいです。

taskInputEl.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    addTask(taskInputEl.value);
    taskInputEl.value = "";
  }
});
JavaScript

空文字は追加しない

1日目で書いた addTask をもう一度確認します。

let nextId = 1;

function addTask(title) {
  const trimmed = title.trim();
  if (!trimmed) {
    return;
  }

  const newTask = {
    id: nextId++,
    title: trimmed,
    done: false,
  };

  state.tasks.push(newTask);
  render();
}
JavaScript

ここでのポイントは、

  • trim() で前後の空白を削除してからチェックしている
  • 空文字なら何もしない(state を変えない)

という「入力の最低限のバリデーション」です。


状態管理の“感覚”をつかむためのミニ実験

実験 1:コンソールからタスクを追加してみる

ブラウザのコンソールを開いて、こう打ってみてください。

state.tasks.push({ id: 999, title: "コンソールから追加", done: false });
render();
JavaScript

画面に「コンソールから追加」が出てきたら、
「画面は state の結果である」という感覚が、少しリアルになります。

実験 2:filter を直接変えてみる

同じくコンソールで、

state.filter = "completed";
render();
JavaScript

とすると、「完了タスクだけ」が表示されます。

この感覚が大事です。

  • 画面を直接いじるのではなく
  • state を変えて、render で画面を“再計算”する

これが「状態管理(state)+再描画ロジック」の基本です。


2日目のまとめと、明日へのつなぎ

今日あなたがやったことは、ToDo アプリの「動く中身」のコアです。

  • タスクを配列+オブジェクトで管理し、id で識別した
  • 完了切り替え(done true/false)を id ベースで実装した
  • 削除を filter で「残すものだけ残す」形で実装した
  • filter と done 状態の連動を体験した
  • state を変えたら render で画面を作り直す、という流れを体に入れた

明日(3日目)はここから、

  • フィルタボタンの「選択中スタイル」を state と連動させる
  • タスク数(全件/未完了)のカウンターを追加する
  • 設計を少し整理して、関数を分割する

といった「設計の磨き込み」に入っていきます。

もし余力があれば、2日目のうちに

  • タスクを 5 件くらい追加して、完了/削除/フィルタをいじり倒す
  • toggleTaskDonedeleteTask の中に console.log(state.tasks) を入れて、
    配列がどう変わっているか眺めてみる

など、「中身の配列」と「画面の動き」を結びつけてみてください。
そこがつながると、一気に“設計が見える人”になっていきます。

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