シャドウDOM(Shadow DOM)
シャドウDOMはWeb Componentsにとってカスタム要素と並ぶ重要な側面で、文書構造や振る舞いのカプセル化を司っています(カプセル化については前回を参照)。
ここで、シャドウDOMの概念について簡単におさらいしておきましょう。シャドウDOMは、通常の文書(window.document
)とは切り離され、独立してレンダリングされる、シャドウツリーと呼ばれるDOMツリーを扱うための機能です。ほとんどのケースで、通常のDOMツリーと同じ感覚で操作しても問題ありません。シャドウツリーはいくつでも作ることができ、各シャドウツリーの根本となる要素はシャドウルートと呼ばれます。通常の文書から切り離されたままでは表示ができないので、実用上は通常の文書のツリーのどこかの要素に接続して扱うことになりますが、このとき接続する先の要素をシャドウホストと呼びます。
それぞれの用語を図にまとめると、図2のように表せます。
シャドウツリーと文書ツリーは明確に区切られており、カプセル化されているシャドウツリーの内部でエラーが起きても、文書ツリーを壊すことはありません。
概念だけだとわかりづらいので、実際にどのように見えるのか確認してみましょう。図3は、リスト5のレンダリング結果をDev Toolsで表示したものです。
文書ツリー側でシャドウホスト、つまりシャドウツリーと文書ツリーを紐づける役割を担っているのが、カスタム要素である
です。シャドウホストは#shadow-root
以下のシャドウツリーと繋がっており、通常のDOMツリーと同じように表示されますが、前述の通り、文書ツリーとシャドウツリーは別々にレンダリングされています。
さて、ここまで解説したことで、ようやくAPIの解説ができるようになりました。シャドウDOMにとって重要なことは、シャドウホストと紐づいたシャドウルートを生み出し、シャドウツリーを組み上げることです。そのために用意されているのが、カスタム要素のクラスのプロパティとして提供されているattachShadow
メソッドです(リスト6)。
class MyProfile extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); // (1) const para = document.createElement('p'); this.shadowRoot.appendChild(para); // (2) } }
(1)を実行することで、シャドウホストであるカスタム要素にシャドウルートが紐付きます。パラメータのmode
はDev Tools等でシャドウルートの内部にアクセスできるかどうかで、open
ならば可視、closed
ならば不可視となります。実装を完全に隠したい場合はclosed
を選びたくなるところですが、closed
にした場合にも比較的容易に内部へアクセスできることが公式ドキュメントで言及されているため、事実上、指定できるパラメータはopen
一択となるようです。
シャドウルートとの紐付けが済むと、カスタム要素のクラスでshadowRoot
というプロパティが利用できるようになります。(2)のように動的に要素を追加することで、シャドウツリーを構築することができます。shadowRoot
はElementやDocumentと同じくNodeのインターフェースを持っているため、appendChild
のような見慣れたメソッドを利用できます。
このように、シャドウDOMの仕組みを利用することで、文書ツリーから切り離された文書構造を組み上げ、カスタム要素に紐づけることができます。
HTMLテンプレート(HTML Templates)
HTMLテンプレートは、再利用可能な文書構造をHTMLファイルの中に記述できる機能です。
HTMLファイルの中に<template>
要素で囲んだ領域を作ると、通常のレンダリングではページに表示されない要素になります。これをJavaScriptから参照したりコピーしたりすることで、文書構造を再利用します。本来はWeb Componentsと関係なく利用できる機能ですが、シャドウDOMの文書構造を定義する用途との相性が非常に良いため、Web Componentsを成立させるための重要な要素のひとつに数えられています。
また、HTMLテンプレートで作成したテンプレートの一部に後から要素を挿入するための仕組みとして、<slot>
という要素が用意されています。
それでは、実際の使い方を見てみましょう。シャドウDOMの文書構造をテンプレートを利用して定義します(リスト7)。
class MyProfile extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); let template = document.getElementById('my-profile'); // (1) let templateContent = template.content; this.shadowRoot.appendChild(templateContent.cloneNode(true)); // (2) } } customElements.define("my-profile", MyProfile);
本記事のこれまでの例では、シャドウDOMの文書構造はinnerHTML
を用いて定義していました。しかし、リスト7ではHTML側にテンプレートを用意するため、(1)のように要素を呼び出すだけでOKです。要素をそのまま操作するとテンプレートの定義内容が変わってしまい、他のコピーした先にも影響してしまうため、(2)でcloneNode
を呼び出し、元の要素をコピーしています。
JavaScript側の文書構造の定義はこれでOKです。
次は、(1)で呼び出したテンプレートをどのように定義しているか見てみましょう(リスト8)。
<!DOCTYPE html> <html lang="ja"> <body> <div> <my-profile> <span slot="fullname">Taro Suzuki</span> <span slot="age">15</span> </my-profile> </div> <template id="my-profile"> <div> <h1>My Profile</h1> <div id="fullname">full name: <slot name="fullname">John Doe</slot></div> <div id="age">age: <slot name="age">unknown</slot></div> </div> </template> <script src="./template.js"></script> </body> </html>
リスト7で呼び出していたテンプレートは、(1)に定義したものです。テンプレート内の構造としては、リスト4で定義していたものとあまり変わらないように見えますが、(2)の部分は従来<span>
要素だったところを<slot>
に置き換えてあります。<slot>
は後から別の要素に置き換えることができる特殊な要素です。置き換えられなかった場合は、<slot>
の子要素がデフォルト値として使用されます。
スロットを置き換える要素は、(3)のようにカスタム要素の子要素として記述します。<slot>
のname
属性と、置き換えたい要素のslot
属性が一致した場合に、スロットは置き換えられます。
このように、HTMLテンプレートの機能を利用することで、再利用性と拡張性に優れた文書構造をHTMLファイルの中に記述できます。
まとめ
Web Componentsを構成する3つの要素にそれぞれ着目して解説を行いました。それぞれが特徴的なので混同して困ることはないのですが、やや煩雑な印象がありますね。次回は、Web Componentsを簡便に扱うためのライブラリを解説します。