uhyohyo.net

JavaScript初級者から中級者になろう

十七章第六回 Shadow DOM 3

このページの最終更新日:

今回もShadow DOMの話です。とはいえ、大体のことは前回までで紹介してしまいました。

今回のテーマはShadow DOMとイベントです。具体的には、shadowツリーの中で発生したイベントはどうなるのでしょうか。

早速ですが、次のサンプルを見てください(サンプル1)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Shadow DOMとイベントのサンプル</title>
  </head>
  <body>
    <div onclick="console.log(event.target)">
      <my-element></my-element>
    </div>
    <template id="mytmp">
      <p>
        <button>ボタン</button>
      </p>
    </template>
    <script>
      class MyElement extends HTMLElement {
        constructor(){
          super();

          this.attachShadow({
            mode: 'open',
          }).appendChild(document.getElementById('mytmp').content.cloneNode(true));
        }
      }
      customElements.define('my-element', MyElement);
    </script>
  </body>
</html>

8行目に注目してください。ここにclickイベントに対するイベントハンドラが仕込んであり、event.target、すなわちイベントが発生した要素を表示しています。

ここで、このページを開くとボタンがひとつ表示されます。それをクリックしてclickイベントが発生した場合を考えてみましょう。

普通の場合、event.target、すなわちイベントオブジェクトのtargetプロパティはクリックされた要素になるはずです。この場合、クリックされた要素はbutton要素(13行目)です。問題は、button要素はmy-element要素により表示されており、shadowツリーの中に存在しているということです。

結果を言ってしまうと、このときevent.targetとなるのはmy-element要素です。

つまり、shadowツリーの中で発生したイベントがshadowツリーの外に出た場合、shadowホストが発生源であるように偽装されるのです。なお、shadowホストというのはshadowツリーを持っている要素のことです。

今回の場合、my-elementのshadowツリーの中で発生したイベントはイベントバブリングによって上へと伝わっていきますが、それがmy-elementの外に出た時点でshadowツリーの情報は隠蔽され、イベントの発生源はmy-element要素であることになります。

これはWeb Components的観点からいけばとても妥当ですね。カスタム要素を使う側は、その要素の中でイベントが発生した場合、その中のどこで発生したかという情報には興味が無く、それよりはそのカスタム要素で発生したという情報のほうが重要です。カスタム要素を使う側はその内部構造を知らなくても要素を使えないといけないのです。

ただし、shadowツリーがオープンな場合には、イベントに関する内部の情報が一応取得できるようになっています。そのためには、イベントオブジェクトが持つcomposedPathメソッドを使います。次の例ではcomposedPathの結果を表示しています(サンプル2)。


<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Shadow DOMとイベントのサンプル2</title>
  </head>
  <body>
    <div onclick="console.log(event.composedPath())">
      <my-element></my-element>
    </div>
    <template id="mytmp">
      <p>
        <button>ボタン</button>
      </p>
    </template>
    <script>
      class MyElement extends HTMLElement {
        constructor(){
          super();

          this.attachShadow({
            mode: 'open',
          }).appendChild(document.getElementById('mytmp').content.cloneNode(true));
        }
      }
      customElements.define('my-element', MyElement);
    </script>
  </body>
</html>

ボタンをクリックするとコンソールにevent.composedPath()の結果が表示されます。これはノードの配列で、このイベントが通る全てのノードが列挙されています。一番最初はクリックされたbutton要素で、どんどん上へと遡り最後はdocument、及びその上のwindowまで遡って終わりです。

よく見ると、この一覧はshadowツリーの中の要素もちゃんと列挙されています。ShadowRootもちゃんと含まれているのが面白いですね。どうしても必要なときはこれを使いましょう。

composedPathはあまり使う機会がないかもしれませんが、shadowツリーを出るときのイベントの挙動は理解しておいたほうがいいのではないかと思います。


今回はこれで終わりです。今回まで、3回にわたりShadow DOMを紹介しました。Shadow DOMは、カスタム要素を作るときには必須といえるほど便利です。これで皆さんもShadow DOMを使ってカスタム要素を作れるようになりましたね。