テンプレート、スロット、シャドウ

ウェブ コンポーネントの利点は再利用性です。UI ウィジェットを一度作成すると、何度でも再利用できます。ウェブ コンポーネントを作成するには JavaScript は必要ですが、JavaScript ライブラリは必要ありません。HTML と関連する API には、必要なものがすべて含まれています。

Web Component 標準は、HTML テンプレートカスタム要素Shadow DOM の 3 つの部分で構成されています。これらを組み合わせることで、すでに説明した他の HTML 要素と同様に、カスタマイズされた自己完結型の(カプセル化された)再利用可能な要素を構築できます。この要素は既存のアプリにシームレスに統合できます。

このセクションでは、<star-rating> 要素を作成します。これは、ユーザーが 1 ~ 5 段階の星評価でエクスペリエンスを評価できるようにするウェブ コンポーネントです。カスタム要素に名前を付けるときは、すべて小文字にすることをおすすめします。ダッシュも追加します。これにより、通常の要素とカスタム要素を区別できます。

<template> 要素、<slot> 要素、slot 属性、JavaScript を使用して、カプセル化された Shadow DOM を含むテンプレートを作成する方法を説明します。その後、定義した要素を再利用して、要素やウェブ コンポーネントと同様にテキストのセクションをカスタマイズします。また、カスタム要素内外から CSS を使用する方法についても簡単に説明します。

<template> 要素

<template> 要素は、JavaScript でクローンを作成して DOM に挿入する HTML フラグメントを宣言するために使用します。デフォルトでは、要素のコンテンツはレンダリングされません。むしろ、JavaScript を使用してインスタンス化されます。

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

<template> 要素のコンテンツは画面に書き込まれないため、<form> とその内容はレンダリングされません。はい。この Codepen は空白ですが、[HTML] タブを調べると、<template> マークアップがあります。

この例では、<form> は DOM の <template> の子ではありません。<template> 要素のコンテンツは、HTMLTemplateElement.content プロパティから返される DocumentFragment の子要素になります。表示させるには、JavaScript を使用してコンテンツを取得し、そのコンテンツを DOM に追加する必要があります。

この簡単な JavaScript では、カスタム要素は作成されませんでした。この例では、<template> の内容を <body> に追加しています。コンテンツは表示可能でスタイル可能な DOM の一部になりました。

DOM に表示された前の Codepen のスクリーンショット。

1 つの星評価だけのテンプレートを実装するように JavaScript を要求することはあまり役に立ちませんが、繰り返し使用するカスタマイズ可能な星評価ウィジェット用のウェブ コンポーネントを作成すると便利です。

<slot> 要素

発生状況ごとにカスタマイズした凡例を含めるためのスロットが用意されています。HTML では、<template> 内のプレースホルダとして <slot> 要素が提供されます。名前を指定すると、「名前付きスロット」が作成されます。名前付きスロットを使用して、ウェブ コンポーネント内のコンテンツをカスタマイズできます。<slot> 要素を使用すると、カスタム要素の子をシャドウツリー内のどこで挿入するかを制御できます。

テンプレートで、<legend><slot> に変更します。

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

name 属性は、要素が名前付きスロットの名前と一致する slot 属性を持つ場合に、スロットを他の要素に割り当てるために使用します。カスタム要素がスロットに一致しない場合、<slot> のコンテンツがレンダリングされます。そのため、一般的なコンテンツを含む <legend> を含めました。これは、コンテンツのない <star-rating></star-rating> を HTML に含めるだけでレンダリングしても問題ありません。

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

スロット属性は、<template> 内の <slot> の内容を置き換えるために使用されるグローバル属性です。このカスタム要素では、スロット属性を持つ要素は <legend> です。そうする必要はありません。テンプレートで、<slot name="star-rating-legend"><anyElement slot="star-rating-legend"> に置き換えられます。ここで、<anyElement> は任意の要素(別のカスタム要素を含む)にできます。

未定義の要素

<template> では <rating> 要素を使用しました。これはカスタム要素ではありません。未知の要素です。ブラウザで要素を認識しなくても、エラーになることはありません。認識されない HTML 要素は、ブラウザでは匿名のインライン要素として扱われ、CSS でスタイルを設定できます。<span> と同様に、<rating> 要素と <star-rating> 要素には、ユーザー エージェントに適用されるスタイルやセマンティクスはありません。

<template> とコンテンツはレンダリングされないことに注意してください。<template> は、レンダリングされないコンテンツを含む既知の要素です。<star-rating> 要素はまだ定義されていません。要素を定義するまで、ブラウザは認識できない要素と同様にその要素を表示します。現時点では、認識されない <star-rating> は匿名のインライン要素として扱われるため、3 番目の <star-rating> の凡例と <p> を含むコンテンツは、<span> 内にある場合と同じように表示されます。

この認識されない要素をカスタム要素に変換する要素を定義します。

カスタム要素

カスタム要素を定義するには JavaScript が必要です。定義されると、<star-rating> 要素のコンテンツは、関連付けられているテンプレートのすべてのコンテンツを含むシャドウルートに置き換えられます。テンプレートの <slot> 要素は、slot 属性値が <slot> の名前値と一致する <star-rating> 内の要素の内容に置き換えられます(存在する場合)。そうでない場合は、テンプレートのスロットのコンテンツが表示されます。

スロットに関連付けられていないカスタム要素内のコンテンツ(3 番目の <star-rating><p>Is this text visible?</p>)は、シャドウルートに含まれないため、表示されません。

HTMLElement を拡張して、star-rating という名前のカスタム要素を定義します。

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

要素が定義されたので、ブラウザが <star-rating> 要素を見つけるたびに、テンプレートである #star-rating-template で要素の定義どおりにレンダリングされます。ブラウザは Shadow DOM ツリーをノードにアタッチし、テンプレート コンテンツのクローンをその Shadow DOM に追加します。attachShadow() を使用できる要素には制限があります

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

デベロッパー ツールを見ると、<template><form> が、各カスタム要素のシャドウルートの一部であることがわかります。<template> コンテンツのクローンは、デベロッパー ツールの各カスタム要素に表示され、ブラウザに表示されますが、カスタム要素自体のコンテンツは画面にレンダリングされません。

各カスタム要素に複製されたテンプレートのコンテンツが表示されている DevTools のスクリーンショット。

<template> の例では、テンプレート コンテンツをドキュメントの本文に追加し、通常の DOM にコンテンツを追加しています。customElements の定義でも同じ appendChild() を使用しましたが、クローンが作成されたテンプレートのコンテンツがカプセル化された Shadow DOM に追加されています。

星のスタイルが未設定のラジオボタンになったことに注目してください。標準の DOM ではなく Shadow DOM の一部であるため、Codepen の [CSS] タブ内のスタイルは適用されません。そのタブの CSS スタイルは、Shadow DOM ではなくドキュメントにスコープが設定されているため、スタイルは適用されません。カプセル化された Shadow DOM コンテンツのスタイルを設定するには、カプセル化されたスタイルを作成する必要があります。

Shadow DOM

Shadow DOM は、各 Shadow ツリーに CSS スタイルのスコープを適用し、ドキュメントの他の部分から分離します。つまり、外部の CSS はコンポーネントに適用されず、コンポーネント スタイルは、意図的に指定しない限り、ドキュメントの残りの部分には影響しません。

コンテンツを Shadow DOM に追加しているため、<style> 要素を追加して、カプセル化された CSS をカスタム要素に追加できます。

カスタム要素にスコープを設定すれば、ドキュメントの他の部分にもスタイルが浸透する心配がなくなります。セレクタの特異性を大幅に下げることができます。たとえば、カスタム要素で使用される入力はラジオボタンのみであるため、セレクタとして input[type="radio"] ではなく input を使用できます。

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

ウェブ コンポーネントは <template> 内マークアップでカプセル化され、CSS スタイルは Shadow DOM をスコープとし、コンポーネント外のすべてに対して非表示になりますが、レンダリングされるスロット コンテンツ(<star-rating><anyElement slot="star-rating-legend"> 部分)はカプセル化されません。

現在のスコープ外のスタイル設定

Shadow DOM 内からドキュメントのスタイルを設定したり、グローバル スタイルから Shadow DOM のコンテンツのスタイルを設定したりすることもできますが、単純ではありません。Shadow DOM が終了し、通常の DOM が開始されるシャドウ境界を横断できますが、これは意図的なものではありません。

Shadow ツリーは、Shadow DOM 内の DOM ツリーです。シャドウのルートは、シャドウツリーのルートノードです。

:host 疑似クラスは、シャドウ ホスト要素である <star-rating> を選択します。Shadow ホストは、Shadow DOM が接続されている DOM ノードです。ホストの特定のバージョンのみをターゲットにするには、:host() を使用します。これにより、クラスや属性セレクタなど、渡されたパラメータに一致するシャドウホスト要素のみが選択されます。すべてのカスタム要素を選択するには、グローバル CSS で star-rating { /* styles */ } を使用するか、テンプレート スタイルで :host(:not(#nonExistantId)) を使用します。特異性に関しては、グローバル CSS が優先されます。

::slotted() 擬似要素は、Shadow DOM 内から Shadow DOM の境界を横断します。セレクタに一致すると、スロット要素が選択されます。この例では、::slotted(legend) は 3 つの凡例に一致します。

グローバル スコープで CSS の Shadow DOM をターゲットにするには、テンプレートを編集する必要があります。part 属性は、スタイルを設定するすべての要素に追加できます。次に、::part() 疑似要素を使用して、渡されたパラメータに一致するシャドウツリー内の要素を照合します。擬似要素のアンカー(元の要素)は、ホストまたはカスタム要素の名前(この場合は star-rating)です。パラメータは part 属性の値です。

テンプレートのマークアップが次のようになっている場合:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

次のように <form><fieldset> をターゲットにできます。

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

パート名はクラスと同様に機能します。1 つの要素に複数のスペース区切りのパート名を含めることができ、複数の要素に同じパート名を設定できます。

Google は、カスタム要素を作成するための便利なチェックリストを用意しています。宣言型 Shadow DOM についても学習することをおすすめします。

理解度チェック

テンプレート、スロット、シャドウに関する知識をテストします。

デフォルトでは、Shadow DOM の外部にあるスタイルは、内部の要素のスタイルを設定します。

正しい
もう一度お試しください。
誤り
正解です。

<template> 要素の説明として正しいものはどれですか。

ページのコンテンツを表示するために使用される汎用的な要素です。
もう一度お試しください。
プレースホルダ要素です。
もう一度お試しください。
デフォルトでは表示されない HTML のフラグメントの宣言に使用する要素です。
正解です。