Shadow DOMは、コンポーネントの作成に役立つ新しいDOM機能です。Shadow DOMは、エレメント内のスコープ付きのサブツリーと考えることができます。

詳細はWeb Fundamentalsを読んでください。このドキュメントでは、Shadow DOMの概要の内、Polymerに関連する部分を説明しています。Shadow DOMに関する包括的な解説は、Web FundamentalsのShadow DOM v1: self-contained web componentsを参照してください。

ページタイトルとメニューボタンのあるヘッダーコンポーネントについて考えてみましょう。このエレメントのDOMツリーは次のようになるでしょう。:

<my-header>
  <header>
    <h1>
    <button>

Shadow DOMを使用すると、スコープ付きサブツリー内に子を置くことができるため、ドキュメントレベル(document-level)のCSSから意図せずボタンのスタイルを再適用してしまうといったことはなくなります。このサブツリーはShadow Treeと呼ばれます。

<my-header>
  #shadow-root
    <header>
      <h1>
      <button>

Shadow RootがShadow Treeのトップです。<my-header>に追加(attached)されたツリーのエレメントはShadowホストと呼ばれます。ホストには、Shadow Rootを参照するshadowRootというプロパティがあります。このShadow Rootには、そのホストエレメントを参照するhostプロパティがあります。

Shadow Treeはエレメントのchildrenとは区別されます。このShadow Treeは、外部のエレメントにとっては関知する必要のない(カプセル化された)コンポーネントの実装の一部と考えることができます。一方、エレメントの子(children)は、(外部のエレメントに対しても)publicなインタフェースの一部です。

以下のように、attachShadowを呼び出すことで命令的にエレメントにShadow Treeを追加(attach)できます:

var div = document.createElement('div');
var shadowRoot = div.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

Polymerは、DOMテンプレートを使用して宣言的にShadow Treeを追加するためのメカニズムを提供しています。エレメントにDOMテンプレートを追加すると、Polymerはエレメントの各インスタンスにShadow Rootを追加(attach)して、テンプレートの内容をShadow Treeに複製します。

<dom-module id="my-header">
  <template>
    <style>...</style>
    <header>
      <h1>I'm a header</h1>
      <button>Menu</button>
    </header>
  </template>
</dom-module>

テンプレート内に<style>エレメントが含まれていることに注意してください。Shadow Treeに配置されたCSSはShadow Tree内部にスコープを持ち、DOMの他の部分に対してスコープがリークすることはありません。

デフォルトでは、エレメントにShdow DOMがある場合、Shadow Treeがエレメントの子に代わってレンダリングされます。子をレンダリングするためには、<slot>エレメントをShadow Treeに追加します。<slot>は、子ノードのレンダリング先を示すプレースホルダと考えることができます。例として、以下のような<my-header>のShadow Treeについて考えてみましょう。:

<header>
  <h1><slot></slot></h1>
  <button>Menu</button>
</header>

ユーザーは次のように子を追加できます:

<my-header>Shadow DOM</my-header>

<slot>エレメントが子に置き換えられたかのようにヘッダーがレンダリングされます。

<my-header>
  <header>
    <h1>Shadow DOM</h1>
    <button>Menu</button>
  </header>
</my-header>

実際のエレメント(訳注:上記のように実際にレンダリングされたエレメント)の子孫ツリーは、そのShadow DOMツリーとは区別して、Light DOMと呼ばれることもあります。

レンダリングするためにLight DOMとShadow DOMツリーを単一のツリーに変換するプロセスは、ツリーのフラット化(flattening the tree)と呼ばれます。<slot>エレメントがレンダリングされることはありませんが、フラット化されたツリーには含まれるので、イベントバブリングのような処理に加えることができます。

name属性付きのスロットを使用することで、フラット化されたツリーのどこに子を割り当てるべきか指定することもできます。

<h2><slot name="title"></slot></h2>
<div><slot></slot></div>

以下のように、名前付きのスロットは、一致するslot属性持つトップレベルの子だけを受け入れます。(訳注:トップレベルでない子のケースは、後述のサンプルコードで改めて実例付きの解説があります。):

<span slot="title">A heading</span>

name属性を持たないスロットは、slot属性を持たない全ての子のデフォルトのスロットになります。子のslot属性に対応する、名前付きスロットがShadow Tree上に存在しない場合にはその子が表示されることはありません。

次のようなShadow Treeを持ったexample-cardエレメントを例にして考えてみましょう:

<h2><slot name="title"></slot></h2>
<div><slot></slot></div>

これが次のように使われているとします:

<example-card>
  <span slot="title">Card Title</span>
  <div>
    Some text for the body of the card.
  </div>
  <span slot="footer">This footer doesn't show up.</span>
</example-card>

最初のspantitle属性を持つスロットに割り当てられます。slot属性を持たないdivは、デフォルトのスロットに割り当てられます。Shadow Treeにないスロット名を持つ最後のspanは、フラット化されたツリーには出現せずレンダリングもされません。

トップレベルの子だけがスロットにマッチすることに注意してください。次の例で考えてみましょう。:

<example-card>
  <div>
   <span slot="title">Am I a title?</span>
  </div>
  <div>
    Some body text.
  </div>
</example-card>

<example-card>にはトップレベルの子として二つの<div>エレメントがあります。どちらのエレメントもデフォルトのスロットに割り当てられます。spanはトップレベルの子ではないため、spanslot属性が割り当てに影響することはありません。

スロットには一つもノードが割り当てられていないときに表示されるフォールバックコンテンツを含めることができます。例えば:

<fancy-note>
  #shadow-root
    <slot name="icon">
      <img src="note.png">
    </slot>
    <slot></slot>
</fancy-note>

ユーザーは次のように<fancy-note>エレメントに独自のアイコンを指定できます。:

<!-- shows note with warning icon -->

<fancy-note>

  <img slot="icon" src="warning.png">

  Do not operate heavy equipment while coding.

</fancy-note>

ユーザーがアイコンの指定を省略すると、フォールバックコンテンツとしてデフォルトのアイコンが表示されます。:

<!-- shows note with default icon -->

<fancy-note>

  Please code responsibly.

</fancy-note>

slotエレメントを他のスロットに割り当てることもできます。例えば、二つのレベルのShadow Treeを考えてみましょう。

<parent-element>
  #shadow-root
    <child-element>
      <!-- parent-element renders its light DOM children inside
           child-element -->
      <slot id="parent-slot">

<child-element>
  #shadow-root
    <div>
      <!-- child-element renders its light DOM children inside this div -->
      <slot id="child-slot">

このようなマークアップを考えてみましょう。:

<parent-element>
  <span>I'm light DOM</span>
</parent-element>

フラット化されたツリーは次のようになります。:

<parent-element>
  <child-element>
    <div>
      <slot id="child-slot">
        <slot id="parent-slot>
          <span>I'm in light DOM</span>

最初は処理の順番について少し混乱するかもしれません。各レベルにおいて、Light DOMの子は、ホストのShadow DOMの各スロットに割り当てられています。まず<span>I'm in light</span><parent-element>のShadow DOMであるslot(#parent-slot)に割り当てられます。次にこのslot(#parent-slot)が<child-element>のShadow DOMであるslot(#child-slot)に割り当てられます。

注意:この例では、説明のためにスロットにidを使用していますが、これはname属性と同じ働きをするものではありません。 これらのスロットにはnameが付与されておらずデフォルトのスロットになります。

slotエレメントはレンダリングされないので、 レンダーツリーはとてもシンプルです。:

<parent-element>
  <child-element>
    <div>
      <span>I'm in light DOM</span>

仕様上の用語では、スロットのdistributed nodesとは割り当てられたノードのことであり、各スロットは割り当てられたノードまたはフォールバックコンテンツで置き換えられます。したがって、上記の例では、#child-slotには単一のspanのdistributed nodeがあるといえます。distributed nodesは、レンダーツリー内のスロットに置き換えられたノードのリストと考えることができます。

Shadow DOMには、割り当て(distribution)をチェックするための新しいAPIがいくつか用意されています。:

  • HTMLElement.assignedSlotプロパティは、エレメントに割り当てられたスロットを返します。エレメントにスロットが割り当てられていない場合はnullを返します。
  • HTMLSlotElement.assignedNodesメソッドは、指定されたスロットに関連付けられたノードのリストを返します。 {flatten:true}オプションを指定して呼び出すと、スロットのdistributed nodesが返されます。
  • HTMLSlotElement.slotchangeイベントは、slotのdistributed nodeが変更された時点で発生します。

詳細については、Web FundamentalsでのWorking with slots in JSを参照してください。

Polymer.FlattenedNodesObserverクラスは、エレメントのフラット化されたツリーを記録(track)するユーティリティを提供します。つまり、<slot>エレメントがdistributed nodeによって置き換えられたノードの子ノードのリストです。 FlattenedNodesObserverlib/utils/flattened-nodes-observer.htmlから読み込むことができるオプションのユーティリティです。

<link rel="import" href="/bower_components/polymer/lib/utils/flattened-nodes-observer.html">

Polymer.FlattenedNodesObserver.getFlattenedNodes(node)は、指定したノードのフラット化されたノードのリストを返します。

Polymer.FlattenedNodesObserverクラスを使用して、フラット化されたノードリストの変更を記録(track)します。

this._observer = new Polymer.FlattenedNodesObserver(this.$.slot, (info) => {
  this._processNewNodes(info.addedNodes);
  this._processRemovedNodes(info.removedNodes);
});

FlattenedNodesObserverにはノードが追加または削除されたときに呼び出されるコールバックを渡します。コールバックは引数として、addedNodes配列とremovedNodes配列を持つObject(訳注:info)を一つ受け取ります。

このメソッドは、監視を停止するためのハンドルを返します。:

this._observer.disconnect();

FlattenedNodesObserverに関していくつか注意事項があります:

  • コールバックの引数には、単なるエレメントでなく、追加および削除されたノードのリストを指定します。エレメントだけに興味がある場合は、ノードのリストをフィルタリングすることができます。:

    info.addedNodes.filter(function(node) {
      return (node.nodeType === Node.ELEMENT_NODE)
    });
    
  • オブザーバーのハンドルには、ユニットテストに利用できるflushメソッドも用意されています。

Shadow Treeのカプセル化を守るために、いくつかのイベントはShadow DOMの境界で停止されます。

それ以外のバブリングイベントは、ツリーをバブルアップしながらリターゲティングされます。リターゲティングは、同じスコープ内のエレメントがリッスン対象のエレメントと同等に扱われるようにイベントのターゲットの調整を行います。

例えば、次のようなツリーがあるとします。:

<example-card>
  #shadow-root
    <div>
      <fancy-button>
        #shadow-root
          <img>

ユーザーがimageエレメントをクリックすると、クリックイベントはツリーをバブルアップします。:

  • imageエレメント自身のリスナーは、ターゲットとして<img>を受け取ります。
  • <fancy-button>のリスナーは、<fancy-button>をターゲットとして受け取ります。元のターゲットはShadow Rootの内側にあるからです。
  • <example-card>のShadow DOM内の<div>のリスナーも<fancy-button>をターゲットとして受け取ります。やはり、同じShadow DOMツリー内にあるためです。
  • <example-card>のリスナーは、<example-card>自身をターゲットとして受け取ります。

これらイベントには、イベントが通過するノードを配列にして返す、compositedPathメソッドを提供します。今回のケースでは、配列には次のものが含まれるでしょう。:

  • <img>エレメントそれ自体
  • <fancy-button>のShadow Root
  • <div>エレメント
  • <example-card>のShadow Root
  • <example-card>のすべての祖先(例えば、<body><html>documentおよびWindow

デフォルトでは、カスタムイベントはShadow DOMの境界を越えて伝播することはありません。カスタムイベントがShadow DOMの境界を越えてリターゲティングされるようにするには、composedフラグをtrueに設定してイベントを作成する必要があります。:

var event = new CustomEvent('my-event', {bubbles: true, composed: true});

Shadow Treeのイベントの詳細については、Web FundamentalsのShadow DOMに関する記事The Shadow DOM event modelを参照してください。

Shadow Tree内のスタイルは、Shadow Treeの内部にスコープされ、Shadow Tree外のエレメントに作用することはありません。Shadow Tree外のスタイルもまた、Shadow Tree内のセレクタにマッチすることはありません。しかし、colorのような継承可能なスタイルプロパティは、それにも関わらずホストからShadow Treeに下位へ継承されます。

<style>

  body { color: white; }

  .test { background-color: red; }

</style>

<styled-element>
  #shadow-root
    <style>
      div { background-color: blue; }
    </style>
    <div class="test">Test</div>

この例では、<div>の背景色は青になりますが、本来divセレクタはメインドキュメント内の.testセレクタよりもCSSの詳細度が低いはずです。これは、メインドキュメントのセレクタがShadow DOMの<div>にマッチしないためです。一方、ドキュメントのbodyに設定された白い文字色は<styled-element>に下位へ継承され、Shadow Root内部へ適用されます。

Shadow Tree内で指定したスタイルルールがShadow Tree外のエレメントにマッチするケースが一つだけあります。擬似クラス:hostまたは関数型擬似クラス:host()を使用して、hostエレメントに対してスタイルを定義することができるのです。

#shadow-root
  <style>
    /* custom elements default to display: inline */
    :host {
      display: block;
    }
    /* set a special background when the host element
       has the .warning class */
    :host(.warning) {
      background-color: red;
    }
  </style>

擬似エレメントセレクタ::slotted()を使用することで、スロットに割り当てられてたLight DOMの子に対してもスタイルを設定できます。例えば、::slotted(img)は、Shadow Tree内のスロットに割り当てられた全てのimageタグを選択します。

  #shadow-root
    <style>
      ::slotted(img) {
        border-radius: 100%;
      }
    </style>

詳細については、Web Fundamentalsの記事内のStylingを参照してください。

Shadow Tree外のCSSルールを使用して、Shadow Tree内のいかなるエレメントに対しても直接的にスタイルを適用することはできません。例外は、ツリーで下位に継承される一部のCSSプロパティ(colorやfontなど)です。Shadow Treeは、そのホストからCSSプロパティを継承します。

あなたが作成したエレメントをユーザーがカスタマイズするのを許可するには、CSSカスタムプロパティカスタムプロパティミックスインを利用して、特定のスタイルプロパティを公開します。カスタムプロパティは、エレメントにスタイリングAPIを追加する手段を提供します。

ポリフィルの制限事項:ポリフィルの提供するカスタムプロパティとミックスインを使用する場合、注意すべき多くの制限があります。詳細については、the Shady CSS README fileを参照してください。

カスタムプロパティは、CSSルールの中で代入可能な変数と考えることもできます:

:host {
  background-color: var(--my-theme-color);
}

これによって、ホストの背景色をカスタムプロパティ--my-theme-colorの値で設定します。あなたが作成したエレメントを利用するユーザーは誰でも、以下のようにより高いレベルでプロパティを設定できます。:

html {
  --my-theme-color: red;
}

カスタムプロパティはツリーを下って継承されるので、ドキュメントレベルで設定された値はShadow Tree内からアクセスすることができます。

代入値は利用者によってプロパティが設定されなかった場合に適用されるデフォルト値を含めることができます。:

:host {
  background-color: var(--my-theme-color, blue);
}

デフォルト値は別のvar()関数であっても構いません。:

background-color: var(--my-theme-color, var(--another-theme-color, blue));

カスタムプロパティミックスインは、カスタムプロパティの仕様をベースに構築された機能です。基本的に、ミックスインはオブジェクトの値をとるカスタムプロパティになります。:

html {
  --my-custom-mixin: {
    color: white;
    background-color: blue;
  }
}

コンポーネントは、@applyルールを使用することでルールセットをまとめてインポートしたりミックスインしたりできます。:

:host {
  @apply --my-custom-mixin;
}

@applyルールには、@applyが使用されたルールセット内に--my-custom-mixinの中身をインラインで挿入するのと同じ効果があります。

Shadow DOMはすべてのプラットフォームで利用できるわけではないため、PolymerではShady DOMとShady CSSのポリフィルをインストールして活用することができます。これらのポリフィルは、webcomponents-lite.jsポリフィルのバンドルに含まれています。

これらのポリフィルは、優れたパフォーマンス性を担保しながら、ネイティブのShadow DOMの合理的(reasonable)なエミュレーションを提供します。しかし、完全なポリフィルを提供できないShadow DOMの機能もいくつか存在します。ネイティブのShadow DOMが実装されていないブラウザをサポートする必要がある場合、これらの制限に注意する必要があります。また、Shady DOMの利用したアプリケーションをデバッグする際は、Shady DOMのポリフィルに関するいくらか詳細な理解があると役立ちます。

ポリフィルは、Shadow DOMをエミュレートするために複数の技術を組み合わせて利用しています。:

  • Shady DOM:Shady DOMはShadow Tree及びその子孫ツリーの論理的な分割を内部的に維持します。それによりLight DOMやShadow DOMに追加された子は正しくレンダリングされます。さらにShady DOMはネイティブのShadow DOM APIをエミュレートするために、影響を受けるエレメントのDOM APIにパッチを適用します。
  • Shady CSS: Shady CSSは、Shadow DOMの子にクラスを追加したり、スタイルルールの書き換えを行うことでスタイルのカプセル化を提供します。それによって、正しいスコープを適用します。

以降のセクションでは、各ポリフィルについて掘り下げて考察しています。

ネイティブShadow DOMをサポートしていないブラウザでは、ドキュメント及びその子孫ツリーだけがレンダリングされます。

フラット化されたツリーにおけるShadow DOMのレンダリングをエミュレートするのに、Shady DOMポリフィルは、論理的にツリーを分割しながら、仮想的な(virtual)childrenshadowRootプロパティを維持する必要があります。各ホストエレメントの実際のchildren(ブラウザに表示される子孫ツリー)は、事前にフラット化されたShadow DOMとLight DOMの子のツリーです。開発者ツールを使用して表示されるツリーは、レンダーツリーのように見えますが、論理的なツリーではありません。

ポリフィルを使用した場合、ブラウザのツリーのビューにslotエレメントは現れません。したがって、ネイティブのShadow DOMと異なり、スロットがイベントバブリングに加わることもありません。

ポリフィルはShadow DOMに影響を受けるノードの既存のDOM APIにパッチを適用します。影響を受けるノードは、Shadow Tree内のノード、Shadow TreeをホストするノードやShadowホストのLight DOMの子ノードです。例えば、あるノード上で、Shadow Rootを渡してappnedChildメソッドを呼び出すと、ポリフィルはLight DOMの子の仮想ツリーに子を追加し、レンダリングツリーのどこに表示すべきか計算した後で、それを実際の子孫ツリーのあるべき場所へ追加します。 (訳補:説明だけでは分かりにくいので、READ MEに記載されたサンプルコードを参照してください。)

詳細には、Shady DOM polyfill READMEを参照してください。

Shady CSSポリフィルはShadow DOMのスタイルのカプセル化をエミュレートするだけでなく、CSSカスタムプロパティとカスタムプロパティミックスインのエミュレーションも提供します。

カプセル化をエミュレートするために、Shady CSSポリフィルは、Shady DOMツリー内のエレメントにクラスを追加します。また、エレメントのテンプレート内で定義されたスタイルルールを書き換えてそのエレメントだけに適用されるようにします。

Shady CSSは、ドキュメントレベル(document-level)のスタイルシートに定義されたスタイルルールについては書き換えを行いません。つまり、ドキュメントレベルで定義したスタイルがShadow Treeにリークする可能性があるこということです。ただし、<custom-style>というCustom Elementが提供されており、エレメントの外側であってもポリフィルが適用されたスタイルを記述できます。これには、カスタムCSSプロパティのサポートや、スタイルがShadow Treeへのリークするのを防ぐために行うルールの書き換えも含まれます。

<custom-style>
  <style>
    /* Set CSS custom properties */
    html { --my-theme-color: blue; }
    /* Document-level rules in a custom-style don't leak into shady DOM trees */
    .warning { color: red; }
  </style>
</custom-style>

詳細については、Shady CSS polyfill READMEを参照してください。

さらなる理解のために: