2020年12月6日

Shadow DOM

Shadow DOMはカプセル化のために使用されます。コンポーネントが、メインドキュメントから誤ってアクセスされることのない、独自の「シャドウ」DOMツリーを持つことを可能にし、ローカルスタイルルールなどを適用できます。

組み込みのShadow DOM

複雑なブラウザコントロールがどのように作成され、スタイル設定されているか考えたことはありますか?

例えば<input type="range">のようなものです。

ブラウザはそれらを描画するために内部的にDOM/CSSを使用します。そのDOM構造は通常私たちからは隠されていますが、開発者ツールで見ることができます。例えばChromeでは、開発者ツールで「Show user agent shadow DOM」オプションを有効にする必要があります。

すると<input type="range">は次のようになります。

#shadow-rootの下に見えるものが「シャドウDOM」と呼ばれるものです。

組み込みのシャドウDOM要素は、通常のJavaScript呼び出しやセレクターでは取得できません。これらは通常の子供ではなく、強力なカプセル化技術です。

上記の例では、便利な属性pseudoが見えます。これは非標準で、歴史的な理由で存在します。これを使ってCSSでサブ要素をスタイル設定することができます。

<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
  background: red;
}
</style>

<input type="range">

もう一度言いますが、pseudoは非標準の属性です。時系列的に、ブラウザは最初にコントロールを実装するために内部DOM構造の実験を開始し、その後、時間が経ってから、私たち開発者が同様のことをできるように、シャドウDOMが標準化されました。

この後、DOM仕様やその他の関連仕様でカバーされている最新のシャドウDOM標準を使用します。

シャドウツリー

DOM要素は2種類のDOMサブツリーを持つことができます。

  1. ライトツリー – HTMLの子からなる通常のDOMサブツリー。これまでの章で見てきたすべてのサブツリーは「ライト」でした。
  2. シャドウツリー – HTMLには反映されず、詮索好きな目から隠された、非表示のDOMサブツリー。

もし要素が両方を持つ場合、ブラウザはシャドウツリーのみをレンダリングします。しかし、シャドウツリーとライトツリーの間で一種の構成を設定することもできます。詳細については、この章の後半のShadow DOMスロット、構成で説明します。

シャドウツリーは、カスタム要素でコンポーネントの内部を隠し、コンポーネントローカルのスタイルを適用するために使用できます。

例えば、この<show-hello>要素は、シャドウツリーで内部DOMを隠します。

<script>
customElements.define('show-hello', class extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({mode: 'open'});
    shadow.innerHTML = `<p>
      Hello, ${this.getAttribute('name')}
    </p>`;
  }
});
</script>

<show-hello name="John"></show-hello>

これがChrome開発者ツールでのDOMの最終的な表示方法です。すべてのコンテンツは「#shadow-root」の下にあります。

まず、elem.attachShadow({mode: …})の呼び出しによってシャドウツリーが作成されます。

2つの制限事項があります。

  1. 要素ごとに作成できるシャドウルートは1つだけです。
  2. elemは、カスタム要素であるか、「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h6」、「header」、「main」、「nav」、「p」、「section」、または「span」のいずれかである必要があります。<img>のような他の要素は、シャドウツリーをホストできません。

modeオプションはカプセル化レベルを設定します。次の2つの値のいずれかを持つ必要があります。

  • "open" – シャドウルートはelem.shadowRootとして利用可能です。

    どのコードでもelemのシャドウツリーにアクセスできます。

  • "closed"elem.shadowRootは常にnullです。

    attachShadowによって返された参照によってのみシャドウDOMにアクセスできます(そしておそらくクラス内部に隠されています)。<input type="range">のようなブラウザネイティブのシャドウツリーはクローズされています。それらにアクセスする方法はありません。

attachShadowによって返されるシャドウルートは要素のようなものです。innerHTMLまたはappendのようなDOMメソッドを使用して、要素をポピュレートすることができます。

シャドウルートを持つ要素は「シャドウツリーホスト」と呼ばれ、シャドウルートのhostプロパティとして利用可能です。

// assuming {mode: "open"}, otherwise elem.shadowRoot is null
alert(elem.shadowRoot.host === elem); // true

カプセル化

シャドウDOMはメインドキュメントから強く区切られています。

  1. シャドウDOM要素は、ライトDOMからのquerySelectorには見えません。特に、シャドウDOM要素は、ライトDOMの要素と競合するIDを持つ可能性があります。それらはシャドウツリー内でのみ一意である必要があります。
  2. シャドウDOMは独自のスタイルシートを持っています。外部DOMからのスタイルルールは適用されません。

例:

<style>
  /* document style won't apply to the shadow tree inside #elem (1) */
  p { color: red; }
</style>

<div id="elem"></div>

<script>
  elem.attachShadow({mode: 'open'});
    // shadow tree has its own style (2)
  elem.shadowRoot.innerHTML = `
    <style> p { font-weight: bold; } </style>
    <p>Hello, John!</p>
  `;

  // <p> is only visible from queries inside the shadow tree (3)
  alert(document.querySelectorAll('p').length); // 0
  alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
  1. ドキュメントのスタイルは、シャドウツリーには影響しません。
  2. …ただし、内部からのスタイルは機能します。
  3. シャドウツリー内の要素を取得するには、ツリー内部からクエリする必要があります。

参考文献

まとめ

シャドウDOMは、コンポーネントローカルのDOMを作成する方法です。

  1. shadowRoot = elem.attachShadow({mode: open|closed})elemのシャドウDOMを作成します。mode="open"の場合、elem.shadowRootプロパティとしてアクセスできます。
  2. innerHTMLまたはその他のDOMメソッドを使用してshadowRootをポピュレートすることができます。

シャドウDOM要素

  • 独自のID空間を持ち、
  • querySelectorのようなメインドキュメントからのJavaScriptセレクターには見えず、
  • メインドキュメントからではなく、シャドウツリーからのスタイルのみを使用します。

シャドウDOMが存在する場合、ブラウザは、いわゆる「ライトDOM」(通常の子供)の代わりにレンダリングします。この章のShadow DOMスロット、構成では、それらを構成する方法を見ていきます。

チュートリアルマップ

コメント

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