LoginSignup
4
4

More than 5 years have passed since last update.

Polymerドキュメント(日本語) Shadow DOM & styling/Shadow DOM concepts 〜Shadow DOMのコンセプト〜

Last updated at Posted at 2017-04-26

目次へ移動

翻訳ドキュメントの管理ページ

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の他の部分に対してスコープがリークすることはありません。

Shadow 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">Title</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 id="icon">
      <img src="note.png">
    </slot>
    <slot></slot>
</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>

マルチレベルの割り当て(distribution)

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)に割り当てられます。

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

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

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

スロットのAPI

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を参照してください。

子の追加と削除の監視(observe)

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>のリスナーは、自分自身をターゲットとして受け取ります。

これらイベントには、イベントが通過するノードを配列にして返す、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 DOMのスタイリング

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>にマッチしないためです。一方、ドキュメント本体に設定された白い文字色は<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>

擬似要素セレクタ::slotte()を使用することで、スロットに割り当てられてた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のポリフィル

Shadow DOMはすべてのプラットフォームで利用できるわけではないため、PolymerではShady DOMとShadow 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の子にクラスを追加したり、スタイルルールの書き換えを行うことでスタイルのカプセル化を提供します。それによって、正しいスコープを適用します。

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

Shady 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に記載されたサンプルコードを引用して紹介します。)

<div id="host"></div>
<script>
  host.attachShadow({mode: 'open'});
  host.shadowRoot.appendChild(document.createElement('a'));
</script>

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

Shady CSSポリフィル

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

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

Shady CSSは、ドキュメントレベル(document-level)のスタイルシートに定義されたスタイルルールについては書き換えません。つまり、ドキュメントレベルで定義したスタイルがShadow Treeにリークする可能性があるこということです。ただし、カスタム要素には<custom-style>が用意されており、要素の外側でもポリフィルが適用されたスタイルを記述できます。これには、カスタム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を参照してください。

参考情報

さらなる理解のために:

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4