JavaScript | ゼロからはじめるプログラミング、30日で基礎を学ぶJavaScript:ミニWebアプリ開発 - Day26:TODOアプリ②

JavaScript JavaScript
スポンサーリンク

Day26 後半のゴール

前半で「削除ボタンを押すと、そのタスクが配列から消えて再描画される」ところまではできました。
後半ではそこから一歩進めて、

削除処理を関数として整理する
コードの見通しを良くする
今後の「完了機能」「保存機能」を見据えた形に整える

という「設計の仕上げ」をしていきます。


削除処理を関数に切り出す

なぜ関数にするのかをもう一度確認する

前半の削除処理は、renderTasks の中に直接書いていました。

deleteButtonElement.addEventListener("click", () => {
  tasks.splice(index, 1);
  renderTasks();
});
JavaScript

これでも動きますが、次のような問題が出てきます。

削除のルールを変えたくなったときに、renderTasks の中をいじる必要がある
削除処理のテストやデバッグがしづらい
「配列をいじる処理」と「画面を作る処理」が混ざってしまう

そこで、「削除」という行為を関数として切り出します。

deleteTask 関数を作る

削除処理を担当する関数を作ります。

function deleteTask(index) {
  tasks.splice(index, 1);
}
JavaScript

そして、削除ボタンのイベントではこう書きます。

deleteButtonElement.addEventListener("click", () => {
  deleteTask(index);
  renderTasks();
});
JavaScript

これで役割が分かれます。

deleteTask
配列(tasks)からタスクを消す担当

renderTasks
配列の内容を画面に反映する担当

削除ボタンのイベント
「どの index を消すか」を決めて、流れを組み立てる担当

この分離が、アプリを大きくしていくときの土台になります。


コード全体を「入力・処理・出力」で見直す

TODOアプリの構造を言葉で説明できるようにする

ここで一度、TODOアプリ全体を「入力・処理・出力」で整理してみます。

入力
タスクのテキスト(追加)
削除ボタンのクリック(削除)

処理
addTask(text) で tasks に追加
deleteTask(index) で tasks から削除

出力
renderTasks() で tasks の内容を画面に描画

この構造をそのままコードに反映させると、こうなります。

const taskInputElement = document.getElementById("taskInput");
const addButtonElement = document.getElementById("addButton");
const taskListElement = document.getElementById("taskList");

let tasks = [];

function addTask(text) {
  tasks.push(text);
}

function deleteTask(index) {
  tasks.splice(index, 1);
}

function renderTasks() {
  taskListElement.textContent = "";

  tasks.forEach((text, index) => {
    const taskItemElement = document.createElement("div");

    const taskTextElement = document.createElement("span");
    taskTextElement.textContent = text;

    const deleteButtonElement = document.createElement("button");
    deleteButtonElement.textContent = "削除";

    deleteButtonElement.addEventListener("click", () => {
      deleteTask(index);
      renderTasks();
    });

    taskItemElement.appendChild(taskTextElement);
    taskItemElement.appendChild(deleteButtonElement);

    taskListElement.appendChild(taskItemElement);
  });
}

addButtonElement.addEventListener("click", () => {
  const text = taskInputElement.value.trim();

  if (text === "") {
    return;
  }

  addTask(text);
  renderTasks();
  taskInputElement.value = "";
});
JavaScript

このコードは、「誰が何を担当しているか」を説明しやすい形になっています。
それが、設計として強い状態です。


削除機能の挙動を丁寧に追ってみる

1クリックで何が起きているかを分解する

削除ボタンを1回クリックしたとき、内部では次の流れが起きています。

削除ボタンのクリックイベントが発火する
そのボタンに紐づいている index が使われる
deleteTask(index) が呼ばれ、tasks からその要素が消える
renderTasks() が呼ばれ、画面が「今の tasks」をもとに描き直される

これをコードに沿って追ってみます。

deleteButtonElement.addEventListener("click", () => {
  deleteTask(index);
  renderTasks();
});
JavaScript

deleteTask(index) の中では、

function deleteTask(index) {
  tasks.splice(index, 1);
}
JavaScript

が実行され、配列が変わります。

その後の renderTasks() では、
一度 taskListElement を空にしてから、
更新された tasks をもとにタスクを並べ直します。

ここで大事なのは、

「画面から要素を1個だけ消す」のではなく、
「配列を更新してから、画面を全部描き直す」

という発想です。
この考え方に慣れると、バグが減り、機能追加もしやすくなります。


削除機能のよくあるつまずきポイント

index がズレる問題をどう避けているか

初心者がよくハマるのが、「index がズレる」問題です。

例えば、もし renderTasks を使わずに、

削除ボタンの親要素だけ remove する
配列はそのまま

という実装をすると、
配列の中身と画面の並びがズレていきます。

今回の実装では、

削除のたびに renderTasks で「全部描き直す」
forEach の index は、その時点の tasks に対して毎回計算される

という形にしているので、
index のズレ問題をシンプルに回避できています。

「配列を真実として、画面は毎回描き直す」
この設計が、実はかなり強力な安全装置になっています。


デバッグ例:削除がうまくいかないときの見方

どこを console.log すればいいか

もし、

削除ボタンを押しても何も起きない
違うタスクが消える気がする

と感じたら、次のようにログを仕込んでみてください。

function deleteTask(index) {
  console.log("削除前 tasks:", tasks);
  console.log("削除する index:", index);

  tasks.splice(index, 1);

  console.log("削除後 tasks:", tasks);
}
JavaScript

さらに、renderTasks の中にも入れておくと安心です。

function renderTasks() {
  console.log("renderTasks を実行します。tasks:", tasks);

  taskListElement.textContent = "";

  tasks.forEach((text, index) => {
    console.log("描画中 index:", index, "text:", text);

    const taskItemElement = document.createElement("div");
    const taskTextElement = document.createElement("span");
    taskTextElement.textContent = text;

    const deleteButtonElement = document.createElement("button");
    deleteButtonElement.textContent = "削除";

    deleteButtonElement.addEventListener("click", () => {
      deleteTask(index);
      renderTasks();
    });

    taskItemElement.appendChild(taskTextElement);
    taskItemElement.appendChild(deleteButtonElement);
    taskListElement.appendChild(taskItemElement);
  });
}
JavaScript

これで、

削除前の配列の状態
どの index を消そうとしているか
削除後の配列の状態
描画時にどの index と text が使われているか

が全部見えるようになります。

「何が起きているかを可視化する」ことが、
デバッグの本質です。


Day26 後半の完成コード

タスク追加+削除が揃った TODO アプリ

HTML は Day25 と同じで構いません。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>TODOアプリ</title>
  </head>
  <body>
    <h1>TODOアプリ</h1>

    <h2>タスク追加</h2>
    <input id="taskInput" type="text" placeholder="やることを入力">
    <button id="addButton">追加</button>

    <h2>タスク一覧</h2>
    <div id="taskList"></div>

    <script>
      const taskInputElement = document.getElementById("taskInput");
      const addButtonElement = document.getElementById("addButton");
      const taskListElement = document.getElementById("taskList");

      let tasks = [];

      function addTask(text) {
        tasks.push(text);
      }

      function deleteTask(index) {
        tasks.splice(index, 1);
      }

      function renderTasks() {
        taskListElement.textContent = "";

        tasks.forEach((text, index) => {
          const taskItemElement = document.createElement("div");

          const taskTextElement = document.createElement("span");
          taskTextElement.textContent = text;

          const deleteButtonElement = document.createElement("button");
          deleteButtonElement.textContent = "削除";

          deleteButtonElement.addEventListener("click", () => {
            deleteTask(index);
            renderTasks();
          });

          taskItemElement.appendChild(taskTextElement);
          taskItemElement.appendChild(deleteButtonElement);

          taskListElement.appendChild(taskItemElement);
        });
      }

      addButtonElement.addEventListener("click", () => {
        const text = taskInputElement.value.trim();

        if (text === "") {
          return;
        }

        addTask(text);
        renderTasks();
        taskInputElement.value = "";
      });
    </script>
  </body>
</html>

この時点で、

タスクを追加できる
タスクを削除できる
配列でタスクを管理している
配列から画面を描画している

という「TODOアプリの基本セット」が揃いました。


Day26 後半のまとめ

今日の後半で押さえたポイントは、こう整理できます。

削除処理を deleteTask(index) として関数に切り出した
renderTasks は「画面を作るだけ」に専念させた
削除ボタンは forEach の index を使って「自分のタスク」を特定している
配列を更新してから画面を描き直す設計にすることで、index のズレ問題を避けている
デバッグでは「削除前後の tasks」と「描画時の index・text」を見る

ここまで来ると、
次の「完了フラグを付ける」「localStorage に保存する」といった機能も、
同じ設計の延長線上で自然に追加できるようになります。

TODOアプリは小さいけれど、設計と実装のエッセンスがぎゅっと詰まった題材です。
ここで身につけた感覚は、他のどんなミニアプリにもそのまま応用できます。

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