tanaka101

完了/未完了を切り替える

チェックボックスでタスクの完了状態を切り替える機能を実装します。

このレッスンで作るもの

チェックボックスをクリックすると、タスクの完了/未完了が切り替わる機能を作ります。完了したタスクには打ち消し線が表示されます。

このレッスンのゴール

完了機能を作るために必要なこと

  1. タスクに完了状態を追加 - completed プロパティで管理
  2. チェックボックスのイベント処理 - クリック時に状態を切り替える
  3. 完了時のスタイル - 打ち消し線を表示する

Step 1: タスクに完了状態を追加する

タスク追加時に completed: false を設定します。

main.js
form.addEventListener('submit', (e) => {
  e.preventDefault();
 
  const text = input.value.trim();
 
  if (!text) {
    return;
  }
 
  // completedプロパティを追加
  todos.push({ id: nextId, text, completed: false });
  nextId++;
 
  renderTodos();
  input.value = '';
});

これでタスクのデータ構造が以下のようになります。

{ id: 1, text: "買い物に行く", completed: false }

プロパティ名は自由

プロパティ名は自由ですが、後から見て意味が分かる名前をつけることが大切です。

名前だけでは不安なときは、コメントで補足するのも良い習慣です。

/** タスクが完了済みかどうか */
completed: false

Step 2: 切り替えを作成する

指定したIDのタスクの完了状態を反転させるを作ります。

main.js
// 完了状態を切り替える関数
function toggleComplete(id) {
  todos = todos.map((todo) => {
    if (todo.id === id) {
      return { ...todo, completed: !todo.completed };
    }
    return todo;
  });
  renderTodos();
}

map() について

map() は配列の各要素を変換して、新しい配列を作るです。

map() は各要素に対してを実行し、その戻り値を集めた新しい配列を返します。

const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
// doubled は [2, 4, 6]

今回は、IDが一致するタスクだけ completed を反転させています。

スプレッド構文について

{ ...todo, completed: !todo.completed }... はスプレッド構文です。

スプレッド構文は、オブジェクトの中身を展開します。

const todo = { id: 1, text: "買い物", completed: false };
 
// todoの中身を展開して、completedだけ上書き
const updated = { ...todo, completed: true };
// { id: 1, text: "買い物", completed: true }

元のオブジェクトを変更せず、新しいオブジェクトを作成できます。

Step 3: チェックボックスにイベントを設定する

renderTodos() を修正します。

main.js
function renderTodos() {
  list.innerHTML = '';
 
  todos.forEach((todo) => {
    const li = document.createElement('li');
    li.className = 'todo-item';
 
    // completedがtrueならchecked属性を追加
    li.innerHTML = `
      <input type="checkbox" ${todo.completed ? 'checked' : ''}>
      <span>${todo.text}</span>
      <button type="button">削除</button>
    `;
 
    // チェックボックスにイベントを設定
    const checkbox = li.querySelector('input[type="checkbox"]');
    checkbox.addEventListener('change', () => {
      toggleComplete(todo.id);
    });
 
    // 削除ボタンにイベントを設定
    const deleteButton = li.querySelector('button');
    deleteButton.addEventListener('click', () => {
      deleteTodo(todo.id);
    });
 
    list.appendChild(li);
  });
}

ポイント

  • ${todo.completed ? 'checked' : ''} - 三項演算子で条件分岐。completedtrue なら checked 属性を追加
  • change イベント - チェックボックスの状態が変わったときに

Step 4: 完了時のスタイルを追加する

CSSで完了したタスクに打ち消し線を追加します。

style.css
/* 完了したタスクのスタイル */
.todo-item input[type="checkbox"]:checked + span {
  text-decoration: line-through;
  color: #888;
}

の解説

input[type="checkbox"]:checked + span を分解すると:

部分意味
input[type="checkbox"]チェックボックス要素
:checkedチェックされている状態
+ span直後の兄弟要素の span

つまり「チェックされたチェックボックスの直後にある span」にスタイルを適用します。

動作確認

ブラウザで確認してみましょう。

  1. タスクを追加する
  2. チェックボックスをクリック
  3. 打ち消し線が表示されればOK!
  4. もう一度クリックして、元に戻ることも確認

完成コード

ここまでの全体像です。

main.js 全体
// HTML要素を取得
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const list = document.getElementById('todo-list');
 
// タスクを保存する配列
let todos = [];
 
// 次のタスクに割り当てるID
let nextId = 1;
 
// タスクを削除する関数
function deleteTodo(id) {
  todos = todos.filter((todo) => todo.id !== id);
  renderTodos();
}
 
// 完了状態を切り替える関数
function toggleComplete(id) {
  todos = todos.map((todo) => {
    if (todo.id === id) {
      return { ...todo, completed: !todo.completed };
    }
    return todo;
  });
  renderTodos();
}
 
// タスクを画面に表示する関数
function renderTodos() {
  list.innerHTML = '';
 
  todos.forEach((todo) => {
    const li = document.createElement('li');
    li.className = 'todo-item';
 
    li.innerHTML = `
      <input type="checkbox" ${todo.completed ? 'checked' : ''}>
      <span>${todo.text}</span>
      <button type="button">削除</button>
    `;
 
    // チェックボックスにイベントを設定
    const checkbox = li.querySelector('input[type="checkbox"]');
    checkbox.addEventListener('change', () => {
      toggleComplete(todo.id);
    });
 
    // 削除ボタンにイベントを設定
    const deleteButton = li.querySelector('button');
    deleteButton.addEventListener('click', () => {
      deleteTodo(todo.id);
    });
 
    list.appendChild(li);
  });
}
 
// フォーム送信時の処理
form.addEventListener('submit', (e) => {
  e.preventDefault();
 
  const text = input.value.trim();
 
  if (!text) {
    return;
  }
 
  todos.push({ id: nextId, text, completed: false });
  nextId++;
 
  renderTodos();
  input.value = '';
});
style.css 全体
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background-color: #f5f5f5;
  min-height: 100vh;
  padding: 40px 20px;
}
 
.container {
  max-width: 480px;
  margin: 0 auto;
  background: #fff;
  border-radius: 8px;
  padding: 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
 
h1 {
  font-size: 24px;
  margin-bottom: 20px;
  color: #333;
}
 
/* フォーム */
#todo-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}
 
#todo-input {
  flex: 1;
  padding: 12px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
 
#todo-input:focus {
  outline: none;
  border-color: #0066ff;
}
 
#todo-form button {
  padding: 12px 20px;
  font-size: 16px;
  background-color: #0066ff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
 
#todo-form button:hover {
  background-color: #0052cc;
}
 
/* タスク一覧 */
#todo-list {
  list-style: none;
}
 
.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-bottom: 1px solid #eee;
}
 
.todo-item:last-child {
  border-bottom: none;
}
 
.todo-item input[type="checkbox"] {
  width: 20px;
  height: 20px;
  cursor: pointer;
}
 
.todo-item span {
  flex: 1;
  font-size: 16px;
}
 
/* 完了したタスクのスタイル */
.todo-item input[type="checkbox"]:checked + span {
  text-decoration: line-through;
  color: #888;
}
 
.todo-item button {
  padding: 4px 8px;
  font-size: 14px;
  background: none;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
  color: #666;
}
 
.todo-item button:hover {
  background-color: #fee;
  border-color: #f66;
  color: #c00;
}

次のステップ

完了/未完了の切り替えができるようになりました!

しかし、ページをリロードするとタスクが全部消えてしまいます。
次のレッスンでは、localStorage を使ってデータを保存する機能を実装します。