Shadow DOM 101

はじめに

Web Components は最先端の規格のことで、次のような特長があります。

  1. ウィジェットの作成を可能にする
  2. ...確実に再利用できる
  3. また、次のバージョンのコンポーネントで内部実装の詳細が変更されても、ページが分割されることはありません。

つまり、HTML/JavaScript を使用するタイミングと Web Components を使用するタイミングを決める必要があるということですか?いいえ。HTML と JavaScript はインタラクティブで 視覚に訴えるものにしますウィジェットはインタラクティブな視覚的要素です。ウィジェットを開発する際は、HTML と JavaScript のスキルを活用すると効果的です。ウェブ コンポーネントの標準は、それを可能にするように設計されています。

しかし、HTML と JavaScript で構築されたウィジェットが使いにくいという根本的な問題があります。それは、ウィジェット内の DOM ツリーがページの他の部分からカプセル化されていないということです。このようにカプセル化されていないと、ドキュメントのスタイルシートが誤ってウィジェット内のパーツに適用される、JavaScript が誤ってウィジェット内のパーツを変更する、ID がウィジェット内の ID と重複するといった場合があります。

Web Components は次の 3 つの部分で構成されています。

  1. テンプレート
  2. Shadow DOM
  3. カスタム要素

Shadow DOM は、DOM ツリーのカプセル化問題に対処します。Web Components の 4 つの部分は連携して動作するように設計されていますが、Web Components のどの部分を使用するかを選ぶこともできます。このチュートリアルでは、Shadow DOM の使用方法について説明します。

ハロー、シャドウ ワールド

Shadow DOM を使用すると、要素に関連付けられた新しい種類のノードを取得できます。この新しい種類のノードはシャドウルートと呼ばれます。シャドウルートが関連付けられている要素は、シャドウホストと呼ばれます。シャドウホストのコンテンツはレンダリングされません。代わりに、シャドウルートのコンテンツがレンダリングされます。

たとえば、次のようなマークアップがあるとします。

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

ページの例

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

さらに、ページの JavaScript にボタンの textContent が何であるかを尋ねても、シャドウルートの下の DOM サブツリーがカプセル化されているため、「こんにちは、映画の世界!」ではなく「Hello, world!」と出力されます。

コンテンツとプレゼンテーションの分離

次に、Shadow DOM を使用してコンテンツをプレゼンテーションから分離する方法について説明します。次のような名前タグがあるとします。

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

マークアップは次のとおりです。今日はこう書く予定です。Shadow DOM は使用しません。

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

DOM ツリーにはカプセル化がないため、ネームサーバーの構造全体がドキュメントに公開されます。ページ上の他の要素がスタイルやスクリプトに同じクラス名を誤って使用すると、うまくいかなくなります。

大変な事態を避けることができます。

ステップ 1: プレゼンテーションの詳細を非表示にする

セマンティックな観点では、おそらく次のことのみを考慮します。

  • 名前タグ。
  • 名前は「Bob」です。

まず、求めている真の意味に近いマークアップを記述します。

<div id="nameTag">Bob</div>

次に、表示に使用されるすべてのスタイルと div を <template> 要素に含めます。

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

この時点で「Bob」のみがレンダリングされます。表示用 DOM 要素は <template> 要素内を移動したため、レンダリングされませんが、JavaScript からアクセスできます。それを実行して、Shadow ルートにデータを入力します。

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

シャドウルートを設定したので、名前タグが再びレンダリングされます。名前タグを右クリックして要素を調べると、わかりやすいセマンティック マークアップであることがわかります。

<div id="nameTag">Bob</div>

これは、Shadow DOM を使用して、name タグの表示詳細がドキュメントから非表示になっていることを示しています。プレゼンテーションの詳細は Shadow DOM にカプセル化されます。

ステップ 2: コンテンツとプレゼンテーションを分離する

name タグにより、プレゼンテーションの詳細はページから非表示になりましたが、実際にはプレゼンテーションとコンテンツが分離されません。これは、コンテンツ(「Bob」という名前)がページ内にあっても、レンダリングされる名前がシャドウルートにコピーした名前であるためです。名前タグの名前を変更する場合は、2 か所で変更する必要があり、それらが同期しなくなる可能性があります。

HTML 要素は構成単位です。たとえば、表内にボタンを配置できます。ここで必要なのはコンポジションです。名前タグは、赤い背景、「Hi!」のテキスト、name タグの内容を組み合わせたものである必要があります。

コンポーネント作成者は、<content> という新しい要素を使用して、ウィジェットでコンポジションがどのように機能するかを定義します。これにより、ウィジェットの表示内に挿入ポイントが作成され、その挿入ポイントにより、シャドウホストからコンテンツが選択され、その時点で表示されます。

Shadow DOM のマークアップを次のように変更すると、

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

名前タグがレンダリングされると、シャドウホストのコンテンツが <content> 要素が表示される場所に投影されます。

名前が 1 つの場所(ドキュメント)内にのみ存在するため、ドキュメントの構造がよりシンプルになりました。ページでユーザー名を更新する必要がある場合は 次のように記述します

document.querySelector('#nameTag').textContent = 'Shellie';

以上です。名前タグのレンダリングはブラウザによって自動的に更新されます。これは、<content> でネームサーバーのコンテンツを所定の場所に投影しているためです。

<div id="ex2b">

これで、コンテンツとプレゼンテーションが分離されました。コンテンツはドキュメント内にあり、プレゼンテーションは Shadow DOM 内にあります。 これらは、何かをレンダリングするタイミングになるとブラウザによって自動的に同期されます。

ステップ 3: 利益

コンテンツと表示を分離することで、コンテンツを操作するコードを簡素化できます。たとえば、name タグの例では、複数のコードではなく 1 つの <div> を含む単純な構造を扱うだけで済みます。

プレゼンテーションを変更する場合、コードを変更する必要はありません。

たとえば、名前タグをローカライズするとします。これはまだ名前タグであるため、ドキュメント内のセマンティック コンテンツは変更されません。

<div id="nameTag">Bob</div>

シャドウルートのセットアップ コードに変更はありません。Shadow ルートに書き込まれるものだけが変更されます。

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

名前更新コードはシンプルで一貫性のあるコンポーネントの構造に依存するため、これは現在のウェブの状況を大きく改善しています。名前更新コードがレンダリングに使用される構造を認識する必要はありません。レンダリングの内容を考慮すると 英語では名前が 2 番目に表示されます(「Hi!私の名前は)ですが、最初は日本語(「「」の前」)です。この区別は、表示されている名前を更新するという観点では意味的に無意味であるため、名前更新コードはその詳細を認識する必要がありません。

追加の実習: Advanced Projection

上記の例では、<content> 要素が Shadow ホストからすべてのコンテンツをチェリーピックしています。select 属性を使用すると、どのコンテンツ要素をプロジェクトの対象にするかを制御できます。複数のコンテンツ要素を使用することもできます。

たとえば、次のようなドキュメントがあるとします。

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

Shadow ルートは、CSS セレクタを使用して特定のコンテンツを選択します。

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

<div class="email"> 要素は、<content select="div"> 要素と <content select=".email"> 要素の両方と照合されます。メールアドレスは何回表示され 何色で表示されますか?

答えは、Bob のメールアドレスが一度だけ表示され、黄色で表示されていることです。

その理由は、Shadow DOM をハッキングした人々ならご存じのとおり、実際に画面上にレンダリングされるツリーを構築することは、大規模なパーティーのようなものだからです。コンテンツ要素は、ドキュメントのコンテンツをバックステージの Shadow DOM レンダリング パーティに送信するためのインビテーションです。これらの招待状は順番に配信されます。招待状を受け取るのは、宛先(つまり select 属性)によって異なります。コンテンツが一度招待されると、常にその招待を承諾すれば拒否されます。そのアドレスにその後の招待状が再度送信された場合 誰も家にいないため パーティーにも届きません

上記の例では、<div class="email">div セレクタと .email セレクタの両方に一致しますが、div セレクタを持つコンテンツ要素がドキュメントの先頭にあるため、<div class="email"> はイエロー パーティに移動し、ブルーパーティには誰も来ません。(悲しみは会社を愛しているので、それがこんなに青い理由だからかもしれません)。

どの当事者にも招待されていないものは、まったくレンダリングされません。これが、最初の例の「Hello, world」テキストについてです。これは、まったく異なるレンダリングを行う場合に役立ちます。ドキュメントにセマンティック モデルを記述します。これはページのスクリプトがアクセスできるものですが、レンダリングのためには非表示にし、JavaScript を使用して Shadow DOM 内のまったく異なるレンダリング モデルに接続します。

たとえば、HTML には日付選択ツールがあります。<input type="date"> と入力すると、便利なポップアップ カレンダーが表示されます。しかし、デザートの島での休暇(Red Vines で作られたハンモックなど)の日付の範囲をユーザーに選択してもらう場合はどうすればよいでしょうか。次のようにドキュメントを設定します。

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

テーブルを使用して日付の範囲などをハイライト表示する 滑らかなカレンダーを作成する Shadow DOM を作成しますユーザーがカレンダーの日付をクリックすると、コンポーネントは startDate と endDate の入力の状態を更新します。ユーザーがフォームを送信すると、これらの入力要素の値が送信されます。

レンダリングされないラベルをドキュメントに含めたのはなぜですか?その理由は、ユーザーが Shadow DOM をサポートしていないブラウザでフォームを表示しても、フォームがまだ使用可能で、見た目が良くないためです。次のような画面が表示されます。

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Shadow DOM 101 に合格しました

以上が Shadow DOM の基本です。これで Shadow DOM 101 は完了です。Shadow DOM を使用すると、さらに多くのことができます。たとえば、1 つの Shadow ホストで複数の Shadow を使用したり、カプセル化にネストされた Shadow を使用したり、Model-Driven View(MDV)と Shadow DOM を使用してページを設計したりできます。ウェブコンポーネントは単なる Shadow DOM ではありません

これについては、別の投稿で説明します。