テンプレートとスロットの使用

この記事では、<template><slot> 要素を使用して柔軟なテンプレートを作成し、それをウェブコンポーネントのシャドウ DOM を表示するために使用する方法について説明します。

テンプレートの真実

ウェブページで同じマークアップ構造を繰り返し再利用する必要がある場合、同じ構造を何度も繰り返すよりも、何らかのテンプレートを使用する方が理にかなっています。 これは以前から可能でしたが、HTML の <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);

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

ウェブコンポーネントを用いたテンプレートの使用

テンプレートはそれ自体でも便利ですが、ウェブコンポーネントと組み合わせるとさらに効果的です。 テンプレートをシャドウ DOM の内容として使用するウェブコンポーネントを定義してみましょう。 同様に <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() メソッドを使用して複製したものをシャドウルートに追加していることに注意してください。

また、その内容をシャドウ 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>

Note: テンプレートはブラウザーの対応が進んでいます。 Shadow DOM API は既定で、 Firefox (バージョン 63 以降) 、Chrome、Opera、Safari、Edge (バージョン 79 以降) で対応しています。

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

ここまではいいのですが、この要素はあまり柔軟ではありません。 中には高々 1 つのテキストを表示できるだけなので、現時点では通常の段落よりも有用ではありません。 <slot> 要素を使用することで、各要素インスタンスに異なるテキストを表示することを宣言的に行えるようにすることができます。 これは <template> よりも対応が限定されており、Chrome 53, Opera 40, Safari 10, Firefox 59, Edge 79 から利用可能です。

スロットは 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>

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

Note: 無名の <slot> には、カスタム要素のトップレベルの子ノードのうち slot 属性を持たないすべてのノードが入ります。これにはテキストノードも含まれます。

簡単な例での説明は以上です。 もっと実行してみたい場合は、 GitHub 上にあります (ライブ実行版もあります)。

より踏み込んだ例

記事の最後に、もう少し本格的なものを見てみましょう。

以下の一連のコードは、 <slot><template> と若干の JavaScript と組み合わせて使用する方法を示すコードスニペットです。

なお、 <slot> 要素は技術的には、 <template> 要素なしで、例えば、通常の <div> 要素内で使うことも可能であり、それでもシャドウ DOM 内容に対して <slot> のプレースホルダー機能を活用することができますし、そうすれば、最初にテンプレート要素の content プロパティにアクセス(してそれを複製)する必要があるという小さなトラブルも実際に避けることができます。 しかし、一般的には <template> 要素内にスロットを追加する方がより実用的です。なぜなら、既にレンダリングされた要素に基づいてパターンを定義する必要があることはほとんどないからです。

また、まだレンダリングされていない場合でも、 <template> を使用することで、テンプレートとしてのコンテナーの目的がより意味的に明確になるはずです。また、 <template> には、 <td> のような、 <div> に追加すると消えてしまうような項目を直接追加することができます。

Note: 完全な例は 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 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> 要素にはいくつかの特徴があります。

新しい <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'})
        .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" 名前付きスロット<dt><dd> 子要素を使って参照しています。

最後に多少のスタイルを追加

最後に若干の CSS スタイルを、文書中の <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

このレンダリング結果について、以下の点に注意してください。