Shadow DOM 301

高度なコンセプトと DOM API

Eric Bidelman

この記事では、Shadow DOM でできることについて詳しく説明します。これは、Shadow DOM 101Shadow DOM 201 で説明されているコンセプトに基づいています。

複数のシャドウルートの使用

パーティーを開催する場合、全員が同じ部屋に詰め込まれると息苦しくなります。複数の部屋にグループを分散するオプションが必要な場合。Shadow DOM をホストする要素もこれを行うことができます。つまり、一度に複数の Shadow ルートをホストできます。

複数のシャドールートをホストに接続しようとするとどうなるかを見てみましょう。

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

シャドウ ツリーをすでに接続しているにもかかわらず、「Root 2 FTW」とレンダリングされます。これは、ホストに最後に追加されたシャドウツリーが優先されるためです。レンダリングに関しては、LIFO スタックです。この動作は DevTools で検証されます。

最後のシャドウだけがレンダリング パーティに招待される場合、複数のシャドウを使用する意味はありません。シャドウの挿入ポイントを入力します。

シャドウ挿入ポイント

「シャドウ挿入ポイント」(<shadow>)は、プレースホルダであるという点で、通常の挿入ポイント<content>)に似ています。ただし、ホストのコンテンツのプレースホルダではなく、他のシャドー ツリーのホストです。Shadow DOM のインセプションだ!

ご想像のとおり、深く掘り下げていくほど複雑になります。このため、複数の <shadow> 要素が使用されている場合の動作は、仕様で明確に定められています。

元の例に戻ると、最初のシャドウ root1 は招待リストから除外されています。<shadow> 挿入ポイントを追加すると、再び表示されます。

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

この例には興味深い点がいくつかあります。

  1. 「Root 2 FTW」は引き続き「Root 1 FTW」の上にレンダリングされます。これは、<shadow> 挿入ポイントの配置が原因です。逆の順序にするには、挿入ポイントを root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>'; に移動します。
  2. root1 に <content> 挿入ポイントが追加されています。これにより、レンダリング時にテキストノード「Light DOM」が使用されます。

<shadow> でレンダリングされる内容

<shadow> でレンダリングされている古いシャドウ ツリーを知っておくと便利な場合があります。このツリーへの参照は .olderShadowRoot を介して取得できます。

**root2.olderShadowRoot** === root1 //true

ホストのシャドウルートを取得する

要素が Shadow DOM をホストしている場合は、.shadowRoot を使用してその最新の Shadow ルートにアクセスできます。

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

ユーザーが影に侵入することを懸念している場合は、.shadowRoot を null に再定義します。

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

少し手間がかかりますが、機能します。結局のところ、Shadow DOM はセキュリティ機能として設計されていないことを覚えておくことが重要です。コンテンツの完全な分離には使用しないでください。

JS で Shadow DOM を構築する

JS で DOM を構築したい場合、HTMLContentElementHTMLShadowElement にインターフェースがあります。

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

この例は、前のセクションの例とほぼ同じです。唯一の違いは、select を使用して、新しく追加された <span> を取得していることです。

挿入ポイントの操作

ホスト要素から選択され、シャドウツリーに分散されるノードは、ドラムロール、分散ノードと呼ばれます。挿入ポイントから移動される際に、シャドー境界を越えることは許可されます。

挿入ポイントについて概念的に奇妙な点は、DOM を物理的に移動しない点です。ホストの��ードはそのまま残ります。挿入ポイントは、ノードをホストからシャドウツリーに再投影するだけです。プレゼンテーション / レンダリングに関するものです。「これらのノードをここに移動」「これらのノードをこの場所でレンダリング」などです。

例:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

これにより、h2 は Shadow DOM の子ではありません。これに関連して、もう 1 つ注意点があります。

Element.getDistributedNodes()

<content> に移動することはできませんが、.getDistributedNodes() API を使用すると、挿入ポイントで分散ノードに対してクエリを実行できます。

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

.getDistributedNodes() と同様に、ノードの .getDestinationInsertionPoints() を呼び出すと、ノードがどの挿入ポイントに分散されているかを確認できます。

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

ツール: Shadow DOM Visualizer

Shadow DOM であるブラック マジックを理解するのは困難です。初めて理解しようとしたときのことを覚えています。

Shadow DOM のレンダリングの仕組みを可視化するために、d3.js を使用するツールを作成しました。左側の両方のマークアップ ボックスは編集可能です。独自のマークアップを貼り付けて、動作を試したり、挿入ポイントがホストノードをシャドウ ツリーにスウィズルしたりできます。

Shadow DOM ビジュアライザー
Shadow DOM Visualizer を起動する

ご利用とご感想をお待ちしております。

イベントモデル

イベントによっては Shadow 境界を越えるものもあれば、越えないものもあります。イベントが境界を越える場合、シャドールートの上限境界が提供するカプセル化を維持するために、イベント ターゲットが調整されます。つまり、イベントは Shadow DOM の内部要素ではなく、ホスト要素から発生したものであるかのようにリターゲティングされます

再生アクション 1

  • これは興味深い問題です。ホスト要素(<div data-host>)から青色のノードに mouseout が表示されます。分散ノードですが、ShadowDOM ではなくホスト内にあります。マウスを黄色の部分にさらに移動すると、青いノードに mouseout が表示されます。

Play Action 2

  • ホストに 1 つの mouseout が表示されます(一番最後)。通常、黄色のブロックすべてに対して mouseout イベントがトリガーされます。ただし、この場合、これらの要素は Shadow DOM の内部にあり、イベントは上限境界を越えてバブルアップしません。

再生アクション 3

  • 入力をクリックしても、focusin は入力ではなくホストノード自体に表示されます。再び標的にされた!

常に停止されるイベント

次のイベントはシャドウ境界を越えることはありません。

  • abort
  • エラー
  • 選択
  • 変更
  • load
  • リセット
  • resize
  • scroll
  • selectstart

まとめ

Shadow DOM が驚くほど強力であることにご同意いただけたら幸いです。今回初めて、<iframe> やその他の古い手法の追加なしで適切なカプセル化を実現しました。

Shadow DOM は確かに複雑ですが、ウェブ プラットフォームに追加する価値のあるものです。しばらく使ってみてください。学びましょう。自由に質問してください。

詳しくは、Dominic の入門記事「Shadow DOM 101」と、私の記事「Shadow DOM 201: CSS とスタイル設定」をご覧ください。