template と slot の使い方

This translation is incomplete. この記事の翻訳にご協力ください

この記事では shadow DOM で使われる <template><slot> 要素の使い方を説明します。

テンプレートの真実

あるWebページ上で同じ構造を繰り返し使用する必要がある場合、同じ実装を繰り返し書くよりも、テンプレートのようなものを作って利用する方が合理的でしょう。これは以前から可能でしたが、<template> 要素より簡単に使えるようになりました。 この要素と中身は DOM 上ではレンダリングされませんが、JavaScript から参照することができます。

以下の簡単なサンプルを見てみましょう。

<template id="my-paragraph">
  <p>My paragraph</p>
</template>

テンプレートの内部はページには表示されません。以下のコードのようにJavaScript を用いて参照を取り DOM に追加するとページ上に表示できます。

let template = document.getElementById('my-paragraph');
let templateContent = template.content;
document.body.appendChild(templateContent);

つまらない例ですがすでに有用性は見えてきたでしょう。

Web Componentsを利用したテンプレートの使用

テンプレートはそれ自身でも有用ですが web コンポーネントと共に使用することでより上手く使えます。テンプレートを shadow DOM として活用する web コンポーネントを <my-paragraph> と名付け定義しましょう。

customElements.define('my-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.getElementById('my-paragraph');
      let templateContent = template.content;

      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateContent.cloneNode(true));
  }
})

ここで、テンプレートの内容を使用するために Node.cloneNode() を使用してクローンしたものを shadow root に追加していることに注意してください。

テンプレートの内容を shadow DOM に追加しているので、テンプレートの内部に <style> 要素を用意しスタイルを含むことができます。このスタイルはカスタム要素の内部でカプセル化されます。これは通常の DOM に追加するだけでは正しく動きません。

したがって、例えば

<template id="my-paragraph">
  <style>
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>My paragraph</p>
</template>

こうすれば HTML ドキュメントに以下を追加することで使用できます。

<my-paragraph></my-paragraph>

注意: テンプレートはブラウザでよくサポートされています。Shadow DOM APIはデフォルトのFirefox (バージョン63以降) 、Chrome、OperaそしてSafariでサポートされています。Edgeでも現在開発が行われています。

スロットによる柔軟性の強化

ここまでのサンプルでは高々1種類のテキストを表示できるのみで、普通の paragraph よりも使えません。<slot> 要素を用いることで、各要素のインスタンスに異なるテキストを表示させることができます。<slot>  は <template> よりサポートが限られており、Chrome 53以降、Opera 40以降、Safari 10以降、Firefox 59以降で実装されていますが、Edge ではまだサポートされていません。slot はその name 属性で区別されており、template の中で任意のマークアップで slot の内容のデフォルト値を埋めることができます。

上記の例に slot を追加することを考えます。パラグラフの要素を以下のように書くことができます。

<p><slot name="my-text">デフォルトテキスト</slot></p>

slot の内容が定義されていない場合や、ブラウザが slot をサポートしていな場合は <my-paragraph> は fallback コンテンツを保持し、このサンプルの場合では "デフォルトテキスト" を表示させることになります。

内容を定義したい slot の名前を slot 属性に設定した要素を <my-paragraph> の中に用意すると、その中身が slot の内容になります。中身は HTML 構造を持つ任意のもので埋めることができます。

<my-paragraph>
  <span slot="my-text">新しいテキストを代入します</span>
</my-paragraph>

以下のようにも設定できます。

<my-paragraph>
  <ul slot="my-text">
    <li>新しいテキストを代入します</li>
    <li>リストも代入できます</li>
  </ul>
</my-paragraph>

注意: スロットに挿入できるのは Slotable な要素に限られます; 要素がスロットに挿入されたとき、slotted と呼ばれます。

簡単なサンプルでの説明は以上です。他にも実装してみたい場合は、GitHub上のサンプルコードをご利用ください(実行例)。

より踏み込んだ例

他の例もみてみましょう。

これからのコードは <slot> を <template> と共に使用する方法の例です。以下の2点を目指す JavaScript です。

  • shadow root の中で <element-details> 要素を slot を用いて作ること。
  • <element-details> 要素を、その shadow root と一緒にレンダリングされるように作ること。つまり、要素の内容が slots の中身に代入されるようになります。

<slot> 要素は <template> 要素なしで使用することが可能です。例えば、 <div> 要素の中で宣言しても Shadow DOM で使用した場合と同様にプレースホルダーとしての役割は果たします。しかし、<template> 要素の中で使用する方がより一般的で実用的です。

テンプレートを利用したコンテナの目的は <template> を使用することで意味的にわかりやすくすることです。さらに、<template> の中には <td> など直接追加して良い要素があり、これらは <div> 要素の中に追加された場合は消えます。

注意: element-detailsの完全なコードはここから見ることができます (実行例)。

template をスロットと共に作る

まず最初に<template> 要素の中に <slot> 要素を作成し、新しい "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 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">&lt;<slot name="element-name">NEED NAME</slot>&gt;</code>
        <i class="desc"><slot name="description">NEED DESCRIPTION</slot></i>
      </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 属性は以下のように定義されています。
    • <slot name="element-name">
    • <slot name="description">
    • <slot name="attributes">
  • <template> の中で各 slot は<details> 要素の中に実装されています。

<template> から <element-details> 要素を作る

次に <element-details> と名付けた新しいカスタム要素を作りましょう。 上で確認した簡単な例と同様に、Element.attachShadow を利用してカスタム要素に shadow root を追加します。

customElements.define('element-details',
  class extends HTMLElement {
    constructor() {
      super();
      var template = document
        .getElementById('element-details-template')
        .content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .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> 

このコードについて以下の点に注意してください。

  • 2つの <element-details> 要素が使用されており、いずれも slot 属性を "element-name" および "description" と指定することで対応する slot を参照しています。
  • 1つ目の <element-details> 要素でのみ "attributes" と名付けられた slot を参照しています。2個目の <element-details> 要素では参照していません。
  • 1つ目の <element-details> 要素は <dl> 要素を用いて "attributes" と名付けられた slot を参照しています。

スタイルを追加する

最後にもう少しCSSスタイルを追加します。これは、1個目の <element-details> の中で使われている <dl><dt><dd> 要素のために用意されています。 

  dl { margin-left: 6px; }
  dt { font-weight: bold; color: #217ac0; font-size: 110% }
  dt { font-family: Consolas, "Liberation Mono", Courier }
  dd { margin-left: 16px }

結果

以上のコードを繋げてどのような結果がレンダリングされるかを確認しましょう。

ScreenshotLive sample

以下のことに着目してください。

  • ドキュメント内で <element-details> 要素のインスタンスは <details> 要素を直接使用しませんが、 shadow root が <details> を生成することでレンダリングされます。
  • レンダリングされた <details> の出力結果で、<element-details> 要素のコンテンツは shadow root から名前付きスロットを埋め込みます。言い換えれば、<element-details> 要素のDOMツリーは shadow root のコンテンツと共に構成されます。
  • 両方の <element-details> 要素おいて、"attributes" 名前付きスロットが配置される前に、 shadow root から自動的に Attributes 見出しは自動的に追加されます。
  • 最初の <element-details> は shadow root から名前付きスロットを明示的に参照している <dl> 要素を持つため、<dl> のコンテンツは  shadow root から "attributes" 名前付きスロットを置き換えています。
  • 二つ目の <element-details> は shadow root から名前付きスロットを明示的に参照していないため、名前付きスロットのコンテンツは shadow root のデフォルトのコンテンツが埋め込まれます。