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.
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.
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.
Welche Antwort ist eine korrekte Beschreibung des <template>
-Elements?