基礎から学ぶJavaScript入門 90日コース | ブラウザデータ保存 - Day 55:中間総合制作

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

Day 55:中間総合制作

Day 55では、第4フェーズ「ブラウザデータ保存」の中間総合制作として、ToDo管理アプリ Ver.3 を作成します。今回の完成機能は、ToDo追加、編集、削除、完了、検索、フィルター、localStorage 保存です。ここまでの学習で、すでにかなり多くの要素を1つずつ積み上げてきました。Day 45ではDOM総合制作としてToDoアプリ Ver.1 を作り、Day 46からDay 50にかけて localStorage、JSON、CRUD、保存対応、完了状態保存を学びました。さらにDay 51からDay 54では、検索機能、フィルター機能、ソート機能、UI改善を学んできました。Day 55は、それらを「別々の練習」としてではなく、「1つの完成アプリ」として統合する回です。

初心者の方にとって、このDay 55はとても重要な節目です。なぜなら、ここで学ぶのは新しい1個の命令というより、「どうやって複数の機能を1つの設計にまとめるか」だからです。機能が増えるほど、ただコードを書き足していくだけでは整理しにくくなります。そのため、どのデータを本体として持ち、どう画面へ表示し、どの操作でデータを変え、いつ保存し、どう絞り込むかを、流れとして整理する力が必要になります。つまり、Day 55は「作る力」だけでなく「まとめる力」を育てる回でもあります。

今回は、プログラミング初心者向けに、いきなり大きな完成コードだけを見せるのではなく、まず全体像を整理し、そのあと機能ごとに考え方を分解し、最後に統合版のテンプレートへつなげていきます。大切なところ、特に「データ本体と画面表示の関係」「検索とフィルターの組み合わせ」「編集や削除の実装で気をつける点」などは、より深く説明していきます。ここまで学んだ内容を本当に自分の力にするための回だと思って読み進めると理解しやすいです。

ToDo管理アプリ Ver.3 とは何か

ToDo管理アプリ Ver.3 は、これまで学んだ内容をまとめて搭載した中間総合制作アプリです。Ver.1 はDOM操作中心の基本形であり、Ver.2 は保存対応版でした。Ver.3 では、そこに編集、検索、フィルターを加え、より「管理アプリらしい」形へ発展させます。

今回の完成機能を整理すると、次の通りです。

  • ToDo追加:新しいToDoを作る
  • 編集:登録済みToDoの内容を直す
  • 削除:不要なToDoを消す
  • 完了:終わったToDoの状態を切り替える
  • 検索:キーワードに合うToDoを探す
  • フィルター:すべて、未完了、完了済みを切り替える
  • `localStorage` 保存:ページを更新しても状態を維持する

初心者の方は、この一覧を見ると多く感じるかもしれません。しかし、実際にはすべてがバラバラの機能ではありません。多くの処理が、同じデータ配列を操作し、それを保存し、再表示する、という共通の流れでつながっています。そこを見抜けるようになることがDay 55の大きな目標です。

今回の制作で一番大切な考え方

今回の制作で一番大切なのは、「アプリの本体は画面ではなくデータである」という考え方です。これは第4フェーズを通して何度も出てきた考え方ですが、Ver.3 では特に重要です。なぜなら、追加、編集、削除、完了、検索、フィルターのすべてが、まずデータ本体に対して行われるからです。

たとえば、ToDoを編集したいとき、画面に見えている文字を直接書き換えるだけでは不十分です。本当に変えるべきなのは、配列の中にあるToDoデータです。そして、データを変えたあとに、その結果として画面を再表示し、さらに localStorage に保存し直す必要があります。

この流れを言葉にすると、次のようになります。

  • データを変更する
  • 保存する
  • 画面を更新する

初心者の方は、どの機能でもこの流れが中心にあると意識すると、コード全体がかなり整理しやすくなります。

まずはToDoデータの形を決める

複数機能をまとめるには、最初にデータの形をしっかり決めることが大切です。今回のToDoアプリ Ver.3 では、次のような形がわかりやすいです。

const todo = {
  text: "JavaScriptを勉強する",
  completed: false
};
JavaScript

これが複数集まると、全体は配列になります。

const todos = [
  { text: "牛乳を買う", completed: false },
  { text: "JavaScriptを勉強する", completed: true }
];
JavaScript

今回は編集、削除、完了、検索、フィルターまですべてこの配列に対して行います。初心者の方は、「アプリの中心は todos 配列である」と最初にはっきり理解しておくと、そのあとがかなりわかりやすくなります。

全体の流れを先に整理する

いきなりコードを書く前に、アプリ全体の流れを日本語で整理しておきます。これが非常に大切です。

  • ページが開いたら `localStorage` から保存済みデータを取り出す
  • 保存データがあれば配列に戻す
  • 現在の検索条件とフィルター条件に合わせて表示する
  • 追加ボタンを押したら新しいToDoを配列へ追加する
  • 編集ボタンを押したら対象のToDo内容を変更する
  • 削除ボタンを押したら対象のToDoを配列から削除する
  • 完了ボタンを押したら `completed` を切り替える
  • どの変更でも保存し直して画面を更新する
  • 検索欄やフィルターボタンが変わったら、表示する一覧を切り替える

この流れが見えていると、機能が多くても混乱しにくくなります。初心者の方は、大きな制作ほど「まず日本語で設計する」習慣を大事にするとよいです。

必要なHTMLを考える

今回必要になるHTMLの部品を整理すると、次のようになります。

  • ToDo入力欄
  • 追加ボタン
  • 検索欄
  • フィルターボタン3つ
  • エラーメッセージ表示欄
  • ToDo一覧表示エリア

まずは基本形のHTMLを見てみます。

<h2>ToDo管理アプリ Ver.3</h2>

<input type="text" id="todoInput" placeholder="ToDoを入力してください">
<button id="addButton">追加</button>

<p id="errorMessage"></p>

<input type="text" id="searchInput" placeholder="検索キーワードを入力してください">

<button id="allButton">すべて</button>
<button id="incompleteButton">未完了</button>
<button id="completedButton">完了済み</button>

<ul id="todoList"></ul>

初心者の方は、アプリに必要な部品を先に並べてからJavaScriptに入ると、何を取得し、何を操作するのかが見えやすくなります。

状態管理のための変数を用意する

Ver.3 では、ToDo配列だけでなく、「今どんな検索条件か」「今どんなフィルターか」も管理すると便利です。そのため、次のような変数を用意します。

let todos = [];
let currentFilter = "all";
let currentSearch = "";
JavaScript

ここでの意味は次の通りです。

  • `todos` はToDoデータ本体です。
  • `currentFilter` は今の表示モードです。
  • `currentSearch` は今の検索キーワードです。

初心者の方は、これを「画面の見せ方を決めるための状態」と理解するとよいです。ToDoデータだけでなく、アプリの現在の条件も変数として持っておくと整理しやすくなります。

保存と読み込みを最初に固める

まずは localStorage の読み込みと保存を関数化します。ここはアプリ全体の土台になります。

// localStorage から保存データを取得する
const savedTodos = localStorage.getItem("todos");

// 保存データがあれば配列に戻し、なければ空配列を使う
todos = savedTodos !== null ? JSON.parse(savedTodos) : [];

// ToDo 配列を保存する関数
function saveTodos() {
  localStorage.setItem("todos", JSON.stringify(todos));
}
JavaScript

これで、ページ再読み込み後もデータを維持できる準備ができます。初心者の方は、保存対応アプリでは「最初に読み込む」「変化のたびに保存する」の2つが必ず必要だと覚えるとよいです。

表示関数をアプリの中心にする

今回のアプリでは、表示関数がとても重要です。なぜなら、検索やフィルターを考慮して、今見せるべきToDo一覧を毎回作り直す必要があるからです。

function renderTodos() {
  // いったん表示を空にする
  todoList.innerHTML = "";

  // まず検索条件で絞り込む
  let filteredTodos = todos.filter(function (todo) {
    return todo.text.includes(currentSearch);
  });

  // 次にフィルター条件で絞り込む
  if (currentFilter === "incomplete") {
    filteredTodos = filteredTodos.filter(function (todo) {
      return todo.completed === false;
    });
  } else if (currentFilter === "completed") {
    filteredTodos = filteredTodos.filter(function (todo) {
      return todo.completed === true;
    });
  }

  // データが空ならメッセージを表示する
  if (filteredTodos.length === 0) {
    const li = document.createElement("li");
    li.textContent = "表示するToDoがありません。";
    todoList.appendChild(li);
    return;
  }

  // 条件に合う ToDo を順番に表示する
  filteredTodos.forEach(function (todo) {
    const li = document.createElement("li");
    li.textContent = todo.text;
    todoList.appendChild(li);
  });
}
JavaScript

この段階ではまだ編集・削除・完了ボタンは入れていませんが、「検索とフィルターを合わせて表示する」という中心構造が見えます。初心者の方は、表示関数が単なる描画ではなく、「今の条件に合わせた一覧を作る場所」だと理解すると重要性が見えてきます。

検索とフィルターを組み合わせる考え方

ここはかなり大切なので深掘りします。検索とフィルターは別々の機能ですが、Ver.3 では同時に使えるほうが便利です。たとえば、「未完了の中から『買い物』を含むToDoだけ見たい」ということがあります。そのためには、検索とフィルターを別々に処理するのではなく、両方を順番に適用する必要があります。

流れは次の通りです。

  • まず検索キーワードで絞り込む
  • その結果に対してフィルター条件を適用する

この順番でも逆順でも最終結果は同じことが多いですが、初心者向けには「まず文字条件、そのあと状態条件」と考えるとわかりやすいです。ここを理解できると、複数条件の絞り込みという考え方が身につきます。

追加機能を作る

追加機能は、これまでの学習内容をそのまま使えます。入力欄から文字を取り出し、空欄ならエラー表示し、問題なければ配列へ追加して保存・再表示します。

addButton.addEventListener("click", function () {
  // エラーメッセージを初期化する
  errorMessage.textContent = "";

  // 入力値を取得する
  const text = todoInput.value.trim();

  // 空欄ならエラー表示する
  if (text === "") {
    errorMessage.textContent = "ToDo内容を入力してください。";
    return;
  }

  // 新しい ToDo を追加する
  todos.push({
    text: text,
    completed: false
  });

  // 保存する
  saveTodos();

  // 再表示する
  renderTodos();

  // 入力欄を空にする
  todoInput.value = "";
});
JavaScript

初心者の方は、ここでも「配列変更 → 保存 → 再表示」の流れが守られていることを確認するとよいです。

完了機能を作る

完了機能では、対象のToDoの completed を切り替えます。ここで重要なのは、表示中の順番ではなく、元の todos 配列のどの要素かを正しく扱うことです。初心者向けには、まず forEach の2番目の引数 index を使い、元配列をそのまま表示する前提で学ぶのがわかりやすいです。

// 完了状態を切り替える
todos[index].completed = !todos[index].completed;

// 保存する
saveTodos();

// 再表示する
renderTodos();
JavaScript

ただし、検索やフィルターで表示中の順番が変わる場合、この index の扱いは少し注意が必要です。初心者向けの安全策としては、各ToDoに id を持たせて識別する方法があります。本格的にはその方法がより良いですが、今回は学習の流れを優先して、まず index の仕組みを理解することを重視します。

編集機能を作る

編集機能は、初心者の方にとって少しだけ難易度が上がります。なぜなら、「対象を選ぶ」「新しい内容を受け取る」「空欄を防ぐ」「保存し直す」という複数の処理が入るからです。最初の段階では、prompt() を使った簡易編集がわかりやすいです。

// 編集ボタンが押されたときの処理
const newText = prompt("新しいToDo内容を入力してください", todos[index].text);

// キャンセルされたら何もしない
if (newText === null) {
  return;
}

// 前後の空白を除く
const trimmedText = newText.trim();

// 空欄なら何もしない
if (trimmedText === "") {
  return;
}

// ToDo内容を更新する
todos[index].text = trimmedText;

// 保存する
saveTodos();

// 再表示する
renderTodos();
JavaScript

初心者の方は、編集も本質的にはUpdateであり、「データを書き換えて保存し直す」処理だと理解すると整理しやすいです。

削除機能を作る

削除機能は、対象のToDoを配列から取り除く処理です。ここでは splice() を使います。

// 配列から削除する
todos.splice(index, 1);

// 保存する
saveTodos();

// 再表示する
renderTodos();
JavaScript

削除は初心者の方にもわかりやすい機能ですが、大事なのはやはり「データ本体を消すこと」と「画面から見えなくすること」は別であり、両方を正しく行う必要があるという点です。

検索機能を作る

検索機能は、検索欄の入力値を currentSearch に入れて、renderTodos() を呼び直すだけで実現できます。つまり、検索処理そのものをボタンの中に書くのではなく、「今の検索条件を更新する」だけにすると整理しやすいです。

searchInput.addEventListener("input", function () {
  currentSearch = searchInput.value.trim();
  renderTodos();
});
JavaScript

この形だと、入力されるたびにリアルタイム検索になります。初心者の方は、「表示関数に全部の条件処理をまとめているので、イベント側は条件更新だけでよい」と理解できると設計力がぐっと上がります。

フィルター機能を作る

フィルター機能も同じ考え方です。各ボタンが currentFilter を更新して renderTodos() を呼びます。

allButton.addEventListener("click", function () {
  currentFilter = "all";
  renderTodos();
});

incompleteButton.addEventListener("click", function () {
  currentFilter = "incomplete";
  renderTodos();
});

completedButton.addEventListener("click", function () {
  currentFilter = "completed";
  renderTodos();
});
JavaScript

この設計にしておくと、検索条件とフィルター条件が自然に組み合わさります。初心者の方は、「表示のルールを変数で持つ」という考え方がここでとても大切だと感じられるはずです。

完成版コード

ここまでの内容をまとめた、ToDo管理アプリ Ver.3 の完成版テンプレートを載せます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Day 55 ToDo管理アプリ Ver.3</title>
  <style>
    /* 完了済みの見た目 */
    .completed {
      text-decoration: line-through;
      color: gray;
    }

    /* エラーメッセージの見た目 */
    .error {
      color: red;
    }

    /* リスト項目の余白 */
    li {
      margin-bottom: 10px;
    }

    /* ボタンの余白 */
    button {
      margin-left: 6px;
    }
  </style>
</head>
<body>
  <h2>ToDo管理アプリ Ver.3</h2>

  <input type="text" id="todoInput" placeholder="ToDoを入力してください">
  <button id="addButton">追加</button>

  <p id="errorMessage" class="error"></p>

  <input type="text" id="searchInput" placeholder="検索キーワードを入力してください">

  <br><br>

  <button id="allButton">すべて</button>
  <button id="incompleteButton">未完了</button>
  <button id="completedButton">完了済み</button>

  <ul id="todoList"></ul>

  <script>
    // 必要な要素を取得する
    const todoInput = document.getElementById("todoInput");
    const addButton = document.getElementById("addButton");
    const errorMessage = document.getElementById("errorMessage");
    const searchInput = document.getElementById("searchInput");
    const allButton = document.getElementById("allButton");
    const incompleteButton = document.getElementById("incompleteButton");
    const completedButton = document.getElementById("completedButton");
    const todoList = document.getElementById("todoList");

    // localStorage から保存データを取得する
    const savedTodos = localStorage.getItem("todos");

    // 保存データがあれば配列に戻し、なければ空配列を使う
    let todos = savedTodos !== null ? JSON.parse(savedTodos) : [];

    // 現在の検索条件とフィルター条件
    let currentSearch = "";
    let currentFilter = "all";

    // ToDo 配列を保存する関数
    function saveTodos() {
      localStorage.setItem("todos", JSON.stringify(todos));
    }

    // ToDo を画面に表示する関数
    function renderTodos() {
      // 表示を空にする
      todoList.innerHTML = "";

      // まず検索条件で絞り込む
      let filteredTodos = todos.filter(function (todo) {
        return todo.text.includes(currentSearch);
      });

      // 次にフィルター条件を適用する
      if (currentFilter === "incomplete") {
        filteredTodos = filteredTodos.filter(function (todo) {
          return todo.completed === false;
        });
      } else if (currentFilter === "completed") {
        filteredTodos = filteredTodos.filter(function (todo) {
          return todo.completed === true;
        });
      }

      // 表示対象がないとき
      if (filteredTodos.length === 0) {
        const li = document.createElement("li");
        li.textContent = "表示するToDoがありません。";
        todoList.appendChild(li);
        return;
      }

      // 条件に合う ToDo を順番に表示する
      filteredTodos.forEach(function (todo) {
        // 元配列での index を探す
        const index = todos.indexOf(todo);

        // li を作成する
        const li = document.createElement("li");

        // ToDo 内容を表示する span を作る
        const span = document.createElement("span");
        span.textContent = todo.text;

        // 完了済みなら見た目を変える
        if (todo.completed) {
          span.classList.add("completed");
        }

        // 完了ボタンを作る
        const completeButton = document.createElement("button");
        completeButton.textContent = "完了";

        // 編集ボタンを作る
        const editButton = document.createElement("button");
        editButton.textContent = "編集";

        // 削除ボタンを作る
        const deleteButton = document.createElement("button");
        deleteButton.textContent = "削除";

        // 完了ボタンの処理
        completeButton.addEventListener("click", function () {
          todos[index].completed = !todos[index].completed;
          saveTodos();
          renderTodos();
        });

        // 編集ボタンの処理
        editButton.addEventListener("click", function () {
          const newText = prompt("新しいToDo内容を入力してください", todos[index].text);

          // キャンセルされたら何もしない
          if (newText === null) {
            return;
          }

          const trimmedText = newText.trim();

          // 空欄なら何もしない
          if (trimmedText === "") {
            return;
          }

          todos[index].text = trimmedText;
          saveTodos();
          renderTodos();
        });

        // 削除ボタンの処理
        deleteButton.addEventListener("click", function () {
          todos.splice(index, 1);
          saveTodos();
          renderTodos();
        });

        // li に各要素を追加する
        li.appendChild(span);
        li.appendChild(completeButton);
        li.appendChild(editButton);
        li.appendChild(deleteButton);

        // リストに追加する
        todoList.appendChild(li);
      });
    }

    // 追加ボタンの処理
    addButton.addEventListener("click", function () {
      // エラーメッセージを初期化する
      errorMessage.textContent = "";

      // 入力値を取得する
      const text = todoInput.value.trim();

      // 空欄ならエラー表示
      if (text === "") {
        errorMessage.textContent = "ToDo内容を入力してください。";
        return;
      }

      // 新しい ToDo を追加する
      todos.push({
        text: text,
        completed: false
      });

      // 保存・再表示
      saveTodos();
      renderTodos();

      // 入力欄を空にする
      todoInput.value = "";
    });

    // 検索欄の入力処理
    searchInput.addEventListener("input", function () {
      currentSearch = searchInput.value.trim();
      renderTodos();
    });

    // フィルターボタンの処理
    allButton.addEventListener("click", function () {
      currentFilter = "all";
      renderTodos();
    });

    incompleteButton.addEventListener("click", function () {
      currentFilter = "incomplete";
      renderTodos();
    });

    completedButton.addEventListener("click", function () {
      currentFilter = "completed";
      renderTodos();
    });

    // 初期表示
    renderTodos();
  </script>
</body>
</html>

この完成版コードの見方

初心者の方は、完成版コードを一気に全部理解しようとすると大変です。そのため、次の順番で読むのがおすすめです。

  • 最初にHTMLを見て、どんな部品があるか確認する
  • 次に `todos`、`currentSearch`、`currentFilter` を見て、どんな状態を持っているか確認する
  • 次に `saveTodos()` と `renderTodos()` を見て、アプリの中心処理を理解する
  • そのあと追加、検索、フィルター、完了、編集、削除のイベント処理を順番に読む

このように分解して見れば、大きなコードも「小さな部品の集合」として理解しやすくなります。

特に深掘りしたい重要ポイント

今回特に大切なのは、表示関数に条件処理をまとめていることです。もし検索処理を検索イベントの中に全部書き、フィルター処理も別々に書き、完了後の再表示もまた別に書いてしまうと、条件が増えるほどコードがばらばらになります。しかし、renderTodos() に「今の検索条件とフィルター条件を適用して表示する」という役割を集めておけば、他の処理は「状態を変えて renderTodos を呼ぶ」だけで済みます。

これは初心者の方にとって非常に大切な設計の考え方です。複雑な機能ほど、処理の中心をどこに置くかでわかりやすさが大きく変わります。

初心者が特に注意したいポイント

Day 55では、次の点を特に意識すると理解が深まりやすいです。

  • アプリの本体は `todos` 配列であり、画面はその結果です。
  • 編集、削除、完了、追加はすべて「配列変更 → 保存 → 再表示」の流れです。
  • 検索とフィルターは元データを壊さず、表示対象だけを変えます。
  • 条件が複数あるときは、表示関数にまとめると整理しやすいです。
  • 空欄入力や空データ表示など、UI面の配慮も完成度に大きく関わります。

特に重要なのは、「今見えているリスト」を直接操作するのではなく、「元データを変えてから見た目を作り直す」という発想です。この考え方が身につくと、より本格的なアプリにも進みやすくなります。

情報セキュリティの観点での注意

今回のToDo管理アプリ Ver.3 は学習用として非常に良い題材ですが、localStorage に保存する内容には引き続き注意が必要です。ToDo内容には個人の予定や行動パターンが含まれることがあるため、機密性の高い情報や業務上の秘密事項をそのまま保存するのは避けるべきです。

また、編集機能や検索機能があることで、保存された内容にアクセスしやすくなります。初心者の方は、便利な機能を増やすほど「何を保存してよいか」をより慎重に考える必要があると意識しておくと、安全な開発習慣につながります。

Day 55のゴールを整理する

Day 55では、次のことができるようになっていれば十分です。

  • ToDo管理アプリ Ver.3 の全体構造を説明できる
  • ToDo追加、編集、削除、完了を実装できる
  • 検索とフィルターを組み合わせて表示できる
  • `localStorage` を使って状態を保存・復元できる
  • データ本体と画面表示の役割の違いを説明できる
  • 機能をばらばらに作るのではなく、1つの設計にまとめる考え方を理解している

ここまでできれば、Day 55としてはとても順調です。第4フェーズで学んだ内容を、中間総合制作としてしっかり1本のアプリに統合できています。

まとめ

Day 55では、第4フェーズ「ブラウザデータ保存」の中間総合制作として、ToDo管理アプリ Ver.3 を作成しました。今回の完成機能は、ToDo追加、編集、削除、完了、検索、フィルター、localStorage 保存であり、これまで学んできたDOM操作、データ保存、検索、フィルター、UI改善の考え方を1つのアプリとしてまとめ上げる内容でした。特に重要だったのは、アプリの本体は todos 配列であり、画面はその配列をもとに現在の検索条件やフィルター条件を適用して作り直している、という構造です。

また、追加、編集、削除、完了のどの機能でも、「データを変える」「保存する」「再表示する」という共通の流れがあることも大切なポイントでした。検索とフィルターについても、元データを壊すのではなく、見せ方だけを変えているという考え方が重要です。こうした共通構造に気づけるようになると、機能が多いアプリでも整理して考えられるようになります。

次に進む前に、今回の完成版を何も見ずにもう一度作り直してみるのがおすすめです。そのうえで、ソート機能を統合したり、件数表示を追加したり、編集時に入力フォームを画面上で直接使えるようにしたりすると、総合制作としての理解がさらに深まりやすくなります。

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