2022年2月3日

イベントデリゲーション

キャプチャリングとバブリングにより、イベントデリゲーションと呼ばれる、最も強力なイベント処理パターンの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)
};

解説

  1. メソッドelem.closest(selector)は、セレクターに一致する最も近い祖先を返します。私たちのケースでは、ソース要素から上に向かって<td>を探します。
  2. event.targetがどの<td>の中にもない場合、やるべきことがないので、呼び出しはすぐに返ります。
  3. ネストされたテーブルの場合、event.target<td>になる可能性がありますが、現在のテーブルの外側にあります。そのため、それが実際に私たちのテーブルの<td>であるかどうかを確認します。
  4. そして、そうであれば、それを強調表示します。

結果として、テーブル内の<td>の総数に関係なく、高速で効率的な強調表示コードが得られます。

デリゲーションの例:マークアップのアクション

イベントデリゲーションには、他にも用途があります。

たとえば、「保存」、「読み込み」、「検索」などのボタンを持つメニューを作成したいとします。そして、saveloadsearch…などのメソッドを持つオブジェクトがあります。それらをどのように一致させますか?

最初のアイデアは、各ボタンに個別のハンドラーを割り当てることかもしれません。しかし、もっとエレガントな解決策があります。メニュー全体に対するハンドラーと、呼び出すメソッドを持つボタンの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つの部分からなります

  1. その動作を記述するカスタム属性を要素に追加します。
  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つです。

多くの同様の要素に同じ処理を追加するために使用されることがよくありますが、それだけではありません。

アルゴリズム

  1. コンテナに単一のハンドラーを配置します。
  2. ハンドラーでは、ソース要素event.targetを確認します。
  3. イベントが私たちに関心のある要素内で発生した場合は、イベントを処理します。

メリット

  • 初期化を簡素化し、メモリを節約します。多くのハンドラーを追加する必要はありません。
  • コードが少なくて済みます。要素を追加または削除する場合、ハンドラーを追加/削除する必要はありません。
  • DOMの変更:innerHTMLなどを使用して、要素を一括で追加/削除できます。

デリゲーションにはもちろん限界があります。

  • まず、イベントはバブリングする必要があります。バブリングしないイベントもあります。また、低レベルのハンドラーはevent.stopPropagation()を使用しないでください。
  • 第二に、デリゲーションはCPU負荷を追加する可能性があります。なぜなら、コンテナレベルのハンドラーは、私たちに関心があるかどうかに関係なく、コンテナの任意の場所のイベントに反応するからです。しかし、通常、負荷は無視できるほど小さいので、考慮しません。

課題

重要度:5

削除ボタン[x]付きのメッセージのリストがあります。ボタンを機能させます。

こんな感じです

追伸 コンテナにイベントリスナーを1つだけ使用し、イベントデリゲーションを使用してください。

課題のサンドボックスを開きます。

重要度:5

クリック時にノードの子を表示/非表示するツリーを作成します。

要件

  • イベントハンドラーは1つのみ(デリゲーションを使用)
  • ノードタイトルの外側(空のスペース)をクリックしても何も起こりません。

課題のサンドボックスを開きます。

この解決策には2つの部分があります。

  1. すべてのツリーノードタイトルを<span>でラップします。そうすれば、:hoverでCSSスタイルを適用でき、<span>の幅はテキストの幅とまったく同じであるため(そうでない場合とは異なり)、テキスト上で正確にクリックを処理できます。
  2. treeルートノードにハンドラーを設定し、その<span>タイトルのクリックを処理します。

サンドボックスで解答を開きます。

重要度:4

テーブルをソート可能にします。<th>要素をクリックすると、対応する列でソートされます。

<th>には、次のように属性に型があります。

<table id="grid">
  <thead>
    <tr>
      <th data-type="number">Age</th>
      <th data-type="string">Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>5</td>
      <td>John</td>
    </tr>
    <tr>
      <td>10</td>
      <td>Ann</td>
    </tr>
    ...
  </tbody>
</table>

上記の例では、最初の列には数値が、2番目の列には文字列があります。ソート関数は、型に応じてソートを処理する必要があります。

サポートされる型は"string""number"のみです。

動作例

追伸 テーブルは大きく、任意の数の行と列を持つことができます。

課題のサンドボックスを開きます。

重要度:5

ツールチップの動作のためのJSコードを作成します。

マウスがdata-tooltipを持つ要素の上に来ると、その上にツールチップが表示され、マウスが離れると非表示になります。

注釈付きHTMLの例

<button data-tooltip="the tooltip is longer than the element">Short button</button>
<button data-tooltip="HTML<br>tooltip">One more button</button>

このように動作する必要があります

このタスクでは、data-tooltipを持つすべての要素には、内部にテキストのみがあると仮定します。ネストされたタグはありません(まだ)。

詳細

  • 要素とツールチップの間の距離は5pxである必要があります。
  • 可能であれば、ツールチップは要素に対して中央揃えにする必要があります。
  • ツールチップはウィンドウの端を越えることはできません。通常は要素の上に表示されますが、要素がページの上部にあり、ツールチップを表示するスペースがない場合は、要素の下に表示されます。
  • ツールチップの内容はdata-tooltip属性で指定されます。任意のHTMLを使用できます。

ここでは2つのイベントが必要です

  • mouseoverは、ポインターが要素の上に来るとトリガーされます。
  • mouseoutは、ポインターが要素から離れるとトリガーされます。

イベントデリゲーションを使用してください。data-tooltipを持つ要素からのすべての「オーバー」と「アウト」を追跡するために、documentに2つのハンドラーを設定し、そこからツールチップを管理します。

動作が実装された後、JavaScriptに慣れていない人でも、注釈付き要素を追加できます。

追伸 ツールチップは一度に1つだけ表示できます。

課題のサンドボックスを開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がありましたら、コメントする代わりにGitHubのissueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。