Shadow DOM 101

HTML5 Rocks

はじめに

Web Components は下記のような特徴を持つ最先端のオープンスタンダードです:

  1. ウィジェットが作成可能
  2. 再利用が可能
  3. コンポーネントが実装を変更しても、ページを壊さない

これは HTML/JavaScript と Web Components の両方が使えなければならないことを意味しているのかって? いいえ。HTML と JavaScript は、インタラクティブなビジュアルを作るためのものです。ウィジェットはインタラクティブなビジュアルと言えます。ウィジェットを作るに当たって、あなたは得意な HTML と JavaScript のスキルを利用することができます。Web Components はそれを可能にするオープンスタンダードなのです。

ウィジェットを作るのに別のテクノロジーに乗り換えるなんてナンセンスです。ウィジェットを作るのに <canvas> を使わなければならないとしたら悲しいですよね。ペイント内容を変更してもページが壊れない等、信頼に足るものに間違いありませんが、アクセシビリティや検索エンジンへのインデキシング、構成、解像度の独立性は犠牲になってしまいます。

ただ、HTML や JavaScript を使ってウィジェットを作るには、根本的な問題があります:ウィジェット内の DOM ツリーが、ページの他の部分から切り離され (カプセル化され) ていない、という点です。そのため、ウィジェット内の一部にページのスタイルシートが誤って適用されてしまったり、id が被ってしまったりといった事態が発生します。

カプセル化されていないことの弊害は、ライブラリをアップデートしたり、ウィジェットの DOM やスタイル、スクリプトを変更することが、予期しない問題を起こすことです。

Web Components は 4 つのテクノロジーで構成されます:

  1. Templates
  2. Shadow DOM
  3. Custom Elements
  4. HTML Imports

Shadow DOM は DOM ツリーのカプセル化問題を解決します。Web Components の 4 つのテクノロジーは組み合わさって役立つようにデザインされていますが、一部のみを用いることもできます。このチュートリアルでは、どうやって Shadow DOM を使うかを述べていきます。

Shadow DOM は Chrome 35 以降で利用可能です。

Hello, Shadow World

Shadow DOM を使うことで、要素は、Shadow Root と呼ばれる、新しい種類のノードを関連付けることができます。Shadow Root が関連付けられた要素は、Shadow Host と呼ばれます。Shadow Host の内容は表示されません。その代わり、Shadow Root の内容が表示されます。

例えば、下記のようなマークアップがあったとします:

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

ここであなたのページは

の代わりに、このように見えます

それだけではありません。JavaScript で button.textContent を調べてみると、"こんにちは、影の世界!" が見えます。これは Shadow Root の DOM サブツリーがカプセル化されているためです。

コンテンツと見た目を分離する

Shadow DOM を使って、コンテンツと見た目を分ける方法について見て行きましょう。下記のようなネームタグがあるとします:

Hi! My name is
Bob

マークアップはこんな感じ。今ならこう書くと思います。これは、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">
<style>
.outer {
  border: 2px solid brown;

  … 同上 …

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

この時点では、"Bob" のみが表示されています。<template> 要素の中に見た目上必要な DOM要素を動かしたので、それが表示されない代わりに、JavaScript からアクセス可能になりました。ここで Shadow Root を埋めることができます:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
</script>
Template は Shadow DOM 同様、新しいオープンスタンダードで、すでに様々なブラウザで利用可能です。Shadow Root の内容は他にも innerHTMLappendChildgetElementById 等を使って埋めることができます。この記事では Shadow DOM について述べるため、template 要素については深く解説しません。詳しくは HTML で利用可能になった Template タグ をご覧ください。

Shadow Root が作れたら、ネームタグを再度表示してみましょう。右クリックしてインスペクトしてみると、セマンティックなマークアップが見えるはずです:

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

Shadow DOM を使うことで、ドキュメントからネームタグの見た目だけを隠すことができたことがお分かりでしょうか? 見た目は Shadow DOM にカプセル化された、ということです。

ステップ 2: コンテンツと見た目を分離する

ネームタグの見た目をページから切り離すことに成功しましたが、コンテンツから切り離したとはまだ言えません。なぜなら、確かにページ内にコンテンツ ("Bob" という名前) は存在していますが、その名前は Shadow Root にコピーしたものだからです。ネームタグの名前を変更しようと思ったら、2 箇所を変更する必要があり、これは理想的ではありません。

HTML 要素は構成可能なもの、例えば表の中にボタンを置いたりすることができるものです。この構成できる、ということがここでは必要とされます:ネームタグは赤い背景、"Hi!" というテキスト、そしてネームタグに乗るコンテンツでなければなりません。

コンポーネント作者は、<content> という新しい要素を使って、ウィジェットがどう動くかを定義することができます。これはウィジェットの見た目に挿入ポイントを作り、その挿入ポイントが Shadow Host からコンテンツを引っ張ってきます。

Shadow DOM のマークアップを下記のように変更してみましょう:

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

ネームタグが表示されると、Shadow Host のコンテンツが <content> 要素の位置にプロジェクトされます。

名前が一箇所になったので、ドキュメントの構造はシンプルになりました。ユーザーの名前を変更したいと思ったら、下記のようにすればいいだけです:

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

たったこれだけ。ネームタグのコンテンツを <content> の位置にプロジェクトしているので、名前はブラウザによって自動的に更新されます。

実際の Shadow DOM を使った例です:

Bob

これでコンテンツと見た目の分離はできました。コンテンツはドキュメントに、見た目は Shadow DOM にあります。 内容は、表示の際にブラウザが自動的に同期してくれます。

ステップ 3: できあがり

コンテンツと見た目を分けることで、コンテンツを変更するコードは単純になりました。ネームタグの例で言うと、ひとつの <div> タグのみを相手にすれば良いことになります。

見た目を変更する際、コードを変更する必要はありません!

例えば、ネームタグをローカライズしたいとします。ネームタグには変わりないので、ドキュメント上のセマンティックなコンテンツに変更は不要です:

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

Shadow Root をセットアップするコードにも変更はありません。Shadow Root に置かれるものだけを変更します:

<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>

これで日本語のネームタグが完成しました:

Bob

背景画像 by Mike Dowman, Creative Commons license.

これは今日のウェブにとって、大きな進化です。なぜなら、名前の変更が、シンプルで一貫したコンポーネントに依存できるからです。名前を変更するコードが、表示に用いられる構造を知る必要はありません。 何が表示されるか考えた場合、英語なら名前は 2 番目 ("Hi! My name is" の後) ですが、日本語なら最初 ("と申します" の前) に現れます。これらの違いは、表示される名前を変更する、という観点では、セマンティック上意味を持たないので、名前を変更するコードがこれらの詳細を知っている必要はないのです。

Extra Credit: 上級者向けプロジェクション

上記の例で、 <content> 要素は Shadow Host からコンテンツを引っ張ってきました。select 属性を使うことで、content 要素の表示内容をコントロールすることができます。複数の content 要素を利用することもできます。

例えば下記のようなドキュメントがあったとします:

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

Shadow Root は 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>

注意: select はホストノードの直下にある要素しか選択することができません。言い換えると、2 番目以降の子孫 (例 select="table tr") を選択することはできません。

<div class="email"> 要素は <content select="div"><content select=".email"> の両方と突合されます。Bob のメールアドレスは何度表示され、何色になるでしょうか?

Bob
B. Love

答えは一度、そして黄色です。

Shadow DOM をハックしたことのある方ならご存知の通り、スクリーンに実際に表示されるもののツリー構築は、パーティーのようなものです。content 要素は、ドキュメントから Shadow DOM レンダリングパーティーに送られた招待状のようなものです。 招待状は順番に、誰に送られたか (これが select 属性) に応じて届けられます。一度招待されたコンテンツは、招待状を必ず受け取り (受け取らないなんてことあるでしょうか!) 会場に向かいます。同じ住所に再度招待状が送られたとしても、もう誰もいませんので、パーティーには誰も参加しません。

上記の例では、<div class="email">div セレクターにも .email セレクターにも一致します。しかし、div セレクターの content 要素が先に来てしまい、<div class="email"> は黄色パーティーに参加するため、青色パーティーには誰も来ません。

どのパーティーにも招待されなかった場合、表示されることはありません。これが最初の例でご覧頂いた "Hello, world" テキストに起こったことです。これはちょっと変わったレンダリングを行いたい場合に役立つかもしれません:ページ内のスクリプトからアクセス可能でありながら、実際は表示はされず、JavaScript を使って Shadow DOM 内の全く別のレンダリングモデルに接続したいドキュメントに、セマンティックモデルを記述した場合などです。

例えば、HTML には素敵なカレンダー機能があります。<input type="date"> と書くと、かっこいいデートピッカーが表示されます。しかし、複数日を選択したい場合はどうでしょう?こういう風にしてみましょう:

<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 を使って、日付の範囲をハイライトしたかっこいいカレンダーを表示できるようにしましょう。 ユーザーがカレンダー内のある日をクリックしたら、コンポーネントが startDateendDate を更新するようにします。ユーザーがフォームを送ると、input 要素の値が送信されるようにします。

表示されないにも関わらずドキュメントに label を入れたのはなぜでしょう? それは、Shdow DOM をサポートしないブラウザでフォームを見た場合、見た目がそれほどかっこよくないだけで、フォームの利用には問題がないからです。ユーザーが目にするものはこんな感じ:


Shadow DOM 101 は免許皆伝!

これで Shadow DOM の基礎 — 101 は終了です。Shadow DOM を使って、ひとつの Shadow Host 上で複数の Shadow を使ったり、カプセル化された Shadow をネストさせたり、MDV (Model Driven View) と Shadow DOM を組み合わせてデザインするなど、さらに高度なことができるはずです。もちろん、Web Components の魅力は Shadow DOM だけではありません。

これらについては、別のポストで解説していきます。ひとまず、Google+ の Web Components をフォローして下さい。

このチュートリアルの初期版にコメントをくれた Eric Bidelman, Darin Fisher, Dimitri Glazkov, Alex Komoroske, Alex Russell, Paul Irish に感謝します。

Comments

0