キャプチャリングとバブリングにより、イベントデリゲーションと呼ばれる、最も強力なイベント処理パターンの1つを実装できます。
多くの要素が同様の方法で処理される場合、各要素にハンドラーを割り当てる代わりに、共通の祖先に単一のハンドラーを配置するというアイデアです。
ハンドラーでは、event.target
を使用してイベントが実際に発生した場所を確認し、処理します。
例を見てみましょう - 古代中国の哲学を反映した八卦図です。
こちらがそれです
HTMLはこんな感じです
<table>
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="n">...</td>
<td class="ne">...</td>
</tr>
<tr>...2 more lines of this kind...</tr>
<tr>...2 more lines of this kind...</tr>
</table>
このテーブルには9つのセルがありますが、99個でも9999個でも構いません。
私たちのタスクは、クリック時にセル<td>
を強調表示することです。
各<td>
にonclick
ハンドラーを割り当てる代わりに(多数ある可能性があります)、<table>
要素に「キャッチオール」ハンドラーを設定します。
クリックされた要素を取得し、それを強調表示するためにevent.target
を使用します。
コード
let selectedTd;
table.onclick = function(event) {
let target = event.target; // where was the click?
if (target.tagName != 'TD') return; // not on TD? Then we're not interested
highlight(target); // highlight it
};
function highlight(td) {
if (selectedTd) { // remove the existing highlight if any
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // highlight the new td
}
このようなコードは、テーブルにいくつセルがあるか気にしません。いつでも動的に<td>
を追加/削除できますが、強調表示は引き続き機能します。
それでも、欠点があります。
クリックは<td>
上ではなく、その内部で発生する可能性があります。
私たちのケースでは、HTMLの中を見ると、<strong>
のような、<td>
の中にネストされたタグがあることがわかります。
<td>
<strong>Northwest</strong>
...
</td>
当然のことながら、その<strong>
上でクリックが発生すると、それがevent.target
の値になります。
ハンドラーtable.onclick
では、そのようなevent.target
を取得し、クリックが<td>
の内側であったかどうかを確認する必要があります。
こちらが改良されたコードです
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
解説
- メソッド
elem.closest(selector)
は、セレクターに一致する最も近い祖先を返します。私たちのケースでは、ソース要素から上に向かって<td>
を探します。 event.target
がどの<td>
の中にもない場合、やるべきことがないので、呼び出しはすぐに返ります。- ネストされたテーブルの場合、
event.target
は<td>
になる可能性がありますが、現在のテーブルの外側にあります。そのため、それが実際に私たちのテーブルの<td>
であるかどうかを確認します。 - そして、そうであれば、それを強調表示します。
結果として、テーブル内の<td>
の総数に関係なく、高速で効率的な強調表示コードが得られます。
デリゲーションの例:マークアップのアクション
イベントデリゲーションには、他にも用途があります。
たとえば、「保存」、「読み込み」、「検索」などのボタンを持つメニューを作成したいとします。そして、save
、load
、search
…などのメソッドを持つオブジェクトがあります。それらをどのように一致させますか?
最初のアイデアは、各ボタンに個別のハンドラーを割り当てることかもしれません。しかし、もっとエレガントな解決策があります。メニュー全体に対するハンドラーと、呼び出すメソッドを持つボタンのdata-action
属性を追加できます。
<button data-action="save">Click to Save</button>
ハンドラーは属性を読み取り、メソッドを実行します。動作例をご覧ください
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
this.onClick
は(*)
でthis
にバインドされていることに注意してください。これは重要です。そうでなければ、その中のthis
はDOM要素(elem
)を参照し、Menu
オブジェクトを参照せず、this[action]
は私たちが必要とするものではありません。
では、デリゲーションはここでどのような利点を私たちにもたらすのでしょうか?
- 各ボタンにハンドラーを割り当てるコードを書く必要はありません。メソッドを作成してマークアップに配置するだけです。
- HTML構造は柔軟性があり、いつでもボタンを追加/削除できます。
.action-save
、.action-load
などのクラスを使用することもできますが、data-action
属性の方がセマンティック的に優れています。CSSルールでも使用できます。
「ビヘイビア」パターン
イベントデリゲーションを使用して、特別な属性とクラスで要素に「ビヘイビア」を宣言的に追加することもできます。
このパターンは2つの部分からなります
- その動作を記述するカスタム属性を要素に追加します。
- ドキュメント全体のハンドラーはイベントを追跡し、属性付き要素でイベントが発生すると、アクションを実行します。
ビヘイビア:カウンター
たとえば、属性data-counter
は、「クリック時に値を増やす」という動作をボタンに追加します。
Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // if the attribute exists...
event.target.value++;
}
});
</script>
ボタンをクリックすると、その値が増加します。ボタンではなく、ここで重要なのは一般的なアプローチです。
data-counter
を持つ属性はいくつでも使用できます。いつでもHTMLに新しいものを追加できます。イベントデリゲーションを使用してHTMLを「拡張」し、新しい動作を記述する属性を追加しました。
addEventListener
document
オブジェクトにイベントハンドラーを割り当てる場合は、常にaddEventListener
を使用し、document.on<event>
は使用しないでください。後者は競合を引き起こすためです。新しいハンドラーは古いハンドラーを上書きします。
実際のプロジェクトでは、コードのさまざまな部分によって設定された多くのハンドラーがdocument
にあるのが一般的です。
ビヘイビア:トグル
ビヘイビアのもう1つの例。属性data-toggle-id
を持つ要素をクリックすると、指定されたid
を持つ要素が表示/非表示になります。
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<form id="subscribe-mail" hidden>
Your mail: <input type="email">
</form>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
let elem = document.getElementById(id);
elem.hidden = !elem.hidden;
});
</script>
もう一度、私たちがやったことを指摘しましょう。これで、要素にトグル機能を追加するには、JavaScriptを知る必要がなく、data-toggle-id
属性を使用するだけです。
これは非常に便利になる可能性があります。そのような要素ごとにJavaScriptを書く必要はありません。ビヘイビアを使用するだけです。ドキュメントレベルのハンドラーにより、ページの任意の要素で動作します。
単一の要素に複数のビヘイビアを組み合わせることもできます。
「ビヘイビア」パターンは、JavaScriptのミニフラグメントの代替手段となる可能性があります。
まとめ
イベントデリゲーションは本当にクールです!DOMイベントで最も役立つパターンの1つです。
多くの同様の要素に同じ処理を追加するために使用されることがよくありますが、それだけではありません。
アルゴリズム
- コンテナに単一のハンドラーを配置します。
- ハンドラーでは、ソース要素
event.target
を確認します。 - イベントが私たちに関心のある要素内で発生した場合は、イベントを処理します。
メリット
- 初期化を簡素化し、メモリを節約します。多くのハンドラーを追加する必要はありません。
- コードが少なくて済みます。要素を追加または削除する場合、ハンドラーを追加/削除する必要はありません。
- DOMの変更:
innerHTML
などを使用して、要素を一括で追加/削除できます。
デリゲーションにはもちろん限界があります。
- まず、イベントはバブリングする必要があります。バブリングしないイベントもあります。また、低レベルのハンドラーは
event.stopPropagation()
を使用しないでください。 - 第二に、デリゲーションはCPU負荷を追加する可能性があります。なぜなら、コンテナレベルのハンドラーは、私たちに関心があるかどうかに関係なく、コンテナの任意の場所のイベントに反応するからです。しかし、通常、負荷は無視できるほど小さいので、考慮しません。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合は、サンドボックス(plnkr、jsbin、codepen…)を使用してください。