基礎から学ぶJavaScript入門 90日コース | Webページを操作するDOM - Day 40:イベント委譲

JavaScript 90日で身につけるJavaScript
スポンサーリンク
スポンサーリンク
  1. Day 40:イベント委譲(前半)
  2. イベント委譲とは何か
    1. 子要素ではなく“親要素にイベントを付ける”という発想
    2. なぜイベント委譲が必要になるのか
  3. 実践のゴールイメージ:動的に追加したToDoを操作する
  4. 基本のToDoリスト構造を作る
    1. HTMLのベースを用意する
  5. 動的にToDo項目を追加する
    1. JavaScriptでToDo項目(li)を作る関数
    2. フォーム送信でToDoを追加する
  6. 親要素にイベントを設定して子要素を操作する
    1. イベント委譲の基本パターン
    2. クリックされた要素が「完了チェック」か「削除ボタン」かを判定する
  7. 完了チェックと削除の処理を関数に分ける
    1. 削除処理 handleDelete
    2. 完了状態の切り替え handleDoneToggle
  8. 完成コード(イベント委譲で動的ToDoを操作)
  9. イベント委譲の“おいしさ”と注意点
    1. イベント委譲のメリット
    2. 注意点(セキュリティ・安全性も含めて)
  10. Day 40 前半 練習用テンプレート
  11. Day 40 前半のまとめ
  12. Day 40:イベント委譲(後半)
  13. イベント委譲を“設計する目線”で捉え直す
    1. 親要素にイベントを付ける=「窓口を1つにする」
    2. 動的要素への対応=「増えても減っても同じ窓口で扱う」
  14. 複数の操作をイベント委譲でさばくToDoリストを設計する
    1. 今回のToDo項目の構成
    2. HTMLのベースを用意する
  15. ToDo項目を“役割付き”で作る
    1. JavaScriptでToDo項目を作る関数
    2. フォーム送信でToDoを追加する
  16. 親要素にイベントを設定して複数操作をさばく
    1. イベント委譲の“分岐パターン”を作る
  17. 完了・削除・編集の処理をそれぞれ設計する
    1. 削除処理 handleDelete
    2. 完了状態の切り替え handleDoneToggle
    3. 編集処理 handleEdit
  18. 完成コード(複数操作+イベント委譲+動的ToDo)
  19. イベント委譲の“深掘りポイント”
    1. 役割をクラスで表現することの意味
    2. 動的要素への対応が“自然になる”感覚
    3. H3:セキュリティ・安全性の視点
  20. Day 40 後半 練習用テンプレート
  21. Day 40 後半のまとめ

Day 40:イベント委譲(前半)

「親要素にイベントを設定して“あとから増える要素”もスマートに操作する」

Day 40前半では、JavaScriptのイベント処理の中でも実務で頻出のテクニック、イベント委譲を学びます。 テーマは 「親要素にイベントを設定」「動的要素への対応」、実践は 「動的に追加したToDoを操作」 です。

これまでのDayで、addEventListener() を使ってボタンやフォームにイベントを付けてきましたが、 「あとから追加される要素」にも同じようにイベントを付けたい 場面が必ず出てきます。 イベント委譲は、その問題をとてもスマートに解決する考え方です。

イベント委譲とは何か

子要素ではなく“親要素にイベントを付ける”という発想

イベント委譲(Event Delegation)とは、

「たくさんの子要素それぞれにイベントを付ける代わりに、 親要素に1つだけイベントを付けて、 event.target などを使って“どの子が操作されたか”を判定する」

というテクニックです。

例えば、ToDoリストの各項目に「削除」ボタンがある場合、

  • 各削除ボタンに addEventListener('click', ...) を付ける
  • ではなく、ToDoリスト全体(<ul>)に1つだけクリックイベントを付ける

という形にします。

なぜイベント委譲が必要になるのか

特に重要なのは 「動的に追加される要素」 への対応です。

  • ユーザーがフォームからToDoを追加する
  • JavaScriptで <li> 要素を createElement()appendChild() で追加する
  • その新しい <li> にも「削除」ボタンや「完了」チェックのイベントを付けたい

このとき、 追加のたびにイベントを付けるコードを書くのは面倒で、バグの原因にもなります。

イベント委譲なら、

「親要素にだけイベントを付けておき、 あとから増えた子要素も、同じイベント処理でまとめて扱う」

ことができます。

実践のゴールイメージ:動的に追加したToDoを操作する

Day 40前半のゴールは、次のようなToDoリストです。

  • フォームからToDoを追加すると、<li> が動的に増える
  • 各ToDoには「完了」チェックボックスと「削除」ボタンがある
  • これらの操作は、親の <ul> に付けたイベントだけで処理する

つまり、

「ToDoが増えても、イベントリスナーは増えない」

という状態を目指します。

基本のToDoリスト構造を作る

HTMLのベースを用意する

まずは、ToDoリストとフォームの基本構造を用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 前半:イベント委譲でToDo操作</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを操作する</h1>

  <!-- ▼ ToDoリスト(親要素) -->
  <ul id="todoList">
    <!-- ここに<li>が動的に追加されます -->
  </ul>

  <!-- ▼ ToDo追加フォーム -->
  <form id="todoForm">
    <label for="todoInput">ToDo:</label>
    <input type="text" id="todoInput" placeholder="やることを入力してください">
    <button type="submit">追加</button>
  </form>

  <!-- ▼ メッセージ表示エリア -->
  <p id="message">操作結果をここに表示します。</p>

  <script>
    // ここにJavaScriptを書いていきます
  </script>
</body>
</html>

ここで重要なのは、 <ul id="todoList"> が「親要素」であるということです。 この親にイベントを付けて、子の <li> やその中のボタン・チェックボックスをまとめて扱っていきます。

動的にToDo項目を追加する

JavaScriptでToDo項目(li)を作る関数

まずは、1つのToDo項目を作る関数を定義します。 ここでは、完了チェックと削除ボタンを持つ <li> を作ります。

// ▼ DOM要素の取得
const todoListElement = document.getElementById('todoList');
const todoFormElement = document.getElementById('todoForm');
const todoInputElement = document.getElementById('todoInput');
const messageElement = document.getElementById('message');

// ▼ ToDo項目(li)を作る関数
function createTodoItem(text) {
  // <li>要素を作成
  const liElement = document.createElement('li');

  // 完了チェックボックス
  const doneCheckbox = document.createElement('input');
  doneCheckbox.type = 'checkbox';
  doneCheckbox.classList.add('todo-done'); // 判定用のクラス

  // ToDoテキスト表示用の<span>
  const textSpan = document.createElement('span');
  textSpan.textContent = text; // ユーザー入力を安全にテキストとして扱います

  // 削除ボタン
  const deleteButton = document.createElement('button');
  deleteButton.textContent = '削除';
  deleteButton.classList.add('todo-delete'); // 判定用のクラス

  // liの中に、チェックボックス・テキスト・削除ボタンを追加
  liElement.append(doneCheckbox, textSpan, deleteButton);

  return liElement;
}
JavaScript

ここでは、イベントはまだ付けていません。 イベント委譲を使うので、子要素にはイベントを付けないのがポイントです。

フォーム送信でToDoを追加する

次に、フォーム送信イベントでToDoを追加します。

// ▼ フォーム送信時の処理
todoFormElement.addEventListener('submit', function (event) {
  event.preventDefault(); // ページリロードを防ぎます

  const enteredText = todoInputElement.value.trim();
  messageElement.textContent = '';

  if (enteredText === '') {
    messageElement.textContent = 'ToDoが空です。やることを入力してください。';
    return;
  }

  // ToDo項目を作成
  const liElement = createTodoItem(enteredText);

  // 親の<ul>に追加
  todoListElement.appendChild(liElement);

  messageElement.textContent = `「${enteredText}」を追加しました。`;

  // 入力欄をリセット
  todoInputElement.value = '';
});
JavaScript

ここまでで、

  • フォームからToDoを追加すると、<li> が動的に増える
  • <li> にはチェックボックスと削除ボタンがある

という状態ができました。

次のステップで、 親要素にイベントを付けて、これらを操作できるようにします。

親要素にイベントを設定して子要素を操作する

イベント委譲の基本パターン

イベント委譲の基本パターンは、次のようになります。

親要素.addEventListener('イベント種類', function (event) {
  const target = event.target; // 実際に操作された要素

  // targetがどの種類の要素かを判定して、処理を分岐
});
JavaScript

今回のToDoリストでは、

  • 親要素:todoListElement<ul>
  • イベント種類:click
  • 子要素:チェックボックス(完了)、削除ボタン

という構成です。

クリックされた要素が「完了チェック」か「削除ボタン」かを判定する

// ▼ ToDoリスト(親要素)にクリックイベントを設定
todoListElement.addEventListener('click', function (event) {
  const clickedElement = event.target; // 実際にクリックされた要素

  // ▼ 削除ボタンがクリックされた場合
  if (clickedElement.classList.contains('todo-delete')) {
    handleDelete(clickedElement);
    return;
  }

  // ▼ 完了チェックボックスがクリックされた場合
  if (clickedElement.classList.contains('todo-done')) {
    handleDoneToggle(clickedElement);
    return;
  }

  // ▼ それ以外のクリックは何もしません
});
JavaScript

ここで重要なのは、

  • 親の <ul> にだけイベントを付けていること
  • event.target(ここでは clickedElement)から、 クラス名で「どの種類の子要素か」を判定していること

です。

完了チェックと削除の処理を関数に分ける

削除処理 handleDelete

削除ボタンがクリックされたときの処理を関数にします。

// ▼ 削除ボタンがクリックされたときの処理
function handleDelete(deleteButtonElement) {
  // 削除ボタンの親<li>を取得します
  const liElement = deleteButtonElement.closest('li');

  if (!liElement) {
    return; // 念のため安全に
  }

  const textSpan = liElement.querySelector('span');
  const todoText = textSpan ? textSpan.textContent : '';

  // 実際に<li>を削除します
  liElement.remove();

  messageElement.textContent = `「${todoText}」を削除しました。`;
  console.log('ToDo削除:', todoText);
}
JavaScript

ここでのポイントは、

  • closest('li') を使って、「この削除ボタンが属しているToDo項目」を特定していること
  • remove() でその <li> をDOMから削除していること
  • メッセージ表示に textContent を使い、安全にテキストを扱っていること

です。

完了状態の切り替え handleDoneToggle

完了チェックボックスがクリックされたときの処理を関数にします。

// ▼ 完了チェックがクリックされたときの処理
function handleDoneToggle(checkboxElement) {
  const liElement = checkboxElement.closest('li');

  if (!liElement) {
    return;
  }

  const textSpan = liElement.querySelector('span');
  if (!textSpan) {
    return;
  }

  if (checkboxElement.checked) {
    // 完了状態にする(見た目を変える)
    textSpan.style.textDecoration = 'line-through';
    textSpan.style.color = '#888';
    messageElement.textContent = `「${textSpan.textContent}」を完了にしました。`;
  } else {
    // 未完了に戻す
    textSpan.style.textDecoration = 'none';
    textSpan.style.color = '';
    messageElement.textContent = `「${textSpan.textContent}」を未完了に戻しました。`;
  }

  console.log('完了状態切り替え:', textSpan.textContent, 'checked:', checkboxElement.checked);
}
JavaScript

ここでは、

  • チェックボックスの状態(checked)に応じて、 テキストの見た目を変えています。
  • 完了・未完了の切り替えを、親のイベントから子要素を特定して行っていることがポイントです。

完成コード(イベント委譲で動的ToDoを操作)

ここまでのコードを1つにまとめます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 前半:イベント委譲で動的ToDoを操作</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを操作する</h1>

  <ul id="todoList">
    <!-- ここに<li>が動的に追加されます -->
  </ul>

  <form id="todoForm">
    <label for="todoInput">ToDo:</label>
    <input type="text" id="todoInput" placeholder="やることを入力してください">
    <button type="submit">追加</button>
  </form>

  <p id="message">操作結果をここに表示します。</p>

  <script>
    const todoListElement = document.getElementById('todoList');
    const todoFormElement = document.getElementById('todoForm');
    const todoInputElement = document.getElementById('todoInput');
    const messageElement = document.getElementById('message');

    function createTodoItem(text) {
      const liElement = document.createElement('li');

      const doneCheckbox = document.createElement('input');
      doneCheckbox.type = 'checkbox';
      doneCheckbox.classList.add('todo-done');

      const textSpan = document.createElement('span');
      textSpan.textContent = text;

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '削除';
      deleteButton.classList.add('todo-delete');

      liElement.append(doneCheckbox, textSpan, deleteButton);

      return liElement;
    }

    todoFormElement.addEventListener('submit', function (event) {
      event.preventDefault();

      const enteredText = todoInputElement.value.trim();
      messageElement.textContent = '';

      if (enteredText === '') {
        messageElement.textContent = 'ToDoが空です。やることを入力してください。';
        return;
      }

      const liElement = createTodoItem(enteredText);
      todoListElement.appendChild(liElement);

      messageElement.textContent = `「${enteredText}」を追加しました。`;
      todoInputElement.value = '';
    });

    todoListElement.addEventListener('click', function (event) {
      const clickedElement = event.target;

      if (clickedElement.classList.contains('todo-delete')) {
        handleDelete(clickedElement);
        return;
      }

      if (clickedElement.classList.contains('todo-done')) {
        handleDoneToggle(clickedElement);
        return;
      }
    });

    function handleDelete(deleteButtonElement) {
      const liElement = deleteButtonElement.closest('li');
      if (!liElement) return;

      const textSpan = liElement.querySelector('span');
      const todoText = textSpan ? textSpan.textContent : '';

      liElement.remove();
      messageElement.textContent = `「${todoText}」を削除しました。`;
      console.log('ToDo削除:', todoText);
    }

    function handleDoneToggle(checkboxElement) {
      const liElement = checkboxElement.closest('li');
      if (!liElement) return;

      const textSpan = liElement.querySelector('span');
      if (!textSpan) return;

      if (checkboxElement.checked) {
        textSpan.style.textDecoration = 'line-through';
        textSpan.style.color = '#888';
        messageElement.textContent = `「${textSpan.textContent}」を完了にしました。`;
      } else {
        textSpan.style.textDecoration = 'none';
        textSpan.style.color = '';
        messageElement.textContent = `「${textSpan.textContent}」を未完了に戻しました。`;
      }

      console.log('完了状態切り替え:', textSpan.textContent, 'checked:', checkboxElement.checked);
    }
  </script>
</body>
</html>

イベント委譲の“おいしさ”と注意点

イベント委譲のメリット

  • 動的に追加された要素にも自動的に対応できる
    • 新しいToDo項目にも、追加のコードなしで完了・削除が効きます。
  • イベントリスナーの数が減る
    • 親に1つだけイベントを付けるので、パフォーマンスや管理の面で有利です。
  • コードの見通しが良くなる
    • 「親で受けて、子を判定する」というパターンが決まるので、構造がはっきりします。

注意点(セキュリティ・安全性も含めて)

  • event.target は「一番内側の要素」を指すため、 必要に応じて closest() で「本当に扱いたい要素」を特定する必要があります。
  • ユーザー入力を表示するときは、必ず textContent を使うことが重要です。
    • innerHTML を使うと、入力された文字列がHTMLとして解釈されてしまい、 <script> などが実行される危険があります。
  • 削除や完了などの操作は、 「本当にその要素でよいか」を closest('li') などで慎重に特定してから行うと安全です。

Day 40 前半 練習用テンプレート

最後に、今日の内容をそのまま試せる練習用テンプレートを紹介します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 前半:イベント委譲 練習テンプレート</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを操作する練習</h1>

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

  <form id="todoForm">
    <input type="text" id="todoInput" placeholder="ToDoを入力">
    <button type="submit">追加</button>
  </form>

  <p id="message"></p>

  <script>
    console.log('--- イベント委譲 練習開始 ---');

    const todoListElement = document.getElementById('todoList');
    const todoFormElement = document.getElementById('todoForm');
    const todoInputElement = document.getElementById('todoInput');
    const messageElement = document.getElementById('message');

    function createTodoItem(text) {
      const li = document.createElement('li');

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.classList.add('todo-done');

      const span = document.createElement('span');
      span.textContent = text;

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '削除';
      deleteButton.classList.add('todo-delete');

      li.append(checkbox, span, deleteButton);
      return li;
    }

    todoFormElement.addEventListener('submit', (event) => {
      event.preventDefault();

      const text = todoInputElement.value.trim();
      messageElement.textContent = '';

      if (text === '') {
        messageElement.textContent = '入力が空です。';
        return;
      }

      todoListElement.appendChild(createTodoItem(text));
      todoInputElement.value = '';
    });

    todoListElement.addEventListener('click', (event) => {
      const target = event.target;
      console.log('event.target:', target);

      if (target.classList.contains('todo-delete')) {
        const li = target.closest('li');
        const span = li.querySelector('span');
        const text = span ? span.textContent : '';
        li.remove();
        messageElement.textContent = `「${text}」を削除しました。`;
        return;
      }

      if (target.classList.contains('todo-done')) {
        const li = target.closest('li');
        const span = li.querySelector('span');
        if (!span) return;

        if (target.checked) {
          span.style.textDecoration = 'line-through';
          span.style.color = '#888';
        } else {
          span.style.textDecoration = 'none';
          span.style.color = '';
        }
      }
    });

    console.log('--- イベント委譲 練習テンプレートの準備が完了しました ---');
  </script>
</body>
</html>

Day 40 前半のまとめ

Day 40前半では、

  • 親要素にイベントを設定する「イベント委譲」の考え方を学んだこと。
  • 動的に追加されるToDo項目に対しても、 親の <ul> に付けたイベントだけで完了・削除を実現できること。
  • event.targetclosest()classList.contains() を組み合わせて、 「どの子要素が操作されたか」を判定するパターンを身につけたこと。
  • ユーザー入力やDOM操作において、 textContent を使って安全性を意識し続けたこと。

を整理しました。

このイベント委譲の感覚は、 メニュー、タブ、カード一覧、チャットメッセージなど、 「増えたり減ったりするUI」を扱うときに必ず役立ちます。


Day 40:イベント委譲(後半)

「“あとから増えるToDo”を当たり前のように扱えるイベント設計を身につける」

Day 40後半では、前半で学んだイベント委譲をさらに一歩深めて、 「動的に追加したToDoを、状態別に・役割別にスマートに操作する」 ことを目指します。

テーマは同じく、

  • 親要素にイベントを設定
  • 動的要素への対応
  • 実践:動的に追加したToDoを操作

ですが、後半では次のようなポイントを意識して進めていきます。

  • 「完了」「削除」「編集」など、複数の操作をイベント委譲でさばく
  • クラスや data 属性を使って“役割”を明確にする
  • コードを整理して「読めるイベント処理」にする

前半で「イベント委譲って便利だな」と感じたところから、 後半では 「イベント委譲を設計の武器として使う」 段階に入っていきます。

イベント委譲を“設計する目線”で捉え直す

親要素にイベントを付ける=「窓口を1つにする」

イベント委譲は、単に「親にイベントを付けるテクニック」ではなく、

「ユーザー操作の窓口を1つにまとめる設計」

だと捉えると、理解が深まります。

ToDoリストで考えると、

  • 各ToDo項目には、
    • 完了チェック
    • 削除ボタン
    • 編集ボタン(後半で追加)
  • それぞれにイベントを付けると、コードがバラバラになる
  • 親の <ul> にイベントを集約すると、 「ToDoに関する操作はここで全部受ける」という窓口ができる

というイメージです。

動的要素への対応=「増えても減っても同じ窓口で扱う」

動的に追加されるToDo項目は、

  • 最初は存在しない
  • ユーザー操作によって増える
  • 削除によって減る

という性質を持っています。

イベント委譲を使うと、

「増えても減っても、親のイベント処理は変わらない」

という状態を作ることができます。

これは、 「UIが変化しても、イベント設計は安定している」 という、とても強いメリットです。

複数の操作をイベント委譲でさばくToDoリストを設計する

今回のToDo項目の構成

後半では、1つのToDo項目を次のように設計します。

  • 完了チェックボックス(input[type="checkbox"]
  • テキスト表示(<span>
  • 編集ボタン(編集
  • 削除ボタン(削除

それぞれに役割を持たせ、 親の <ul> に付けたイベントから、どの操作かを判定して処理します。

HTMLのベースを用意する

まずは、前半と同じようにベースとなるHTMLを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 後半:イベント委譲で複数操作ToDo</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを複数操作する</h1>

  <!-- ▼ ToDoリスト(親要素) -->
  <ul id="todoList">
    <!-- ここに<li>が動的に追加されます -->
  </ul>

  <!-- ▼ ToDo追加フォーム -->
  <form id="todoForm">
    <label for="todoInput">ToDo:</label>
    <input type="text" id="todoInput" placeholder="やることを入力してください">
    <button type="submit">追加</button>
  </form>

  <!-- ▼ メッセージ表示エリア -->
  <p id="message">操作結果をここに表示します。</p>

  <script>
    // ここにJavaScriptを書いていきます
  </script>
</body>
</html>

ToDo項目を“役割付き”で作る

JavaScriptでToDo項目を作る関数

ここでは、各要素に「役割を示すクラス」を付けておきます。 これにより、イベント委譲で判定しやすくなります。

// ▼ DOM要素の取得
const todoListElement = document.getElementById('todoList');
const todoFormElement = document.getElementById('todoForm');
const todoInputElement = document.getElementById('todoInput');
const messageElement = document.getElementById('message');

// ▼ ToDo項目(li)を作る関数
function createTodoItem(text) {
  const liElement = document.createElement('li');

  // 完了チェックボックス
  const doneCheckbox = document.createElement('input');
  doneCheckbox.type = 'checkbox';
  doneCheckbox.classList.add('todo-done'); // 完了操作用

  // テキスト表示用の<span>
  const textSpan = document.createElement('span');
  textSpan.textContent = text;
  textSpan.classList.add('todo-text'); // 編集対象のテキスト

  // 編集ボタン
  const editButton = document.createElement('button');
  editButton.textContent = '編集';
  editButton.classList.add('todo-edit'); // 編集操作用

  // 削除ボタン
  const deleteButton = document.createElement('button');
  deleteButton.textContent = '削除';
  deleteButton.classList.add('todo-delete'); // 削除操作用

  // liの中に、チェックボックス・テキスト・編集ボタン・削除ボタンを追加
  liElement.append(doneCheckbox, textSpan, editButton, deleteButton);

  return liElement;
}
JavaScript

ここでのポイントは、

  • 各要素に todo-done, todo-edit, todo-delete, todo-text といったクラスを付けていること
  • このクラスが「イベント委譲で判定するためのラベル」になること

です。

フォーム送信でToDoを追加する

前半と同様に、フォーム送信でToDoを追加します。

todoFormElement.addEventListener('submit', function (event) {
  event.preventDefault();

  const enteredText = todoInputElement.value.trim();
  messageElement.textContent = '';

  if (enteredText === '') {
    messageElement.textContent = 'ToDoが空です。やることを入力してください。';
    return;
  }

  const liElement = createTodoItem(enteredText);
  todoListElement.appendChild(liElement);

  messageElement.textContent = `「${enteredText}」を追加しました。`;
  todoInputElement.value = '';
});
JavaScript

ここまでで、

  • 動的にToDo項目が追加される
  • 各項目には完了・編集・削除のUIが揃っている

という状態ができました。

親要素にイベントを設定して複数操作をさばく

イベント委譲の“分岐パターン”を作る

親の <ul> にクリックイベントを付けて、 クリックされた要素のクラスに応じて処理を分岐します。

todoListElement.addEventListener('click', function (event) {
  const clickedElement = event.target;

  // ▼ 削除ボタンがクリックされた場合
  if (clickedElement.classList.contains('todo-delete')) {
    handleDelete(clickedElement);
    return;
  }

  // ▼ 編集ボタンがクリックされた場合
  if (clickedElement.classList.contains('todo-edit')) {
    handleEdit(clickedElement);
    return;
  }

  // ▼ 完了チェックボックスがクリックされた場合
  if (clickedElement.classList.contains('todo-done')) {
    handleDoneToggle(clickedElement);
    return;
  }

  // ▼ それ以外のクリックは何もしません
});
JavaScript

ここでの重要ポイントは、

  • 親に1つだけイベントを付けていること
  • classList.contains() で「どの操作か」を判定していること
  • 各操作を関数に分けて、コードを読みやすくしていること

です。

完了・削除・編集の処理をそれぞれ設計する

削除処理 handleDelete

function handleDelete(deleteButtonElement) {
  const liElement = deleteButtonElement.closest('li');
  if (!liElement) return;

  const textSpan = liElement.querySelector('.todo-text');
  const todoText = textSpan ? textSpan.textContent : '';

  liElement.remove();

  messageElement.textContent = `「${todoText}」を削除しました。`;
  console.log('ToDo削除:', todoText);
}
JavaScript
  • closest('li') で「この削除ボタンが属しているToDo項目」を特定
  • remove() でその項目を削除
  • メッセージ表示に textContent を使い、安全にテキストを扱う

という流れです。

完了状態の切り替え handleDoneToggle

function handleDoneToggle(checkboxElement) {
  const liElement = checkboxElement.closest('li');
  if (!liElement) return;

  const textSpan = liElement.querySelector('.todo-text');
  if (!textSpan) return;

  if (checkboxElement.checked) {
    textSpan.style.textDecoration = 'line-through';
    textSpan.style.color = '#888';
    messageElement.textContent = `「${textSpan.textContent}」を完了にしました。`;
  } else {
    textSpan.style.textDecoration = 'none';
    textSpan.style.color = '';
    messageElement.textContent = `「${textSpan.textContent}」を未完了に戻しました。`;
  }

  console.log('完了状態切り替え:', textSpan.textContent, 'checked:', checkboxElement.checked);
}
JavaScript
  • チェック状態に応じて見た目を変える
  • 完了・未完了の切り替えを、親のイベントから子要素を特定して行っている

という点が、イベント委譲の活かしどころです。

編集処理 handleEdit

編集ボタンがクリックされたときに、 テキストを入力欄に変えて編集できるようにしてみます。

function handleEdit(editButtonElement) {
  const liElement = editButtonElement.closest('li');
  if (!liElement) return;

  const textSpan = liElement.querySelector('.todo-text');
  if (!textSpan) return;

  const currentText = textSpan.textContent;

  // すでに編集モードの場合は何もしない(簡易チェック)
  if (liElement.querySelector('.todo-edit-input')) {
    return;
  }

  // ▼ 編集用の入力欄を作成
  const inputElement = document.createElement('input');
  inputElement.type = 'text';
  inputElement.value = currentText;
  inputElement.classList.add('todo-edit-input');

  // ▼ 決定ボタンを作成
  const saveButton = document.createElement('button');
  saveButton.textContent = '決定';
  saveButton.classList.add('todo-edit-save');

  // ▼ 元のテキストを非表示にして、入力欄+決定ボタンを表示
  textSpan.style.display = 'none';
  liElement.insertBefore(inputElement, editButtonElement);
  liElement.insertBefore(saveButton, editButtonElement.nextSibling);

  // ▼ 決定ボタンのクリックは、イベント委譲で処理してもよいですが、
  //   ここでは分かりやすさのために直接イベントを付けます。
  saveButton.addEventListener('click', function () {
    const newText = inputElement.value.trim();

    if (newText === '') {
      messageElement.textContent = '空のToDoにはできません。';
      return;
    }

    textSpan.textContent = newText;
    textSpan.style.display = '';

    inputElement.remove();
    saveButton.remove();

    messageElement.textContent = `ToDoを「${newText}」に編集しました。`;
    console.log('ToDo編集:', newText);
  });
}
JavaScript

ここでは、

  • 編集ボタンから、そのToDo項目を特定
  • テキストを入力欄に変えて編集モードにする
  • 決定ボタンで編集内容を反映する

という流れを作っています。

本来であれば、決定ボタンもイベント委譲で扱うことができますが、 初心者向けとして「編集モードの中だけは直接イベントを付ける」形にしています。

完成コード(複数操作+イベント委譲+動的ToDo)

ここまでの内容を1つにまとめた完成コードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 後半:イベント委譲で複数操作ToDo</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを複数操作する</h1>

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

  <form id="todoForm">
    <label for="todoInput">ToDo:</label>
    <input type="text" id="todoInput" placeholder="やることを入力してください">
    <button type="submit">追加</button>
  </form>

  <p id="message">操作結果をここに表示します。</p>

  <script>
    const todoListElement = document.getElementById('todoList');
    const todoFormElement = document.getElementById('todoForm');
    const todoInputElement = document.getElementById('todoInput');
    const messageElement = document.getElementById('message');

    function createTodoItem(text) {
      const liElement = document.createElement('li');

      const doneCheckbox = document.createElement('input');
      doneCheckbox.type = 'checkbox';
      doneCheckbox.classList.add('todo-done');

      const textSpan = document.createElement('span');
      textSpan.textContent = text;
      textSpan.classList.add('todo-text');

      const editButton = document.createElement('button');
      editButton.textContent = '編集';
      editButton.classList.add('todo-edit');

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '削除';
      deleteButton.classList.add('todo-delete');

      liElement.append(doneCheckbox, textSpan, editButton, deleteButton);

      return liElement;
    }

    todoFormElement.addEventListener('submit', function (event) {
      event.preventDefault();

      const enteredText = todoInputElement.value.trim();
      messageElement.textContent = '';

      if (enteredText === '') {
        messageElement.textContent = 'ToDoが空です。やることを入力してください。';
        return;
      }

      const liElement = createTodoItem(enteredText);
      todoListElement.appendChild(liElement);

      messageElement.textContent = `「${enteredText}」を追加しました。`;
      todoInputElement.value = '';
    });

    todoListElement.addEventListener('click', function (event) {
      const clickedElement = event.target;

      if (clickedElement.classList.contains('todo-delete')) {
        handleDelete(clickedElement);
        return;
      }

      if (clickedElement.classList.contains('todo-edit')) {
        handleEdit(clickedElement);
        return;
      }

      if (clickedElement.classList.contains('todo-done')) {
        handleDoneToggle(clickedElement);
        return;
      }
    });

    function handleDelete(deleteButtonElement) {
      const liElement = deleteButtonElement.closest('li');
      if (!liElement) return;

      const textSpan = liElement.querySelector('.todo-text');
      const todoText = textSpan ? textSpan.textContent : '';

      liElement.remove();
      messageElement.textContent = `「${todoText}」を削除しました。`;
      console.log('ToDo削除:', todoText);
    }

    function handleDoneToggle(checkboxElement) {
      const liElement = checkboxElement.closest('li');
      if (!liElement) return;

      const textSpan = liElement.querySelector('.todo-text');
      if (!textSpan) return;

      if (checkboxElement.checked) {
        textSpan.style.textDecoration = 'line-through';
        textSpan.style.color = '#888';
        messageElement.textContent = `「${textSpan.textContent}」を完了にしました。`;
      } else {
        textSpan.style.textDecoration = 'none';
        textSpan.style.color = '';
        messageElement.textContent = `「${textSpan.textContent}」を未完了に戻しました。`;
      }

      console.log('完了状態切り替え:', textSpan.textContent, 'checked:', checkboxElement.checked);
    }

    function handleEdit(editButtonElement) {
      const liElement = editButtonElement.closest('li');
      if (!liElement) return;

      const textSpan = liElement.querySelector('.todo-text');
      if (!textSpan) return;

      const currentText = textSpan.textContent;

      if (liElement.querySelector('.todo-edit-input')) {
        return;
      }

      const inputElement = document.createElement('input');
      inputElement.type = 'text';
      inputElement.value = currentText;
      inputElement.classList.add('todo-edit-input');

      const saveButton = document.createElement('button');
      saveButton.textContent = '決定';
      saveButton.classList.add('todo-edit-save');

      textSpan.style.display = 'none';
      liElement.insertBefore(inputElement, editButtonElement);
      liElement.insertBefore(saveButton, editButtonElement.nextSibling);

      saveButton.addEventListener('click', function () {
        const newText = inputElement.value.trim();

        if (newText === '') {
          messageElement.textContent = '空のToDoにはできません。';
          return;
        }

        textSpan.textContent = newText;
        textSpan.style.display = '';

        inputElement.remove();
        saveButton.remove();

        messageElement.textContent = `ToDoを「${newText}」に編集しました。`;
        console.log('ToDo編集:', newText);
      });
    }
  </script>
</body>
</html>

イベント委譲の“深掘りポイント”

役割をクラスで表現することの意味

  • todo-done, todo-edit, todo-delete, todo-text といったクラスは、 「この要素は何のために存在しているか」 をコードに刻むラベルです。
  • イベント委譲では、 「どの役割の要素が操作されたか」をクラスで判定するため、 クラス設計がそのままイベント設計になります。

これは、 「見た目のためのクラス」と「意味のためのクラス」を分ける」 というCSS設計にもつながる考え方です。

動的要素への対応が“自然になる”感覚

イベント委譲を使っていると、

  • 新しいToDo項目を追加しても、 何も考えずに同じUIを追加するだけで、操作が効く
  • 削除しても、イベントリスナーの片付けを意識しなくてよい

という状態になります。

これは、

「UIは変化しても、イベント設計は変化しない」

という、とても心地よい状態です。

H3:セキュリティ・安全性の視点

今回のコードでも、次の点を守っています。

  • ユーザー入力を画面に表示するときは、必ず textContent を使う
    • innerHTML を使うと、入力された文字列がHTMLとして解釈されてしまい、 <script> などが実行される危険があります。
  • closest('li')querySelector('.todo-text') で、 「本当に操作してよい要素か」を特定してから削除・編集を行う

こうした習慣は、 小さなToDoアプリでも、大きなWebサービスでも同じように重要です。

Day 40 後半 練習用テンプレート

最後に、今日の内容をそのまま試せる練習用テンプレートを紹介します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 40 後半:イベント委譲 総合練習テンプレート</title>
</head>
<body>
  <h1>イベント委譲で動的ToDoを複数操作する総合練習</h1>

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

  <form id="todoForm">
    <input type="text" id="todoInput" placeholder="ToDoを入力">
    <button type="submit">追加</button>
  </form>

  <p id="message"></p>

  <script>
    console.log('--- Day 40 後半 総合練習開始 ---');

    const todoListElement = document.getElementById('todoList');
    const todoFormElement = document.getElementById('todoForm');
    const todoInputElement = document.getElementById('todoInput');
    const messageElement = document.getElementById('message');

    function createTodoItem(text) {
      const li = document.createElement('li');

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.classList.add('todo-done');

      const span = document.createElement('span');
      span.textContent = text;
      span.classList.add('todo-text');

      const editButton = document.createElement('button');
      editButton.textContent = '編集';
      editButton.classList.add('todo-edit');

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '削除';
      deleteButton.classList.add('todo-delete');

      li.append(checkbox, span, editButton, deleteButton);
      return li;
    }

    todoFormElement.addEventListener('submit', (event) => {
      event.preventDefault();

      const text = todoInputElement.value.trim();
      messageElement.textContent = '';

      if (text === '') {
        messageElement.textContent = '入力が空です。';
        return;
      }

      todoListElement.appendChild(createTodoItem(text));
      todoInputElement.value = '';
    });

    todoListElement.addEventListener('click', (event) => {
      const target = event.target;
      console.log('event.target:', target);

      if (target.classList.contains('todo-delete')) {
        const li = target.closest('li');
        const span = li.querySelector('.todo-text');
        const text = span ? span.textContent : '';
        li.remove();
        messageElement.textContent = `「${text}」を削除しました。`;
        return;
      }

      if (target.classList.contains('todo-done')) {
        const li = target.closest('li');
        const span = li.querySelector('.todo-text');
        if (!span) return;

        if (target.checked) {
          span.style.textDecoration = 'line-through';
          span.style.color = '#888';
        } else {
          span.style.textDecoration = 'none';
          span.style.color = '';
        }
        return;
      }

      if (target.classList.contains('todo-edit')) {
        const li = target.closest('li');
        const span = li.querySelector('.todo-text');
        if (!span) return;

        const currentText = span.textContent;

        if (li.querySelector('.todo-edit-input')) {
          return;
        }

        const input = document.createElement('input');
        input.type = 'text';
        input.value = currentText;
        input.classList.add('todo-edit-input');

        const saveButton = document.createElement('button');
        saveButton.textContent = '決定';
        saveButton.classList.add('todo-edit-save');

        span.style.display = 'none';
        li.insertBefore(input, target);
        li.insertBefore(saveButton, target.nextSibling);

        saveButton.addEventListener('click', () => {
          const newText = input.value.trim();
          if (newText === '') {
            messageElement.textContent = '空のToDoにはできません。';
            return;
          }

          span.textContent = newText;
          span.style.display = '';
          input.remove();
          saveButton.remove();

          messageElement.textContent = `ToDoを「${newText}」に編集しました。`;
        });
      }
    });

    console.log('--- Day 40 後半 総合練習テンプレートの準備が完了しました ---');
  </script>
</body>
</html>

Day 40 後半のまとめ

Day 40後半では、

  • 親要素にイベントを設定する「イベント委譲」を、 完了・削除・編集といった複数操作に広げて設計したこと。
  • 動的に追加されたToDo項目に対しても、 親のイベントだけで自然に操作できる状態を作ったこと。
  • クラスや closest() を使って、 「どの要素が・どの役割で・どのように操作されたか」を判定するパターンを身につけたこと。
  • ユーザー入力やDOM操作において、 textContent を使って安全性を意識し続けたこと。

を整理しました。

このイベント委譲の感覚は、 ToDoリストに限らず、コメント一覧、チャット、カードUI、タブ切り替えなど、 「増えたり減ったりするインタラクティブなUI」を設計するときの強力な武器になります。

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