テンプレートとスロットの使用
この記事では、<template> と <slot> 要素を使用して柔軟なテンプレートを作成し、それをウェブコンポーネントのシャドウ DOM を表示するために使用する方法について説明します。
テンプレートの真実
ウェブページで同じマークアップ構造を繰り返し再利用する必要がある場合、同じ構造を何度も繰り返すよりも、何らかのテンプレートを使用する方が理にかなっています。
これは以前から可能でしたが、HTML の <template> 要素によって、かなり容易になりました(最近のブラウザーはよく対応しています)。
この要素とその内容は DOM 内で描画されませんが、JavaScript を使って参照することは可能です。
簡単な例を見てみましょう。
<template id="custom-paragraph">
<p>My paragraph</p>
</template>
これはページ上に表示されず、以下のようなコードで JavaScript で参照を取得し、 DOM に追加することで表示されます。
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);
つまらない例ですが、すでに有用性は見えてきたでしょう。
ウェブコンポーネントを用いたテンプレートの使用
テンプレートはそれ自体でも便利ですが、ウェブコンポーネントと組み合わせるとさらに効果的です。
テンプレートをシャドウ DOM の内容として使用するウェブコンポーネントを定義してみましょう。
同様に <my-paragraph> と呼ぶことにします。
customElements.define(
"my-paragraph",
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById("custom-paragraph");
let templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(templateContent.cloneNode(true));
}
},
);
ここで、テンプレートの内容を使用するために Node.cloneNode() メソッドを使用して複製したものをシャドウルートに追加していることに注意してください。
また、その内容をシャドウ DOM に追加しているため、テンプレート内のスタイル情報を <style> 要素に含めることができ、それがカスタム要素内にカプセル化されます。
これは、単に標準 DOM に追加しただけでは機能しません。
したがって、例えば次のようにすると、
<template id="custom-paragraph">
<style>
p {
color: white;
background-color: #666666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
HTML 文書に次のように追加するだけで利用できるようになりました。
<my-paragraph></my-paragraph>
スロットによる柔軟性の強化
ここまではいいのですが、この要素はあまり柔軟ではありません。
中には高々 1 つのテキストを表示できるだけなので、現時点では通常の段落よりも有用ではありません。 <slot> 要素を使用することで、各要素インスタンスに異なるテキストを表示することを宣言的に行えるようにすることができます。
スロットは name 属性で識別され、テンプレート内にプレースホルダーを定義することができます。このプレースホルダーは、その要素がマークアップで使用されたときに、任意のマークアップフラグメントで埋められるようになります。
ですから、この些細な例にスロットを追加したい場合、テンプレートの段落要素を次のように更新してください。
<p><slot name="my-text">既定のテキスト</slot></p>
マークアップに要素が含まれるときにスロットの内容が定義されていない場合、またはブラウザーがスロットに対応していない場合、 <my-paragraph> はに単に代替内容である「既定のテキスト」が入ります。
スロットの内容を定義するために、<my-paragraph> 要素の中に HTML 構造を入れ、 slot 属性の値が埋めたいスロットの名前と同じになるようにします。前と同じように、これは好きなものを指定できます。
<my-paragraph>
<span slot="my-text">別なテキストを入れましょう。</span>
</my-paragraph>
以下のようにも設定できます。
<my-paragraph>
<ul slot="my-text">
<li>別なテキストを入れましょう。</li>
<li>リストの中です。</li>
</ul>
</my-paragraph>
メモ: スロットに挿入することができるノードは「スロット可能 (Slottable)」ノードと呼ばれます。ノードがスロットに挿入されたとき、「スロットされている」と言います。
簡単な例での説明は以上です。 もっと実行してみたい場合は、 GitHub 上にあります(ライブ実行版もあります)。
name 属性はシャドウルートごとに一意である必要があります。同じ名前のスロットが 2 つ存在する場合、一致する slot 属性を持つ要素はすべて、その名前を持つまず <slot> に代入されます。ただし slot 属性自体は一意である必要はありません。 <slot> は、一致する slot 属性を持つ複数の要素によって埋められることができます。
name 属性と slot 属性はどちらも既定で空文字列となるため、 slot 属性を指定しない要素は name 属性を指定しない <slot> (無名スロットまたは既定のスロット)に代入されます。例を以下に示します。
<template id="custom-paragraph">
<style>
p {
color: white;
background-color: #666666;
padding: 5px;
}
</style>
<p>
<slot name="my-text">既定のテキスト</slot>
<slot></slot>
</p>
</template>
その後、このように使用することができます。
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
<span>This will go into the unnamed slot</span>
<span>This will also go into the unnamed slot</span>
</my-paragraph>
この例では、
slot="my-text"をつけたコンテンツは、名前付きスロットに格納されます。- 他のコンテンツは自動的に無名スロットに配置されます。
より踏み込んだ例
記事の最後に、もう少し本格的なものを見てみましょう。
以下の一連のコードは、 <slot> を <template> と若干の JavaScript と組み合わせて使用する方法を示すコードスニペットです。
<element-details>要素を名前付きスロット付きでシャドウルートの中に作成する<element-details>要素を、文書内で使用されたとき、要素の内容とそのシャドウルートの内容を組み合わせてレンダリングされるように設計します。つまり、要素の内容の断片は、そのシャドウルートの中で名前付きスロットを埋めるために使用されます。
なお、 <slot> 要素は技術的には、 <template> 要素なしで、例えば、通常の <div> 要素内で使うことも可能であり、それでもシャドウ DOM 内容に対して <slot> のプレースホルダー機能を活用することができますし、そうすれば、最初にテンプレート要素の content プロパティにアクセス(してそれを複製)する必要があるという小さなトラブルも実際に避けることができます。
しかし、一般的には <template> 要素内にスロットを追加する方がより実用的です。なぜなら、既にレンダリングされた要素に基づいてパターンを定義する必要があることはほとんどないからです。
また、まだレンダリングされていない場合でも、 <template> を使用することで、テンプレートとしてのコンテナーの目的がより意味的に明確になるはずです。また、 <template> には、 <td> のような、 <div> に追加すると消えてしまうような項目を直接追加することができます。
メモ: 完全な例は element-details で(ライブ実行版も)参照することができます。
template をスロットと共に作成
まず最初に <slot> 要素を <template> 要素の中に作成し、名前付きスロットを含んだ新しい "element-details-template" という文書フラグメントを作成します。
<template id="element-details-template">
<style>
details {
font-family: "Open Sans Light", Helvetica, Arial;
}
.name {
font-weight: bold;
color: #217ac0;
font-size: 120%;
}
h4 {
margin: 10px 0 -8px 0;
}
h4 span {
background: #217ac0;
padding: 2px 6px;
}
h4 span {
border: 1px solid #cee9f9;
border-radius: 4px;
}
h4 span {
color: white;
}
.attributes {
margin-left: 22px;
font-size: 90%;
}
.attributes p {
margin-left: 16px;
font-style: italic;
}
</style>
<details>
<summary>
<span>
<code class="name"
><<slot name="element-name">NEED NAME</slot>></code
>
<span class="desc"
><slot name="description">NEED DESCRIPTION</slot></span
>
</span>
</summary>
<div class="attributes">
<h4><span>Attributes</span></h4>
<slot name="attributes"><p>None</p></slot>
</div>
</details>
<hr />
</template>
この <template> 要素にはいくつかの特徴があります。
-
<template>には<style>要素があり、<template>が生成する文書の断片だけを対象とした CSS スタイルの集合を持ちます。 -
<template>は<slot>とそのname属性を用いて、 3 つの名前付きスロット を生成しています。<slot name="element-name"><slot name="description"><slot name="attributes">
-
<template>には名前付きスロットを<details>要素の中に持ちます。
新しい <element-details> 要素を <template> から生成
次に、 <element-details> という名前の新しいカスタム要素を生成して、 Element.attachShadow でそのシャドウルートとして追加し、上記の <template> 要素で生成した文書の断片を取り付けてみましょう。
これは、先ほどの簡単な例で見たのとまったく同じパターンを使っています。
customElements.define(
"element-details",
class extends HTMLElement {
constructor() {
super();
const template = document.getElementById(
"element-details-template",
).content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.cloneNode(true));
}
},
);
<element-details> カスタム要素を名前付きスロットと共に使用する
では、 <element-details> 要素を文書内で実際に使ってみましょう。
<element-details>
<span slot="element-name">slot</span>
<span slot="description"
>A placeholder inside a web component that users can fill with their own
markup, with the effect of composing different DOM trees together.</span
>
<dl slot="attributes">
<dt>name</dt>
<dd>The name of the slot.</dd>
</dl>
</element-details>
<element-details>
<span slot="element-name">template</span>
<span slot="description"
>A mechanism for holding client- side content that is not to be rendered
when a page is loaded but may subsequently be instantiated during runtime
using JavaScript.</span
>
</element-details>
このコードについて以下の点に注意してください。
- このスニペットには
<element-details>要素が 2 つあり、どちらもslot属性を使って<element-details>のシャドウルートに置いた名前付きスロットの"element-name"と"description"を参照しています。 - これら 2 つの
<element-details>要素のうち最初のものだけが"attributes"名前付きスロット を参照しています。 2 番目の<element-details>要素は"attributes"名前付きスロット への参照を欠いています。 - 最初の
<element-details>要素は"attributes"名前付きスロット を<dl>およびその子要素である<dt>と<dd>を使って参照しています。
最後に多少のスタイルを追加
最後に若干の CSS スタイルを、文書中の <dl>、<dt>、<dd> の各要素に追加ししす。
dl {
margin-left: 6px;
}
dt {
color: #217ac0;
font-family: Consolas, "Liberation Mono", Courier;
font-size: 110%;
font-weight: bold;
}
dd {
margin-left: 16px;
}
結果
以上のコードを繋げてどのような結果がレンダリングされるかを確認しましょう。
このレンダリング結果について、以下の点に注意してください。
- 文書内で
<element-details>要素のインスタンスは<details>要素を直接使用しませんが、<details>をシャドウルートが表示させることでレンダリングされます。 - レンダリングされた
<details>の出力結果で、<element-details>要素の内容は名前付きスロットをシャドウルートから埋め込みます。言い換えれば、<element-details>要素の DOM ツリーはシャドウルートの内容と共に構成されます。 - 両方の
<element-details>要素おいて、 Attributes の見出しはシャドウルートから"attributes"名前付きスロットの位置の前に、自動的に追加されます。 - 最初の
<element-details>はシャドウルートから名前付きスロットを明示的に参照している<dl>要素を持つため、<dl>の内容は シャドウルートから"attributes"名前付きスロットを置き換えています。 - 2 つ目の
<element-details>はシャドウルートから名前付きスロットを明示的に参照していないため、名前付きスロットの内容はシャドウルートの既定の内容が埋め込まれます。