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

JavaScript 30日で身につけるJavaScript
スポンサーリンク
スポンサーリンク

Day28 後半のゴール

前半で、タスクを
「{ title: ..., isDone: ... } というオブジェクトで管理する」
「isDone の値に応じてチェックボックスの ON/OFF を反映する」
ところまで進みました。

後半では、ここから一歩踏み込んで、

チェックボックスをクリックしたら isDone を切り替える
完了したタスクの見た目を変える(取り消し線など)
完了状態も含めて localStorage に保存・復元する

という、「完了機能の本体」を作っていきます。


チェックボックスと isDone を双方向につなぐ

クリックされたときに isDone を更新する

前半では、
「task.isDone の値をチェックボックスに反映する」
という一方向の流れだけを作りました。

後半では逆方向、つまり

ユーザーがチェックボックスをクリックする
→ task.isDone の値を更新する

という流れを追加します。

チェックボックスには change イベントを付けるのが定番です。

checkboxElement.addEventListener("change", () => {
  task.isDone = checkboxElement.checked;
});
JavaScript

ここでやっていることはシンプルです。

checkboxElement.checked
今のチェック状態(true / false)

それをそのまま
task.isDone に代入することで、
「UI の状態をデータに反映」しています。

この一行で、
isDone → checked(前半)
checked → isDone(後半)
の両方向がつながります。


完了状態の変更も保存する

チェックを変えたら saveTasks も呼ぶ

完了状態も、当然 localStorage に保存したい情報です。
なので、チェックボックスの change イベントの中で
saveTasks() を呼びます。

checkboxElement.addEventListener("change", () => {
  task.isDone = checkboxElement.checked;
  saveTasks();
  renderTasks();
});
JavaScript

ここで renderTasks() も呼んでいるのは、
「見た目もすぐに反映したい」からです。

このイベントの流れはこうなります。

ユーザーがチェックを付ける/外す
→ task.isDone が更新される
→ saveTasks() で localStorage に保存される
→ renderTasks() で画面が最新状態に描き直される

これで、「完了状態も含めて永続化される TODO」が完成に近づきます。


完了タスクの見た目を変える

取り消し線を付けて「終わった感」を出す

完了したタスクは、未完了と見た目で区別したいですよね。
一番分かりやすいのは「取り消し線」です。

CSS を使う方法もありますが、
まずは JavaScript だけでやってみます。

if (task.isDone) {
  taskTextElement.style.textDecoration = "line-through";
  taskTextElement.style.color = "#888";
} else {
  taskTextElement.style.textDecoration = "none";
  taskTextElement.style.color = "inherit";
}
JavaScript

これを renderTasks の中で、
taskTextElement.textContent = task.title; のあとに書きます。

これで、

isDone === true のタスク
→ 取り消し線+少し薄い色

isDone === false のタスク
→ 普通の表示

という視覚的な違いが生まれます。

「データの状態を UI に反映する」という感覚を、
ここでしっかり体に染み込ませてほしいところです。


完了機能を含めた renderTasks の全体像

追加・削除・完了がそろった描画処理

ここまでの内容を反映した renderTasks は、こんな形になります。

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

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

    const checkboxElement = document.createElement("input");
    checkboxElement.type = "checkbox";
    checkboxElement.checked = task.isDone;

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

    if (task.isDone) {
      taskTextElement.style.textDecoration = "line-through";
      taskTextElement.style.color = "#888";
    } else {
      taskTextElement.style.textDecoration = "none";
      taskTextElement.style.color = "inherit";
    }

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

    checkboxElement.addEventListener("change", () => {
      task.isDone = checkboxElement.checked;
      saveTasks();
      renderTasks();
    });

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

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

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

この関数の中で起きていることを整理すると、

タスク1件ごとに
チェックボックス(完了状態の表示+変更)
テキスト(タイトル+見た目の変化)
削除ボタン(削除機能)

をまとめて作り、
イベントもその場で紐づけています。


localStorage と完了状態の関係を確認する

JSON にも isDone が含まれていることを意識する

tasks は今、こういう形の配列です。

[
  { title: "買い物に行く", isDone: false },
  { title: "メールを送る", isDone: true }
]
JavaScript

これを JSON.stringify(tasks) すると、
だいたいこんな文字列になります。

[
  {"title":"買い物に行く","isDone":false},
  {"title":"メールを送る","isDone":true}
]
JavaScript

つまり、
title だけでなく isDone も含めて
localStorage に保存されている、ということです。

ページを再読み込みしたときは、

loadTasks() で JSON を読み込む
JSON.parse で配列に戻す
renderTasks() でチェック状態と見た目を反映する

という流れになるので、
完了状態もちゃんと復元されます。


例題:完了機能の動きを頭の中でシミュレーションする

シナリオを一つ追ってみる

例えば、こんな操作をしたとします。

タスクを 2 つ追加する
「買い物に行く」
「メールを送る」

この時点で tasks はこうです。

[
  { title: "買い物に行く", isDone: false },
  { title: "メールを送る", isDone: false }
]
JavaScript

次に、「メールを送る」のチェックボックスを ON にします。

checkboxElement.change イベントが発火
→ task.isDone = checkboxElement.checked;
→ そのタスクの isDone が true になる
→ saveTasks() で localStorage に保存
→ renderTasks() で再描画

再描画後は、

1件目:isDone = false → チェックなし、取り消し線なし
2件目:isDone = true → チェックあり、取り消し線あり

という状態になります。

ここまでを自分の頭で追えるようになると、
「コードを読む力」が一段上がります。


Day28 後半の完成コード(イメージ)

追加・削除・完了・保存・復元がそろった TODO

HTML はこれまでと同じ前提で、
JavaScript 部分のイメージをまとめておきます。

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

let tasks = [];

function addTask(text) {
  const task = {
    title: text,
    isDone: false
  };
  tasks.push(task);
}

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

function saveTasks() {
  const json = JSON.stringify(tasks);
  localStorage.setItem("tasks", json);
}

function loadTasks() {
  const json = localStorage.getItem("tasks");

  if (json === null) {
    tasks = [];
    return;
  }

  tasks = JSON.parse(json);
}

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

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

    const checkboxElement = document.createElement("input");
    checkboxElement.type = "checkbox";
    checkboxElement.checked = task.isDone;

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

    if (task.isDone) {
      taskTextElement.style.textDecoration = "line-through";
      taskTextElement.style.color = "#888";
    } else {
      taskTextElement.style.textDecoration = "none";
      taskTextElement.style.color = "inherit";
    }

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

    checkboxElement.addEventListener("change", () => {
      task.isDone = checkboxElement.checked;
      saveTasks();
      renderTasks();
    });

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

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

    taskListElement.appendChild(taskItemElement);
  });
}

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

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

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

loadTasks();
renderTasks();
JavaScript

この時点で、あなたの TODO アプリは、

タスク追加
タスク削除
完了状態の切り替え
完了状態の見た目反映
localStorage への保存・復元

という、かなり「実用レベル」の機能を備えています。


Day28 後半のまとめ

今日の後半で押さえたポイントを、ぎゅっと絞るとこうなります。

チェックボックスの change イベントで task.isDone を更新した
完了状態の変更時にも saveTasks() と renderTasks() を呼ぶようにした
isDone に応じて取り消し線や色を変え、見た目で完了が分かるようにした
tasks をオブジェクト配列にしたことで、完了状態も含めて localStorage に保存できるようになった

ここまで来ると、
「自分で機能を足していける TODO アプリ」の土台は完全にできています。

例えば次のステップとして、

完了タスクを下にまとめる
完了タスクを一括削除する
未完了だけ表示するフィルタを付ける

みたいな機能も、今の設計なら十分狙えます。
もう「おもちゃ」ではなく、ちゃんとしたミニアプリの世界に入っていますよ。

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