WebDesign Dackel

JavaScriptでCSSセレクタを使ったEvent Delegationの実装

Hatena0
Google+0
Pocket0
Feedly0

はじめに

まだDOMツリーに描画されていない要素に対してイベントを設定したい場合、jQueryを使用していると以下のメソッドを使いますよね。

  • jQuery.fn.on(events, selector, handler)
  • jQuery.fn.delegate(selector, eventType, handler)

これらをjQuery無しで実装しようとすると、以下の様なコードになると思います。

document.addEventListener("click", function(e) {
  var el = e.target;
  while (el && el !== document) {
    if (el.nodeName === "A" && el.getAttribute("href").match(/^#/)) {
      // イベントの処理...
      break;
    }
    el = el.parentNode;
  }
}, false);

やっていることは以下の通り。

  1. documentに対してクリックイベントを設定
  2. 子要素で起きた全てのイベントを受け取る
  3. 実際にイベントの起きた要素からparentNodeを使い、documentに当たるまで親要素を参照
  4. nodeNameAhref属性が#で始まる要素を判定
  5. マッチした要素があれば、そこで親要素の参照を終了

最悪、上記のコードでも動作するので問題ありませんが、4に書いた判定部分があまり直感的では無いですよね…。

要素の判別処理をCSSセレクタで指定出来れば、a[href^="#"]のようにシンプルな内容なのに、いちいちnodeNamegetAttribute、またはclassListを駆使して判定するのは面倒です。

そこで今回は、CSSセレクタを使ったEvent Delegationを実装してみます。

Element.matches()を活用

CSSセレクタを使った要素の判定で、そのものズバリなAPIが用意されていました。

Element.matches() – Web API インターフェイス | MDN

これを使うことで、先程はnodeNameなどを参照していた箇所を、以下の様に書き換えることが出来ます。

if (el.matches("a[href^='#']") {
  // イベントの処理...
}

これは便利ですね。

しかし、querySelectorよりもブラウザ対応が遅れていてIE9をはじめ、古いバージョンのChromeFirefoxでプレフィックスが必要みたいです。

先程のページでPolyfillが掲載されていたので、今回はそちらを使用したいと思います。

実装コード

先程のElement.matches(Polyfill)を使い、CSSセレクタでの指定に対応したEvent Delegationの実装を関数化してみます。

// https://developer.mozilla.org/ja/docs/Web/API/Element/matches
function matches(elm, selector) {
  var matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),
  i = matches.length;
  while (--i >= 0 && matches.item(i) !== elm) ;
  return i > -1;
}

function delegateEvent(root, eventType, selector, listener) {
  root.addEventListener(eventType, function(e) {
    var el = e.target;
    while (el && el !== root) {
      if (matches(el, selector)) {
        listener.call(el, e, el);
        break;
      }
      el = el.parentNode;
    }
  }, false);
}

実際に使用する際は、以下の様になります。

delegateEvent(document, "click", "a[href^='#']", function(e, el) {
  // イベントの処理...
});

大分直感的に書けるようになったかなと思います。

注意点や実装のポイント

リスナーの第二引数に該当要素をそのまま渡しているのは、ES6のアロー関数、または.bind()を使った際に、currentTargetに当たる要素が取得できないための対策となっています。

あと、上記の実装だとaddEventListenerに無名関数を渡しているので、そのままだとリスナーの解除が出来ない点に注意が必要かなと思います。