イベントデリゲーションの基本 — parent.addEventListener('click', e => { if (e.target.matches('button')) ... })
イベントデリゲーションは、子要素それぞれにリスナーを付けず、親に1つだけ付けて「イベントの伝播」を利用して処理するテクニックです。動的に増えるボタンにも自動で対応でき、パフォーマンスと保守性が向上します。
仕組みとメリット
- イベントの伝播(バブリング): クリックなどのイベントは、発生した要素から親→さらに親…と上に伝わります。親で受けて「どの子が押されたか」を判定します。
- メリット:
- 追加・削除に強い: 後から追加された子にも自動対応。
- メモリ効率: リスナー数が1つで済む。
- 保守性: 1箇所のロジックで全子要素の挙動を管理。
基本のコード例(ボタンのクリックをまとめて処理)
<div id="toolbar">
<button data-action="save">保存</button>
<button data-action="delete">削除</button>
<button data-action="share">共有</button>
</div>
<script>
const toolbar = document.getElementById("toolbar");
toolbar.addEventListener("click", (e) => {
const btn = e.target.closest("button"); // クリック元から最も近い button
if (!btn || !toolbar.contains(btn)) return; // 親内のbutton以外は無視
const action = btn.dataset.action;
switch (action) {
case "save": console.log("保存します"); break;
case "delete": console.log("削除します"); break;
case "share": console.log("共有します"); break;
}
});
</script>
HTML- ラベル:
closest("button")でボタンを安全に特定し、datasetで処理を切り替え。
よく使うテンプレート集
リストの項目クリックで選択(委譲)
const list = document.getElementById("list");
list.addEventListener("click", (e) => {
const item = e.target.closest("li.item");
if (!item || !list.contains(item)) return;
item.classList.toggle("selected");
});
JavaScript- ラベル: 子の追加・削除に強く、全項目のクリックを親で一括管理。
アイコンごとに別アクション(多ターゲット)
const cards = document.getElementById("cards");
cards.addEventListener("click", (e) => {
const like = e.target.closest(".icon-like");
const del = e.target.closest(".icon-delete");
const card = e.target.closest(".card");
if (!card || !cards.contains(card)) return;
if (like) { card.classList.toggle("liked"); }
else if (del) { card.remove(); }
});
JavaScript- ラベル: 同じ親で複数のターゲットクラスを判定して分岐。
フォーム内の動的ボタンに対応(追加してもOK)
const form = document.getElementById("profile");
form.addEventListener("click", (e) => {
const addPhone = e.target.closest("[data-add='phone']");
if (!addPhone) return;
const field = document.createElement("input");
field.name = "phone";
field.placeholder = "電話番号";
form.querySelector(".phones").appendChild(field);
});
JavaScript- ラベル: 後から追加したボタンでも動く。個別の
addEventListenerは不要。
使い分けのポイント(委譲 vs 直接リスナー)
- 委譲が向く場面:
- 要素が増減する: リストやカードが動的に生成される。
- 数が多い: 子が大量にあり、個別リスナーは負担。
- 一括ロジック: 同じ挙動をまとめて管理したい。
- 直接リスナーが向く場面:
- 単発の要素: 1〜数個で増減しない。
- 厳密な発火位置: バブリングの影響を避けたい特殊ケース。
実務でのコツ
- ターゲット特定は
closestが安全:- ネストされた要素内のクリックでも、目的の親ボタン/項目を拾える。
- スコープ防御:
parent.contains(target)で親の外からのイベント混入を避ける。
- 選択の基準はクラスや data-属性:
data-actionや.itemは意味が明確で、テスト・保守が楽。
- パフォーマンス:
- 親を細かく分けて委譲すると、無駄な判定が減る。大きな
documentより近い親を使う。
- 親を細かく分けて委譲すると、無駄な判定が減る。大きな
ありがちなハマりポイントと対策
- テキストクリックで
e.targetが想定外:- 対策:
e.target.closest("button")で安全に“ボタン”を得る。
- 対策:
- 親外のクリックが引っかかる:
- 対策:
if (!parent.contains(target)) return;を入れる。
- 対策:
- stopPropagation の誤用:
- 対策: 不要に伝播を止めると委譲が効かない。必要な場面だけ使う。
- 動的要素に個別リスナーを足し続ける:
- 対策: 委譲に切り替える。増えるほど効果がある。
練習問題(手を動かして覚える)
<ul id="list">
<li class="item">Apple <button class="del">削除</button></li>
<li class="item">Banana <button class="del">削除</button></li>
</ul>
<button id="add">追加</button>
<script>
const list = document.getElementById("list");
const add = document.getElementById("add");
// 1) 削除ボタンのクリックを親で処理
list.addEventListener("click", (e) => {
const del = e.target.closest(".del");
if (!del || !list.contains(del)) return;
const li = del.closest("li.item");
li.remove();
});
// 2) 追加ボタンで新しい項目を動的生成(委譲だからイベントは不要)
add.addEventListener("click", () => {
const li = document.createElement("li");
li.className = "item";
li.innerHTML = `New <button class="del">削除</button>`;
list.appendChild(li);
});
// 3) 項目クリックで選択トグル(同じ親で別動作)
list.addEventListener("click", (e) => {
const item = e.target.closest("li.item");
if (!item || !list.contains(item)) return;
if (!e.target.closest(".del")) { // 削除ボタン以外のクリックのみ
item.classList.toggle("selected");
}
});
</script>
HTML直感的な指針
- 親に1つだけリスナーを置き、
closestとdataset/クラスでターゲット判定。 - 動的に増えるUIや大量要素は委譲が効率的。スコープ防御と分岐を丁寧に。
- 個別リスナーが増えてきたら、委譲へ切り替えるサイン。
