JavaScript | 2週間で身につく、アプリを作りながら学ぶJavaScriptの基本 - 9日目

JavaScript JavaScript
スポンサーリンク
  1. 9日目のゴールとテーマ
  2. 8日目のタスクアプリをざっくりおさらい
    1. どんな構造だったかを言葉で整理する
  3. 未完了だけ表示する“フィルタ”機能を考える
    1. 「全部表示」と「未完了だけ表示」を切り替えたい
  4. フィルタ用のボタンをHTMLに追加する
    1. モード切り替えのUIを用意する
  5. 表示モードを表す変数を用意する
    1. “今どういう表示状態か”をコードで持つ
  6. renderTasks を「フィルタを考慮する」形に変える
    1. tasks から「表示対象の配列」を作ってから描画する
  7. フィルタ後でも正しいインデックスを持たせる
    1. targetTasks に「元のインデックス」も一緒に持たせる
  8. 表示モード切り替えボタンのイベントを書く
    1. currentFilter を変えてから renderTasks を呼ぶ
  9. 完了タスクを一括削除する機能を考える
    1. 「完了しているものだけ全部消す」
  10. 一括削除ボタンをHTMLに追加する
    1. UIを足してから、処理を考える
  11. 完了タスク一括削除の処理を書く
    1. 新しい配列を作り直すイメージで
  12. タスク数(未完了・完了)を表示する
    1. 「今どれくらい残っているか」が分かると一気にアプリっぽくなる
  13. カウンター表示用のHTMLを追加する
    1. 数字を表示する場所を用意する
  14. カウンターを更新する関数を作る
    1. tasks 配列から数字を計算する
  15. renderTasks と一緒に updateTaskCount も呼ぶ
    1. 「表示」と「カウント」を常に同期させる
  16. タスクをブラウザに保存する(localStorage の入口)
    1. ページをリロードしてもタスクが消えないようにする
  17. localStorage の基本イメージ
    1. キーと値で文字列を保存する
  18. tasks を保存する関数と、読み込む関数を作る
    1. JSON.stringify と JSON.parse を使う
  19. 状態が変わるたびに saveTasks を呼ぶ
    1. 「配列が変わったら保存する」を習慣にする
  20. 初期表示のときに loadTasks → renderTasks を呼ぶ
    1. ページを開いた瞬間に前回の状態を復元する
  21. 9日目で一番大事な感覚
    1. 「状態は配列だけじゃない」「状態は保存もできる」
  22. 9日目のまとめ

9日目のゴールとテーマ

9日目のテーマは「タスク管理アプリを“実用レベル”に近づける」です。
8日目で「追加・完了切替・削除」ができる ToDo アプリができました。
今日はそこに、

未完了だけ表示する
完了タスクを一括削除する
タスク数(未完了・完了)を表示する
ページをリロードしてもタスクが消えないように保存する(localStorage の入口)

といった機能を足して、「毎日使えるアプリ」に一歩近づけていきます。


8日目のタスクアプリをざっくりおさらい

どんな構造だったかを言葉で整理する

8日目のタスクアプリは、こんな構造でした。

タスク1件分は { title: "牛乳を買う", done: false } のようなオブジェクト。
それを tasks という配列に入れていた。
renderTasks() 関数が tasks をもとに HTML を組み立てて、task-list-area に表示していた。
「追加」ボタンでタスクを配列に push して、renderTasks() を呼んでいた。
「完了切替」「削除」は、ボタンの data 属性(data-index, data-action)を使って、
クリックされたタスクのインデックスを特定して処理していた。

今日は、この構造はそのままに、「表示の仕方」と「保存の仕方」を賢くしていきます。


未完了だけ表示する“フィルタ”機能を考える

「全部表示」と「未完了だけ表示」を切り替えたい

まずは、「未完了のタスクだけ見たい」というよくあるニーズに応えます。
やりたいことはシンプルです。

全部表示モード
未完了だけ表示モード

この2つを切り替えられるようにします。

ここで大事なのは、「tasks 自体は変えない」ということです。
あくまで「表示する配列」を変えるだけにします。


フィルタ用のボタンをHTMLに追加する

モード切り替えのUIを用意する

index.html の「タスク一覧」の上あたりに、次のようなブロックを追加します。

<h2>表示モード</h2>
<div>
  <button id="show-all-button">すべて表示</button>
  <button id="show-undone-button">未完了だけ表示</button>
</div>

<h2>タスク一覧</h2>
<div id="task-list-area">
  まだタスクがありません。
</div>
HTML

これで、「すべて表示」「未完了だけ表示」の2つのボタンが用意できました。
JavaScript側でこれらをつかんで、表示モードを切り替えていきます。


表示モードを表す変数を用意する

“今どういう表示状態か”をコードで持つ

main.js に、次のような変数を追加します。

let currentFilter = "all";
HTML

これは、「今の表示モード」を表す変数です。

“all” なら「すべて表示」
“undone” なら「未完了だけ表示」

というルールにします。

この変数をもとに、「renderTasks がどのタスクを表示するか」を決めていきます。


renderTasks を「フィルタを考慮する」形に変える

tasks から「表示対象の配列」を作ってから描画する

今の renderTasks() は、tasks をそのまま全部表示していました。
これを、「まず表示対象の配列を作る → それをもとに HTML を作る」という形に変えます。

function renderTasks() {
  let targetTasks = [];

  if (currentFilter === "all") {
    targetTasks = tasks;
  } else if (currentFilter === "undone") {
    for (let i = 0; i < tasks.length; i = i + 1) {
      if (!tasks[i].done) {
        targetTasks.push(tasks[i]);
      }
    }
  }

  if (targetTasks.length === 0) {
    taskListAreaElement.textContent = "該当するタスクがありません。";
    return;
  }

  let html = "";

  for (let i = 0; i < targetTasks.length; i = i + 1) {
    let task = targetTasks[i];

    let mark = task.done ? "✔" : "・";
    let statusText = task.done ? "[完了]" : "[未完了]";

    html = html +
      '<div>' +
        mark + " " + statusText + " " + task.title +
      '</div>';
  }

  taskListAreaElement.innerHTML = html;
}
HTML

ここで一つ問題が出てきます。
「完了切替」「削除」のボタンを付けるときに、
data-index に何を入れるか、という問題です。

targetTasks は tasks の一部だけなので、
targetTasks のインデックスと tasks のインデックスがズレる可能性があります。

なので、「フィルタをかける前のインデックス(tasks のインデックス)」を
ボタンに埋め込む必要があります。


フィルタ後でも正しいインデックスを持たせる

targetTasks に「元のインデックス」も一緒に持たせる

やり方はいくつかありますが、
ここでは「一時的な配列に { task, index } という形で入れる」方法をとります。

function renderTasks() {
  let targetItems = [];

  if (currentFilter === "all") {
    for (let i = 0; i < tasks.length; i = i + 1) {
      targetItems.push({
        task: tasks[i],
        index: i
      });
    }
  } else if (currentFilter === "undone") {
    for (let i = 0; i < tasks.length; i = i + 1) {
      if (!tasks[i].done) {
        targetItems.push({
          task: tasks[i],
          index: i
        });
      }
    }
  }

  if (targetItems.length === 0) {
    taskListAreaElement.textContent = "該当するタスクがありません。";
    return;
  }

  let html = "";

  for (let i = 0; i < targetItems.length; i = i + 1) {
    let item = targetItems[i];
    let task = item.task;
    let index = item.index;

    let mark = task.done ? "✔" : "・";
    let statusText = task.done ? "[完了]" : "[未完了]";

    html = html +
      '<div>' +
        mark + " " + statusText + " " + task.title +
        ' <button data-index="' + index + '" data-action="toggle">完了切替</button>' +
        ' <button data-index="' + index + '" data-action="delete">削除</button>' +
      '</div>';
  }

  taskListAreaElement.innerHTML = html;
}
HTML

ここでの重要ポイントを整理します。

targetItems は、「表示対象のタスク」と「元のインデックス」をセットにした配列です。
{ task: tasks[i], index: i } という形で入れています。

for ループで HTML を作るとき、
item.task からタイトルや done を取り出し、
item.index を data-index に埋め込んでいます。

これで、フィルタ後でも「どのボタンが tasks の何番目か」が正しく分かるようになります。


表示モード切り替えボタンのイベントを書く

currentFilter を変えてから renderTasks を呼ぶ

main.js に、ボタン要素をつかむコードを追加します。

let showAllButtonElement = document.getElementById("show-all-button");
let showUndoneButtonElement = document.getElementById("show-undone-button");
HTML

そして、イベントを登録します。

showAllButtonElement.addEventListener("click", function () {
  currentFilter = "all";
  renderTasks();
});

showUndoneButtonElement.addEventListener("click", function () {
  currentFilter = "undone";
  renderTasks();
});
HTML

これで、「すべて表示」ボタンを押すと currentFilter が “all” になり、
「未完了だけ表示」ボタンを押すと “undone” になります。

renderTasks は currentFilter を見て、
targetItems を作り直してから画面を描きます。

ここでの大事な感覚は、「表示モードも“状態”の一部」ということです。
tasks だけでなく、currentFilter もアプリの状態です。


完了タスクを一括削除する機能を考える

「完了しているものだけ全部消す」

次は、「完了したタスクを一気に消したい」という機能です。
やりたいことはシンプルです。

tasks の中から、done が true のものを全部取り除く。
そのあと renderTasks() を呼ぶ。

これも、「配列を加工する」パターンの一つです。


一括削除ボタンをHTMLに追加する

UIを足してから、処理を考える

index.html の「表示モード」や「タスク一覧」の近くに、次を追加します。

<h2>その他の操作</h2>
<div>
  <button id="clear-done-button">完了タスクを一括削除</button>
</div>
HTML

JavaScript側で要素をつかみます。

let clearDoneButtonElement = document.getElementById("clear-done-button");
JavaScript

完了タスク一括削除の処理を書く

新しい配列を作り直すイメージで

clearDoneButtonElement にイベントを登録します。

clearDoneButtonElement.addEventListener("click", function () {
  let newTasks = [];

  for (let i = 0; i < tasks.length; i = i + 1) {
    if (!tasks[i].done) {
      newTasks.push(tasks[i]);
    }
  }

  tasks = newTasks;

  renderTasks();
});
JavaScript

ここでの重要ポイントを深掘りします。

newTasks という空の配列を用意しています。
for ループで tasks を1件ずつ見ていき、
done が false(未完了)のものだけ newTasks に入れています。

ループが終わったら、tasks = newTasks; として、
元の tasks を「未完了だけの配列」に置き換えています。

そのあと renderTasks() を呼んで、画面を更新しています。

ここでの考え方は、「配列を直接削っていく」というより、
「条件に合うものだけを集めた新しい配列を作る」というイメージです。
この方が、バグが入りにくく、コードも読みやすくなります。


タスク数(未完了・完了)を表示する

「今どれくらい残っているか」が分かると一気にアプリっぽくなる

次は、「タスクが何件あるか」「未完了が何件か」を表示してみます。
これはユーザーにとっても分かりやすいし、
配列を数える練習としても良い題材です。


カウンター表示用のHTMLを追加する

数字を表示する場所を用意する

index.html に、次のようなブロックを足します。

<h2>タスク数</h2>
<div id="task-count-area">
  合計: 0件 / 未完了: 0件 / 完了: 0件
</div>
HTML

JavaScript側で要素をつかみます。

let taskCountAreaElement = document.getElementById("task-count-area");
JavaScript

カウンターを更新する関数を作る

tasks 配列から数字を計算する

main.js に、次の関数を追加します。

function updateTaskCount() {
  let total = tasks.length;
  let undoneCount = 0;

  for (let i = 0; i < tasks.length; i = i + 1) {
    if (!tasks[i].done) {
      undoneCount = undoneCount + 1;
    }
  }

  let doneCount = total - undoneCount;

  let text = "合計: " + total + "件 / 未完了: " + undoneCount + "件 / 完了: " + doneCount + "件";

  taskCountAreaElement.textContent = text;
}
JavaScript

ここでの重要ポイントを整理します。

total は tasks.length で簡単に求まります。
未完了の数は、ループで done が false のものを数えています。
完了の数は、「合計 − 未完了」で求めています。

最後に、1つの文字列にまとめて textContent に入れています。


renderTasks と一緒に updateTaskCount も呼ぶ

「表示」と「カウント」を常に同期させる

タスクの状態が変わるのは、次のタイミングです。

タスクを追加したとき
完了切替したとき
削除したとき
完了タスクを一括削除したとき

これらのタイミングで、renderTasks だけでなく updateTaskCount も呼ぶようにします。

一番シンプルなのは、「renderTasks の最後で updateTaskCount を呼ぶ」ことです。

function renderTasks() {
  // ...(さっきまでの処理)

  taskListAreaElement.innerHTML = html;

  updateTaskCount();
}
JavaScript

こうしておけば、「画面を描き直すたびにカウンターも更新される」状態になります。
タスクの追加・削除・完了切替など、どの操作をしても、
最終的に renderTasks が呼ばれるようにしておけば、
カウンターのことを個別に気にしなくてよくなります。


タスクをブラウザに保存する(localStorage の入口)

ページをリロードしてもタスクが消えないようにする

ここまでで、かなり“使える”タスクアプリになりました。
でも、ページをリロードすると tasks 配列は初期化されてしまいます。

そこで登場するのが localStorage です。
これは「ブラウザの中に小さな保存箱を持てる仕組み」です。


localStorage の基本イメージ

キーと値で文字列を保存する

localStorage は、ざっくり言うと「キーと値のセットを保存する場所」です。

localStorage.setItem("キー", "値"); で保存。
localStorage.getItem("キー"); で取り出し。

ただし、保存できるのは「文字列」だけです。
なので、配列やオブジェクトを保存したいときは、
一度 JSON という形式の文字列に変換します。


tasks を保存する関数と、読み込む関数を作る

JSON.stringify と JSON.parse を使う

main.js に、次の2つの関数を追加します。

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

function loadTasks() {
  let json = localStorage.getItem("tasks-data");

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

  tasks = JSON.parse(json);
}
JavaScript

ここでの重要ポイントを深掘りします。

JSON.stringify(tasks) は、「tasks 配列を JSON 形式の文字列に変換する」処理です。
それを localStorage.setItem で “tasks-data” というキーで保存しています。

loadTasks では、getItem で文字列を取り出しています。
何も保存されていない場合は null が返ってくるので、そのときは tasks を空配列にしています。

json がある場合は、JSON.parse(json) で「文字列 → 配列」に戻しています。
これで、前回保存した tasks を復元できます。


状態が変わるたびに saveTasks を呼ぶ

「配列が変わったら保存する」を習慣にする

タスクが変わるタイミングは、さっきと同じです。

追加
完了切替
削除
一括削除

これらの処理の中で、renderTasks のあとに saveTasks を呼ぶようにします。

例えば、追加のところはこうなります。

tasks.push(task);

taskInputElement.value = "";

renderTasks();
saveTasks();
JavaScript

完了切替や削除、一括削除の処理の最後にも、
renderTasks(); のあとに saveTasks(); を足していきます。


初期表示のときに loadTasks → renderTasks を呼ぶ

ページを開いた瞬間に前回の状態を復元する

main.js の一番最後を、次のようにします。

loadTasks();
renderTasks();
JavaScript

これで、ページを開いたときに、

localStorage から tasks を読み込む
その tasks をもとに画面を描く

という流れになります。

もし初めて開いたときは、localStorage に何もないので、
tasks は空配列になり、「まだタスクがありません。」と表示されます。


9日目で一番大事な感覚

「状態は配列だけじゃない」「状態は保存もできる」

今日あなたが手に入れた感覚は、かなり本格的です。

表示モード(currentFilter)もアプリの状態である。
状態(tasks, currentFilter)から「表示用の配列」を作って画面を描く。
配列を直接いじるだけでなく、「新しい配列を作り直す」という発想を持てた。
状態をブラウザに保存して、次回起動時に復元できるようになった。

ここまで来ると、「小さなWebアプリ」の基本的な構造はほぼ一通り体験したことになります。


9日目のまとめ

今日のキーポイントを短く整理すると、こうなります。

表示モードを表す currentFilter を導入し、「すべて表示」「未完了だけ表示」を切り替えられるようにした。
renderTasks の中で、「表示対象の配列(targetItems)」を作ってから描画する形にした。
完了タスク一括削除で、「条件に合うものだけを集めた新しい配列を作る」パターンを体験した。
タスク数(合計・未完了・完了)を数えて、画面に表示するカウンターを作った。
localStorage と JSON.stringify / JSON.parse を使って、tasks を保存・復元できるようにした。

次の10日目では、このタスクアプリや名簿アプリで身につけたパターンを振り返りつつ、
「コードを少し整理する」「関数を分けて読みやすくする」といった“リファクタリングの入口”に触れていきます。

もし余裕があれば、
今日のアプリに「完了タスクをグレー表示にする(文字列で工夫)」「検索機能を足す」など、
自分なりの“もう一歩”を足してみてください。
その「もうちょっと良くしたい」という気持ちが、エンジニアとしての感性を一番育ててくれます。

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