Shadow DOM、テンプレート、スロット

ウェブ機能: Shadow DOM <slot>

シャドウルートは、ホスト要素にアタッチされたカプセル化された DOM サブツリーです。その内部のスタイルは外部に漏れることはなく、ページのスタイルは内部に漏れることはありません(継承されたプロパティを除く)。テンプレートはツリーを効率的に構築し、スロットは Light DOM コンテンツをツリーに投影します。

シャドウルートのアタッチ

this.attachShadow({ mode: 'open', delegatesFocus: true });

delegatesFocus: true を使用すると、ホストにフォーカスを合わせると、シャドウツリー内の最初のフォーカス可能な要素にフォーカスが移動し、:focus スタイルがホストにルーティングされます。また、フォームに関連付けられたカスタム要素(static formAssociated = true)では、<label> のクリックが内部コントロールに転送されます。コンポーネントがフォーカス可能なコントロールをラップする場合は、必ず設定してください。ほとんどの場合、mode: 'open' を使用して、開発ツール、ページ スクリプト、サーバー ハイドレーションが element.shadowRoot にアクセスできるようにします(ウェブ コンポーネントのオリエンテーションを参照)。

テンプレート: ツリーを効率的に構築する

<template> を 1 回定義し(モジュール スコープまたは静的フィールド)、インスタンスごとにクローンを作成します。解析済みのフラグメントのクローンを作成する方が、構築のたびに innerHTML 文字列を再解析するよりもはるかに高速です。

const template = document.createElement('template');
template.innerHTML = `
  <div class="card">
    <slot name="header"></slot>
    <div class="body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
`;

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // MANDATORY: deep-clone the template content so each instance gets its own nodes.
    this.shadowRoot.append(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

テンプレートを簡潔に保ちます。深くネストされた「div soup」と、何層にもネストされたシャドウルートは、どちらもスタイル解決とレイアウトのコストを増大させ、大規模なパフォーマンス低下を引き起こします。フラットな構造とセマンティック要素を優先します。

スロット: 驚きを与える部分

デフォルト スロットと名前付きスロットはドキュメントに記載されているとおりに動作します。以下は、注意が必要な動作です。

  • 空白もカウントされます。要素間のテキストノード(純粋な空白を含む)はデフォルトのスロットに割り当てられるため、デフォルトのスロットが完全に「空」になることはほとんどありません。
  • スロットの順序ではなく、DOM の順序が保持されます。投影されたノードは、どのスロットに挿入されるかにかかわらず、:first-child/:last-child の目的で元の Light DOM の順序を維持します。::slotted(*:first-child) ルールは、そのスロットの最初の子ではなく、最初の DOM 子をターゲットにするため、多くの場合、想定どおりに一致しません。
  • slotchange はアップグレードのタイミングを示すシグナルです。割り当てられたノードが変更されたとき(初期割り当てを含む)に呼び出されます。これは、アップグレード後に Light DOM の子ノードが到着したことを確認するための信頼できる方法です。connectedCallback の実行中に子ノードが存在しなかった可能性があるためです。
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

シャドウルートへのスタイルシートの読み込み

メソッド 長所 短所
テンプレートの <style> シンプルで、完全にカプセル化されており、JS なしで動作します。 インスタンスごとに複製されるため、大規模な DOM が肥大化します。
<link rel="stylesheet"> キャッシュ可能な外部ファイル。 追加のリクエスト。スタイルが適用されていないコンテンツが一時的に表示されるリスクがあります。
adoptedStyleSheets すべてのインスタンスで 1 つの CSSStyleSheet を共有。大規模な環境で最速。 JS が必要です。構築されたシートは @import をサポートしていません。

1 つのコンポーネントのインスタンスが多数ある場合は、共有の構築済みスタイルシートを使用することをおすすめします。

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // The same sheet object is reused by every instance; no per-instance parse.
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}