見出し画像

はじめてのJavaScript #33|ToDoリストを作ろう ── 追加・削除・完了チェック

今回のゴール

LocalStorageを使って、ページを閉じてもデータが残るToDoリストを完成させる。


今回作るものの仕様

  • テキストを入力して「追加」ボタンでタスクを追加する

  • タスクの左のチェックボックスで完了・未完了を切り替える

  • 完了したタスクは打ち消し線で表示する

  • 「削除」ボタンでタスクを1件ずつ削除できる

  • ページをリロードしてもデータが残る

考え方を整理しよう

ToDoリストで管理するデータはこのような構造にします。

js

// 1件分のタスク
{
  id:        1,        // 識別用のユニークな番号
  text:      "買い物", // タスクの内容
  completed: false     // 完了かどうか
}

// 複数のタスクを配列で管理
[
  { id: 1, text: "買い物",       completed: false },
  { id: 2, text: "洗濯",         completed: true  },
  { id: 3, text: "プログラミング", completed: false }
]

この配列をLocalStorageに保存・取得しながら画面を更新していきます。

実際に書いてみよう

index.html

html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>ToDoリスト</title>
    <style>
      body {
        font-family: sans-serif;
        max-width: 480px;
        margin: 40px auto;
        padding: 0 20px;
      }
      .todo-item {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px 12px;
        margin: 6px 0;
        background: #f9f8f5;
        border-radius: 8px;
        border: 0.5px solid #ddd;
      }
      .todo-item.completed .todo-text {
        text-decoration: line-through;
        color: #aaa;
      }
      .todo-text {
        flex: 1;
        font-size: 15px;
      }
      .btn-delete {
        background: none;
        border: none;
        color: #e74c3c;
        font-size: 18px;
        cursor: pointer;
        padding: 0 4px;
      }
      .input-area {
        display: flex;
        gap: 8px;
        margin-bottom: 16px;
      }
      .input-area input {
        flex: 1;
        padding: 8px 12px;
        font-size: 15px;
        border: 1px solid #ccc;
        border-radius: 6px;
      }
      .input-area button {
        padding: 8px 16px;
        font-size: 15px;
        background: royalblue;
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
      }
      .summary {
        font-size: 13px;
        color: #888;
        margin-top: 12px;
      }
      .btn-clear {
        margin-top: 12px;
        padding: 6px 14px;
        font-size: 13px;
        background: #e74c3c;
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <h1>ToDoリスト</h1>

    <div class="input-area">
      <input id="todoInput" type="text" placeholder="タスクを入力してください">
      <button id="btnAdd">追加</button>
    </div>

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

    <p class="summary" id="summary"></p>
    <button class="btn-clear" id="btnClear">完了済みをまとめて削除</button>

    <script src="script.js"></script>
  </body>
</html>

script.js

js

const todoInput = document.getElementById("todoInput");
const btnAdd    = document.getElementById("btnAdd");
const todoList  = document.getElementById("todoList");
const summary   = document.getElementById("summary");
const btnClear  = document.getElementById("btnClear");

// ============================================
// LocalStorage からデータを読み込む
// ============================================
function loadTodos() {
  const saved = localStorage.getItem("todos");
  return saved ? JSON.parse(saved) : [];
}

// ============================================
// LocalStorage にデータを保存する
// ============================================
function saveTodos(todos) {
  localStorage.setItem("todos", JSON.stringify(todos));
}

// ============================================
// サマリーを更新する
// ============================================
function updateSummary(todos) {
  const total     = todos.length;
  const completed = todos.filter(function(t) { return t.completed; }).length;
  summary.textContent = "全" + total + "件中 " + completed + "件完了";
}

// ============================================
// リストを画面に描画する
// ============================================
function renderTodos() {
  const todos = loadTodos();
  todoList.innerHTML = "";

  if (todos.length === 0) {
    const li = document.createElement("li");
    li.textContent  = "タスクがありません。追加してみましょう!";
    li.style.color  = "#aaa";
    li.style.padding = "10px 0";
    todoList.appendChild(li);
    updateSummary(todos);
    return;
  }

  todos.forEach(function(todo) {
    const li = document.createElement("li");
    li.className = "todo-item" + (todo.completed ? " completed" : "");

    // チェックボックス
    const checkbox    = document.createElement("input");
    checkbox.type     = "checkbox";
    checkbox.checked  = todo.completed;
    checkbox.addEventListener("change", function() {
      toggleTodo(todo.id);
    });

    // タスクのテキスト
    const span       = document.createElement("span");
    span.className   = "todo-text";
    span.textContent = todo.text;

    // 削除ボタン
    const btnDelete       = document.createElement("button");
    btnDelete.className   = "btn-delete";
    btnDelete.textContent = "✕";
    btnDelete.addEventListener("click", function() {
      deleteTodo(todo.id);
    });

    li.appendChild(checkbox);
    li.appendChild(span);
    li.appendChild(btnDelete);
    todoList.appendChild(li);
  });

  updateSummary(todos);
}

// ============================================
// タスクを追加する
// ============================================
function addTodo() {
  const text = todoInput.value.trim();
  if (!text) {
    todoInput.focus();
    return;
  }

  const todos  = loadTodos();
  const newTodo = {
    id:        Date.now(), // 現在時刻をIDとして使う
    text:      text,
    completed: false
  };

  todos.push(newTodo);
  saveTodos(todos);
  todoInput.value = "";
  renderTodos();
}

// ============================================
// 完了・未完了を切り替える
// ============================================
function toggleTodo(id) {
  const todos = loadTodos();
  const todo  = todos.find(function(t) { return t.id === id; });
  if (todo) todo.completed = !todo.completed;
  saveTodos(todos);
  renderTodos();
}

// ============================================
// タスクを1件削除する
// ============================================
function deleteTodo(id) {
  const todos    = loadTodos();
  const filtered = todos.filter(function(t) { return t.id !== id; });
  saveTodos(filtered);
  renderTodos();
}

// ============================================
// 完了済みをまとめて削除する
// ============================================
btnClear.addEventListener("click", function() {
  const todos      = loadTodos();
  const incomplete = todos.filter(function(t) { return !t.completed; });
  if (todos.length === incomplete.length) {
    alert("完了済みのタスクはありません。");
    return;
  }
  if (!confirm("完了済みのタスクをすべて削除しますか?")) return;
  saveTodos(incomplete);
  renderTodos();
});

// ============================================
// タスクを追加するイベント
// ============================================
btnAdd.addEventListener("click", function() {
  addTodo();
});

todoInput.addEventListener("keydown", function(e) {
  if (e.key === "Enter") addTodo();
});

// 初期描画
renderTodos();

コードのポイント解説

Date.now() でユニークなIDを作る

js

id: Date.now()

Date.now() は現在時刻をミリ秒単位の数値で返します。1ミリ秒ごとに異なる値になるので、タスクのユニークなIDとして使えます。タスクの特定・削除・更新はすべてこのIDを使って行います。

find() で特定のタスクを取得する

js

const todo = todos.find(function(t) { return t.id === id; });

find() は条件に合う最初の要素を返す配列メソッドです。IDが一致するタスクをピンポイントで取得できます。

renderTodos() で常に再描画する

データを変更するたびに renderTodos() を呼び出して画面全体を再描画しています。「データを更新 → 保存 → 再描画」というパターンで統一することで、画面とデータが常に一致した状態を保てます。

まとめ

  • タスクはオブジェクトの配列として管理し、JSON.stringify() でLocalStorageに保存する

  • Date.now() でユニークなIDを生成してタスクの特定・削除・更新に使う

  • 「データを更新 → 保存 → 再描画」のパターンで統一するとデータと画面がずれにくい

次回予告

次回はToDoリストをさらに発展させて、入力した内容が自動保存されるメモ帳アプリを作ります。

→ #34|メモ帳アプリを作ろう ── 自動保存機能を実装する


いいなと思ったら応援しよう!

DOI@日々学びの本質を考え続ける探求者 よろしければ応援お願いします! いただいたチップは引き続きプログラミングや学びについて、皆さんの利益になるようなよい記事を書くことで恩返しをさせていただきます!