はじめに
ウェブ コンポーネントは、以下を可能にする最先端の標準規格です。
- ウィジェットを作成できるようにする
- …確実に再利用できる
- …また、コンポーネントの次のバージョンで内部の実装の詳細が変更されても、ページが破損することはありません。
これは、HTML / JavaScript を使用するタイミングと、ウェブ コンポーネントを使用するタイミングを決定する必要があるということでしょうか。いいえ。HTML と JavaScript を使用すると、インタラクティブなビジュアル要素を作成できます。ウィジェットはインタラクティブな視覚的要素です。ウィジェットを開発する際は、HTML と JavaScript のスキルを活用することをおすすめします。ウェブ コンポーネント標準は、これを実現できるように設計されています。
しかし、HTML と JavaScript で構築されたウィジェットを使いにくくする根本的な問題があります。ウィジェット内の DOM ツリーは、ページの他の部分からカプセル化されていません。このカプセル化の欠如は、ドキュメントのスタイルシートがウィジェット内のパーツに誤って適用される可能性があることを意味します。JavaScript がウィジェット内のパーツを誤って変更する可能性があります。ID がウィジェット内の ID と重複する可能性があります。
ウェブ コンポーネントは、次の 3 つの部分で構成されています。
Shadow DOM は、DOM ツリーのカプセル化の問題に対処します。ウェブ コンポーネントの 4 つの部分は連携して動作するように設計されていますが、ウェブ コンポーネントのどの部分を使用するかを選択することもできます。このチュートリアルでは、Shadow DOM の使用方法について説明します。
Hello, Shadow World
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 を問い合わせた場合、返されるのは「こんにちは、影の世界!」ではなく「Hello, world!」です。これは、シャドウルートの下の DOM サブツリーがカプセル化されているためです。
コンテンツとプレゼンテーションの分離
次に、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 からアクセスすることは可能です。ここでは、シャドウルートにデータを入力するために、この処理を行います。
<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 を使用することで、ドキュメントから名前タグの表示の詳細を隠したことを示しています。プレゼンテーションの詳細は Shadow DOM にカプセル化されます。
ステップ 2: コンテンツとプレゼンテーションを分離する
このネームタグは、ページからプレゼンテーションの詳細を隠しますが、実際にはプレゼンテーションをコンテンツから分離していません。コンテンツ(名前「Bob」)はページ内にありますが、レンダリングされる名前は、シャドウルートにコピーした名前です。名札の名前を変更する場合は、2 か所で変更する必要があり、同期がずれる可能性があります。
HTML 要素はコンポーズ可能です。たとえば、テーブル内にボタンを配置できます。ここで必要なのはコンポジションです。名札は、赤い背景、「Hi!」というテキスト、名札に記載されているコンテンツのコンポジションである必要があります。
コンポーネントの作成者は、<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: 利益
コンテンツと表示を分離することで、コンテンツを操作するコードを簡素化できます。名前タグの例では、そのコードは、複数の <div> ではなく、1 つの <div> を含む単純な構造を処理するだけで済みます。
これで、プレゼンテーションを変更しても、コードを変更する必要はなくなりました。
たとえば、名前タグをローカライズするとします。これは依然として名前タグであるため、ドキュメント内のセマンティック コンテンツは変更されません。
<div id="nameTag">Bob</div>
シャドウルートのセットアップ コードは同じままです。シャドウルートに配置されるものだけが変更されます。
<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! 「私の名前は」のように、日本語で「と申します」と表記されます。表示される名前を更新するという観点からは、この区別は意味的に意味がないため、名前更新コードはこの詳細を知る必要はありません。
追加の実習: 高度な投影
上記の例では、<content> 要素がシャドウホストからすべてのコンテンツをチェリーピックしています。select 属性を使用すると、コンテンツ要素が投影する内容を制御できます。複数のコンテンツ要素を使用することもできます。
たとえば、次のようなドキュメントがあるとします。
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
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 のメールアドレスは何回表示され、何色で表示されていますか?
答えは、Bob のメールアドレスが 1 回表示され、黄色になっている、です。
その理由は、Shadow DOM をハックする人なら知っているように、実際に画面にレンダリングされるツリーを構築することは、巨大なパーティーのようなものだからです。content 要素は、ドキュメントのコンテンツをバックステージの Shadow DOM レンダリング パーティに招待する招待状です。これらの招待状は順番に配信されます。招待状を受け取るユーザーは、招待状の宛先(select 属性)によって異なります。コンテンツは、招待されると常に招待を承諾し(承諾しない人はいないでしょう)、送信されます。そのアドレスに再度招待状が送信されても、誰もいないため、パーティーには参加しません。
上記の例では、<div class="email"> は div セレクタと .email セレクタの両方に一致しますが、div セレクタを持つコンテンツ要素がドキュメント内で先にあるため、<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 を作成します。ユーザーがカレンダーの日付をクリックすると、コンポーネントは 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 の基礎をマスターしました。Shadow DOM では、他にもさまざまなことができます。たとえば、1 つのシャドウホストで複数のシャドウを使用したり、カプセル化のためにネストされたシャドウを使用したり、モデル駆動型ビュー(MDV)と Shadow DOM を使用してページを設計したりできます。また、ウェブ コンポーネントは Shadow DOM だけではありません。
これらについては、後の投稿で説明します。