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

JavaScript 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;
→ そのタスクの isDonetrue になる
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をコピーしました