Shadow DOM, Vorlagen und Slots

Webfunktionen: Shadow DOM <slot>

Ein Shadow-Root ist ein gekapselter DOM-Unterbaum, der an ein Hostelement angehängt ist. Stile innerhalb des Shadow-Roots dringen nicht nach außen und Seitenstile dringen nicht nach innen (außer geerbte Eigenschaften). Vorlagen erstellen den Baum effizient, Slots projizieren Light-DOM-Inhalte hinein.

Shadow-Root anhängen

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

delegatesFocus: true sorgt dafür, dass der Fokus beim Fokussieren des Hosts auf das erste fokussierbare Element im Shadow Tree verschoben wird, leitet das :focus-Styling an den Host weiter und leitet bei formularbezogenen benutzerdefinierten Elementen (static formAssociated = true) <label>-Klicks an das innere Steuerelement weiter. Legen Sie es immer fest, wenn die Komponente fokussierbare Steuerelemente umschließt. Verwenden Sie in fast allen Fällen mode: 'open', damit Entwicklertools, Seitenskripts und Server-Hydration element.shadowRoot erreichen können (siehe Web Components Orientation).

Vorlagen: Baumstruktur effizient erstellen

Definieren Sie einen <template> einmal (Modulbereich oder statisches Feld) und klonen Sie ihn pro Instanz. Das Klonen eines geparsten Fragments ist viel schneller als das erneute Parsen eines innerHTML-Strings bei jeder Erstellung.

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);

Halten Sie Vorlagen schlank. Tief verschachtelte „div-Suppe“ und viele Ebenen verschachtelter Shadow-Roots verursachen messbare Kosten für die Stilauflösung und das Layout, was bei großen Projekten zu erheblichen Leistungseinbußen führt. Bevorzugen Sie eine flache Struktur und semantische Elemente.

Slots: die Teile, die die Leute überraschen

Standard- und benannte Slots funktionieren wie dokumentiert. Die folgenden Verhaltensweisen sind jedoch problematisch:

  • Leerzeichen zählen. Textknoten zwischen Elementen (einschließlich reiner Leerzeichen) werden dem Standard-Slot zugewiesen. Ein Standard-Slot ist daher selten wirklich „leer“.
  • Die DOM-Reihenfolge wird beibehalten, nicht die Slot-Reihenfolge. Projizierte Knoten behalten ihre ursprüngliche Light-DOM-Reihenfolge für :first-child/:last-child bei, unabhängig davon, welchen Slot sie füllen. Eine ::slotted(*:first-child)-Regel ist auf das erste DOM-Unterelement ausgerichtet, nicht auf das erste Unterelement dieser Anzeigenfläche. Daher entspricht sie oft nicht Ihren Erwartungen.
  • slotchange ist das Signal für den Upgrade-Zeitpunkt. Es wird ausgelöst, wenn sich zugewiesene Knoten ändern (einschließlich der ersten Zuweisung). Es ist die zuverlässige Methode, um festzustellen, ob Light DOM-Unterelemente nach einem Upgrade vorhanden sind, wenn sie während connectedCallback möglicherweise nicht vorhanden waren.
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

Stylesheets in einen Shadow-Root laden

Methode Vorteile Nachteile
<style> in der Vorlage Einfach, vollständig gekapselt, funktioniert ohne JS. Wird pro Instanz dupliziert und bläht das DOM bei großen Mengen auf.
<link rel="stylesheet"> Cache-fähige externe Datei. Zusätzliche Anfrage; Risiko, dass Inhalte kurzzeitig ohne Formatierung angezeigt werden.
adoptedStyleSheets Eine freigegebene CSSStyleSheet für alle Instanzen; am schnellsten bei der Skalierung. Erfordert JS; konstruierte Tabellen unterstützen @import nicht.

Wenn Sie viele Instanzen einer Komponente haben, sollten Sie ein freigegebenes konstruiertes Stylesheet verwenden:

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];
  }
}