Vorlage, Anzeigenfläche und Schatten

Der Vorteil von Webkomponenten ist ihre Wiederverwendbarkeit: Sie können ein UI-Widget einmal erstellen und es dann mehrfach verwenden. Sie benötigen JavaScript, um Webkomponenten zu erstellen. Eine JavaScript-Bibliothek ist jedoch nicht erforderlich. HTML und die zugehörigen APIs bieten alles, was Sie brauchen.

Der Webkomponentenstandard besteht aus drei Teilen – HTML-Vorlagen, benutzerdefinierten Elementen und dem Shadow DOM. In Kombination können damit benutzerdefinierte, in sich geschlossene (gekapselte), wiederverwendbare Elemente erstellt werden, die sich wie alle anderen HTML-Elemente, die wir bereits behandelt haben, nahtlos in vorhandene Anwendungen einbinden lassen.

In diesem Abschnitt erstellen wir das <star-rating>-Element, eine Webkomponente, mit der Nutzer eine Erfahrung auf einer Skala von eins bis fünf Sternen bewerten können. Es wird empfohlen, nur Kleinbuchstaben zu verwenden, wenn Sie ein benutzerdefiniertes Element benennen. Fügen Sie auch einen Bindestrich hinzu, da Sie so zwischen regulären und benutzerdefinierten Elementen unterscheiden können.

Wir zeigen Ihnen, wie Sie die Elemente <template> und <slot>, das Attribut slot und JavaScript verwenden, um eine Vorlage mit einem gekapselten Shadow DOM zu erstellen. Dann verwenden wir das definierte Element noch einmal und passen einen Textabschnitt an, genau wie jedes andere Element oder eine Webkomponente. Wir werden auch kurz auf die Verwendung von CSS innerhalb und außerhalb des benutzerdefinierten Elements eingehen.

Das <template>-Element

Das <template>-Element wird verwendet, um HTML-Fragmente zu deklarieren, die geklont und mit JavaScript in das DOM eingefügt werden sollen. Der Inhalt des Elements wird nicht standardmäßig gerendert. Sie werden stattdessen mithilfe von JavaScript instanziiert.

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

Da der Inhalt eines <template>-Elements nicht auf den Bildschirm geschrieben wird, werden <form> und sein Inhalt nicht gerendert. Ja, dieser Codepen ist leer, aber auf dem HTML-Tab siehst du das <template>-Markup.

In diesem Beispiel ist <form> kein untergeordnetes Element von <template> im DOM. Inhalte von <template>-Elementen sind vielmehr untergeordnete Elemente einer DocumentFragment, die vom Attribut HTMLTemplateElement.content zurückgegeben werden. Um sichtbar zu werden, muss JavaScript verwendet werden, um die Inhalte zu erfassen und an das DOM anzuhängen.

Mit diesem kurzen JavaScript wurde kein benutzerdefiniertes Element erstellt. Stattdessen wurde in diesem Beispiel der Inhalt von <template> an <body> angehängt. Der Inhalt ist jetzt Teil des sichtbaren, anpassbaren DOMs.

Screenshot des vorherigen Codepen, wie er im DOM angezeigt wird

Es ist nicht sehr nützlich, JavaScript zur Implementierung einer Vorlage für nur eine Sternebewertung zu verwenden. Es ist jedoch hilfreich, eine Webkomponente für ein wiederholt verwendetes, anpassbares Bewertungs-Widget zu erstellen.

Das <slot>-Element

Wir fügen einen Slot für eine benutzerdefinierte Legende pro Vorkommen hinzu. HTML stellt ein <slot>-Element als Platzhalter in einem <template> bereit. Wenn ein Name angegeben wird, wird eine "benannte Anzeigenfläche" erstellt. Eine benannte Anzeigenfläche kann verwendet werden, um Inhalte innerhalb einer Webkomponente anzupassen. Mit dem <slot>-Element können wir steuern, wo die untergeordneten Elemente eines benutzerdefinierten Elements in dessen Schattenbaum eingefügt werden sollen.

In unserer Vorlage ändern wir <legend> in <slot>:

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

Mit dem Attribut name können anderen Elementen Anzeigenflächen zugewiesen werden, wenn das Element ein Attribut slot hat, dessen Wert mit dem Namen eines benannten Slots übereinstimmt. Wenn das benutzerdefinierte Element keine Übereinstimmung für eine Anzeigenfläche hat, wird der Inhalt von <slot> gerendert. Deshalb haben wir eine <legend> mit allgemeinen Inhalten hinzugefügt, die gerendert werden kann, wenn jemand einfach <star-rating></star-rating> ohne Inhalt in den HTML-Code einfügt.

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

Das slot-Attribut ist ein globales Attribut, mit dem der Inhalt des <slot> innerhalb eines <template> ersetzt wird. In unserem benutzerdefinierten Element ist das Element mit dem Anzeigenflächenattribut ein <legend>. Das muss nicht so sein. In unserer Vorlage wird <slot name="star-rating-legend"> durch <anyElement slot="star-rating-legend"> ersetzt, wobei <anyElement> ein beliebiges Element sein kann, auch ein benutzerdefiniertes Element.

Nicht definierte Elemente

In unserem <template> haben wir ein <rating>-Element verwendet. Dies ist kein benutzerdefiniertes Element. Es ist vielmehr ein unbekanntes Element. Browser schlagen nicht fehl, wenn sie ein Element nicht erkennen. Nicht erkannte HTML-Elemente werden vom Browser als anonyme Inline-Elemente behandelt, die mit CSS formatiert werden können. Ähnlich wie bei <span> haben die Elemente <rating> und <star-rating> keine vom User-Agent angewendeten Stile oder Semantik.

Beachten Sie, dass <template> und Inhalte nicht gerendert werden. Das <template> ist ein bekanntes Element mit Inhalten, die nicht gerendert werden sollen. Das <star-rating>-Element muss noch definiert werden. Solange kein Element definiert ist, wird es im Browser wie alle nicht erkannten Elemente angezeigt. Derzeit wird das nicht erkannte <star-rating> als anonymes Inline-Element behandelt. Daher werden der Inhalt einschließlich Legenden und <p> im dritten <star-rating> so angezeigt, als wären sie in einem <span> enthalten.

Definieren wir das Element, um dieses nicht erkannte Element in ein benutzerdefiniertes Element umzuwandeln.

Benutzerdefinierte Elemente

JavaScript ist erforderlich, um benutzerdefinierte Elemente zu definieren. Wenn definiert, wird der Inhalt des <star-rating>-Elements durch einen Schattenstamm ersetzt, der den gesamten Inhalt der Vorlage enthält, die wir mit ihm verknüpfen. Die <slot>-Elemente aus der Vorlage werden durch den Inhalt des Elements innerhalb der <star-rating> ersetzt, deren slot-Attributwert mit dem Namenswert von <slot> übereinstimmt, sofern vorhanden. Ist dies nicht der Fall, wird der Inhalt der Anzeigenflächen der Vorlage angezeigt.

Inhalte in einem benutzerdefinierten Element, das nicht mit einer Anzeigenfläche verknüpft ist (dem <p>Is this text visible?</p> in unserer dritten <star-rating>), ist nicht im Schattenstamm enthalten und wird daher nicht angezeigt.

Wir definieren das benutzerdefinierte Element mit dem Namen star-rating, indem wir HTMLElement erweitern:

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

Wenn das Element nun definiert ist, wird es jedes Mal, wenn der Browser auf ein <star-rating>-Element stößt, so gerendert, wie es vom Element mit der #star-rating-template-Vorlage, unserer Vorlage, definiert wird. Der Browser hängt dem Knoten einen Schatten-DOM-Baum an und fügt einen Klon des Vorlageninhalts an dieses Schatten-DOM an. Die Elemente, für die Sie attachShadow() verwenden können, sind eingeschränkt.

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

Wenn Sie sich die Entwicklertools ansehen, werden Sie feststellen, dass das <form> aus dem <template> Teil des Schattenstamms jedes benutzerdefinierten Elements ist. In den Entwicklertools ist in jedem benutzerdefinierten Element ein Klon des <template>-Inhalts und im Browser sichtbar. Der Inhalt des benutzerdefinierten Elements selbst wird jedoch nicht auf dem Bildschirm gerendert.

Screenshot der Entwicklertools mit dem Inhalt der geklonten Vorlage für jedes benutzerdefinierte Element

Im <template>-Beispiel haben wir den Vorlageninhalt an den Dokumenttext angehängt und den Inhalt dem regulären DOM hinzugefügt. In der customElements-Definition haben wir dasselbe appendChild()-Element verwendet, aber der geklonte Inhalt der Vorlage wurde an ein gekapseltes Schatten-DOM angehängt.

Sehen Sie, wie die Sterne wieder als Optionsfelder geändert wurden? Da der Stil auf dem CSS-Tab von Codepen nicht Teil des Standard-DOM ist, sondern Teil eines Schatten-DOM ist, wird er nicht angewendet. Die CSS-Stile dieses Tabs beziehen sich auf das Dokument und nicht auf das Schatten-DOM. Daher werden die Stile nicht angewendet. Wir müssen gekapselte Stile erstellen, um die Inhalte unserer gekapselten Shadow DOM-Inhalte zu gestalten.

Schatten-DOM

Das Shadow DOM beschränkt CSS-Stile auf jeden Schattenbaum und isoliert ihn vom Rest des Dokuments. Das bedeutet, dass externe CSS-Elemente nicht auf Ihre Komponente angewendet werden und Komponentenstile keine Auswirkungen auf den Rest des Dokuments haben, es sei denn, wir weisen sie ausdrücklich darauf hin.

Da wir die Inhalte an ein Shadow DOM angehängt haben, können wir ein <style>-Element einschließen, das dem benutzerdefinierten Element gekapselten CSS-Code bereitstellt.

Da die Stile auf das benutzerdefinierte Element beschränkt sind, brauchen wir uns keine Gedanken darüber zu machen, dass Stile in den Rest des Dokuments übertragen werden. Wir können die Spezifität der Selektoren erheblich reduzieren. Da im benutzerdefinierten Element beispielsweise nur Optionsfelder verwendet werden, können Sie input anstelle von input[type="radio"] als Selektor verwenden.

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

Webkomponenten werden mit In-<template>-Markup gekapselt. CSS-Stile sind auf das Schatten-DOM beschränkt und werden vor allem außerhalb der Komponenten verborgen. Der gerenderte Slot-Inhalt, also der <anyElement slot="star-rating-legend">-Teil von <star-rating>, wird nicht gekapselt.

Stil außerhalb des aktuellen Bereichs

Es ist zwar möglich, aber nicht einfach, das Dokument innerhalb eines Shadow DOM zu gestalten und den Inhalt eines Shadow DOM mithilfe der globalen Stile zu gestalten. Die Schattengrenze, an der das Schatten-DOM endet und das reguläre DOM beginnt, kann durchlaufen werden, jedoch nur sehr absichtlich.

Der Schattenbaum ist der DOM-Baum im Schatten-DOM. Die Schattenwurzel ist der Stammknoten des Schattenbaums.

Mit der Pseudoklasse :host wird <star-rating> ausgewählt, das Schatten-Hostelement. Der Shadow-Host ist der DOM-Knoten, mit dem das Shadow-DOM verknüpft ist. Wenn Sie nur bestimmte Versionen des Hosts als Ziel verwenden möchten, verwenden Sie :host(). Dadurch werden nur die Schattenhost-Elemente ausgewählt, die mit dem übergebenen Parameter übereinstimmen, z. B. eine Klasse oder eine Attributauswahl. Wenn Sie alle benutzerdefinierten Elemente auswählen möchten, können Sie star-rating { /* styles */ } im globalen CSS oder :host(:not(#nonExistantId)) in den Vorlagenstilen verwenden. Was die Spezifität angeht, gewinnt das globale Preisvergleichsportal.

Das Pseudoelement ::slotted() überschreitet die Schatten-DOM-Grenze innerhalb des Schatten-DOM. Es wählt ein Element mit Slots aus, wenn es mit dem Selektor übereinstimmt. In unserem Beispiel entspricht ::slotted(legend) unseren drei Legenden.

Zum Targeting auf ein Shadow DOM aus CSS im globalen Geltungsbereich muss die Vorlage bearbeitet werden. Das Attribut part kann jedem Element hinzugefügt werden, das Sie gestalten möchten. Verwenden Sie dann das Pseudoelement ::part(), um Elemente in einem Schattenbaum abzugleichen, die dem übergebenen Parameter entsprechen. Der Anker oder das ursprüngliche Element des Pseudoelements ist der Host- oder der Name des benutzerdefinierten Elements, in diesem Fall star-rating. Der Parameter ist der Wert des Attributs part.

Wenn unser Vorlagen-Markup so begann:

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

So könnten Sie ein Targeting auf <form> und <fieldset> vornehmen:

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

Teilenamen verhalten sich ähnlich wie Klassen: Ein Element kann mehrere durch Leerzeichen getrennte Teilnamen haben und mehrere Elemente können denselben Teilnamen haben.

Google bietet eine fantastische Checkliste zum Erstellen benutzerdefinierter Elemente. Weitere Informationen zu deklarativen Shadow-DOMs

Überprüfen Sie Ihr Wissen

Testen Sie Ihr Wissen über Vorlage, Slot und Schatten.

Standardmäßig werden Stile, die von außerhalb des Shadow-DOMs stammen, auf Elemente darin angewendet.

Richtig
Versuche es bitte noch einmal.
Falsch
Richtig!

Welche Antwort ist eine korrekte Beschreibung des <template>-Elements?

Ein generisches Element, mit dem Inhalte auf Ihrer Seite angezeigt werden.
Versuche es bitte noch einmal.
Ein Platzhalterelement.
Versuche es bitte noch einmal.
Ein -Element, mit dem HTML-Fragmente deklariert werden, die nicht standardmäßig gerendert werden.
Richtig!