Verwenden von benutzerdefinierten Elementen
Eine der Hauptmerkmale von Webkomponenten ist die Möglichkeit, benutzerdefinierte Elemente zu erstellen: das heißt, HTML-Elemente, deren Verhalten vom Webentwickler definiert wird und die das Set der im Browser verfügbaren Elemente erweitern.
Dieser Artikel führt in benutzerdefinierte Elemente ein und erklärt einige Beispiele.
Arten von benutzerdefinierten Elementen
Es gibt zwei Arten von benutzerdefinierten Elementen:
-
Angepasste eingebaute Elemente erben von standardmäßigen HTML-Elementen wie
HTMLImageElement
oderHTMLParagraphElement
. Ihre Implementierung erweitert das Verhalten ausgewählter Instanzen des Standard-Elements.Hinweis: Bitte beachten Sie den
is
Attribut-Referenz für Hinweise zur Implementierungsrealität von benutzerdefinierten eingebauten Elementen. -
Autonome benutzerdefinierte Elemente erben von der HTML-Element-Basis-Klasse
HTMLElement
. Sie müssen ihr Verhalten von Grund auf implementieren.
Implementierung eines benutzerdefinierten Elements
Ein benutzerdefiniertes Element wird als Klasse implementiert, die von HTMLElement
(im Falle autonomer Elemente) oder der zu anpassenden Schnittstelle (im Falle angepasster eingebauter Elemente) abgeleitet wird.
Hier ist die Implementierung eines minimalen benutzerdefinierten Elements, das das <p>
-Element anpasst:
class WordCount extends HTMLParagraphElement {
constructor() {
super();
}
// Element functionality written in here
}
Hier ist die Implementierung eines minimalen autonomen benutzerdefinierten Elements:
class PopupInfo extends HTMLElement {
constructor() {
super();
}
// Element functionality written in here
}
Im Konstruktor der Klasse können Sie den Anfangszustand und Standardwerte festlegen, Ereignislistener registrieren und möglicherweise ein Schatten-DOM erstellen. Zu diesem Zeitpunkt sollten Sie die Attribute oder Kinder des Elements nicht inspizieren oder neue Attribute oder Kinder hinzufügen. Siehe Anforderungen für benutzerdefinierte Element-Konstruktoren und -Reaktionen für die vollständige Liste der Anforderungen.
Lebenszyklus-Callbacks für benutzerdefinierte Elemente
Sobald Ihr benutzerdefiniertes Element registriert ist, ruft der Browser bestimmte Methoden Ihrer Klasse auf, wenn der Code auf der Seite auf bestimmte Weise mit Ihrem benutzerdefinierten Element interagiert. Durch Bereitstellung einer Implementierung dieser Methoden, welche die Spezifikation als Lebenszyklus-Callbacks bezeichnet, können Sie Code als Reaktion auf diese Ereignisse ausführen.
Zu den Lebenszyklus-Callbacks für benutzerdefinierte Elemente gehören:
connectedCallback()
: wird jedes Mal aufgerufen, wenn das Element dem Dokument hinzugefügt wird. Die Spezifikation empfiehlt, dass Entwickler die Einrichtung von benutzerdefinierten Elementen so weit wie möglich in diesem Callback anstelle des Konstruktors implementieren sollten.disconnectedCallback()
: wird jedes Mal aufgerufen, wenn das Element aus dem Dokument entfernt wird.adoptedCallback()
: wird jedes Mal aufgerufen, wenn das Element in ein neues Dokument verschoben wird.attributeChangedCallback()
: wird aufgerufen, wenn Attribute geändert, hinzugefügt, entfernt oder ersetzt werden. Siehe Reaktion auf Attributänderungen für weitere Details zu diesem Callback.
Hier ist ein minimales benutzerdefiniertes Element, das diese Lebenszyklusereignisse protokolliert:
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["color", "size"];
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
console.log("Custom element added to page.");
}
disconnectedCallback() {
console.log("Custom element removed from page.");
}
adoptedCallback() {
console.log("Custom element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} has changed.`);
}
}
customElements.define("my-custom-element", MyCustomElement);
Registrieren eines benutzerdefinierten Elements
Um ein benutzerdefiniertes Element in einer Seite verfügbar zu machen, rufen Sie die define()
-Methode von Window.customElements
auf.
Die define()
-Methode nimmt die folgenden Argumente entgegen:
name
-
Der Name des Elements. Dieser muss mit einem Kleinbuchstaben beginnen, einen Bindestrich enthalten und bestimmte andere Regeln erfüllen, die in der Spezifikation der Definition eines gültigen Namens aufgelistet sind.
constructor
-
Die Konstruktorfunktion des benutzerdefinierten Elements.
options
-
Nur für angepasste eingebaute Elemente enthalten, ist dies ein Objekt, das eine einzige Eigenschaft
extends
enthält, die ein String ist, welcher das zu erweiternde eingebaute Element benennt.
Zum Beispiel registriert dieser Code das WordCount
angepasste eingebaute Element:
customElements.define("word-count", WordCount, { extends: "p" });
Dieser Code registriert das PopupInfo
autonome benutzerdefinierte Element:
customElements.define("popup-info", PopupInfo);
Verwendung eines benutzerdefinierten Elements
Nachdem Sie ein benutzerdefiniertes Element definiert und registriert haben, können Sie es in Ihrem Code verwenden.
Um ein angepasstes eingebautes Element zu verwenden, verwenden Sie das eingebaute Element, aber mit dem benutzerdefinierten Namen als Wert des is
-Attributs:
<p is="word-count"></p>
Um ein autonomes benutzerdefiniertes Element zu verwenden, verwenden Sie den benutzerdefinierten Namen wie bei einem eingebauten HTML-Element:
<popup-info>
<!-- content of the element -->
</popup-info>
Reaktion auf Attributänderungen
Wie eingebaute Elemente können benutzerdefinierte Elemente HTML-Attribute verwenden, um das Verhalten des Elements zu konfigurieren. Um Attribute effektiv zu nutzen, muss ein Element auf Änderungen des Attributwerts reagieren können. Dazu muss ein benutzerdefiniertes Element die folgenden Mitglieder zur Klasse hinzufügen, die das benutzerdefinierte Element implementiert:
- Eine statische Eigenschaft namens
observedAttributes
. Diese muss ein Array enthalten, das die Namen aller Attribute enthält, für die das Element Änderungsbenachrichtigungen benötigt. - Eine Implementierung des
attributeChangedCallback()
-Lebenszyklus-Callbacks.
Das attributeChangedCallback()
-Callback wird dann immer aufgerufen, wenn ein Attribut, dessen Name im observedAttributes
-Eigentum des Elements aufgelistet ist, hinzugefügt, geändert, entfernt oder ersetzt wird.
Dem Callback werden drei Argumente übergeben:
- Der Name des Attributs, das sich geändert hat.
- Der alte Wert des Attributs.
- Der neue Wert des Attributs.
Zum Beispiel wird dieses autonome Element ein size
-Attribut beobachten und die alten und neuen Werte protokollieren, wenn sie sich ändern:
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["size"];
constructor() {
super();
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`Attribute ${name} has changed from ${oldValue} to ${newValue}.`,
);
}
}
customElements.define("my-custom-element", MyCustomElement);
Beachten Sie, dass, wenn die HTML-Deklaration des Elements ein beobachtetes Attribut enthält, dann attributeChangedCallback()
aufgerufen wird, nachdem das Attribut initialisiert wurde, wenn die Deklaration des Elements zum ersten Mal geparst wird. In folgendem Beispiel wird attributeChangedCallback()
aufgerufen, wenn das DOM geparst wird, auch wenn sich das Attribut nie wieder ändert:
<my-custom-element size="100"></my-custom-element>
Ein vollständiges Beispiel, das die Verwendung von attributeChangedCallback()
zeigt, finden Sie unter Lebenszyklus-Callbacks auf dieser Seite.
Benutzerdefinierte Zustände und CSS-Selektoren für benutzerdefinierte Zustands-Pseudo-Klassen
Eingebaute HTML-Elemente können verschiedene Zustände haben, wie "hover", "disabled" und "read only". Einige dieser Zustände können als Attribute mit HTML oder JavaScript gesetzt werden, während andere intern sind und nicht. Ob extern oder intern, meistens gibt es entsprechende CSS Pseudo-Klassen, die verwendet werden können, um das Element auszuwählen und zu stylen, wenn es sich in einem bestimmten Zustand befindet.
Autonome benutzerdefinierte Elemente (jedoch nicht auf eingebauten Elementen basierende Elemente) erlauben es Ihnen auch, Zustände zu definieren und diese mit der :state()
-Pseudo-Klassenfunktion zu wählen.
Der folgende Code zeigt, wie dies funktioniert, anhand des Beispiels eines autonomen benutzerdefinierten Elements, das einen internen "collapsed"
-Zustand hat.
Der collapsed
-Zustand wird als logische Eigenschaft (mit Setter- und Getter-Methoden) dargestellt, die außerhalb des Elements nicht sichtbar ist.
Um diesen Zustand in CSS auswählbar zu machen, ruft das benutzerdefinierte Element zuerst HTMLElement.attachInternals()
im Konstruktor auf, um ein ElementInternals
-Objekt anzuhängen, das wiederum Zugriff auf ein CustomStateSet
über die ElementInternals.states
-Eigenschaft bietet.
Der Setter für den (internen) collapsed
-Zustand fügt der CustomStateSet
-Liste den Bezeichner hidden
hinzu, wenn der Zustand true
ist, und entfernt ihn, wenn der Zustand false
ist. Der Bezeichner ist nur ein String: in diesem Fall haben wir ihn hidden
genannt, aber wir hätten ihn genauso gut collapsed
nennen können.
class MyCustomElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
get collapsed() {
return this._internals.states.has("hidden");
}
set collapsed(flag) {
if (flag) {
// Existence of identifier corresponds to "true"
this._internals.states.add("hidden");
} else {
// Absence of identifier corresponds to "false"
this._internals.states.delete("hidden");
}
}
}
// Register the custom element
customElements.define("my-custom-element", MyCustomElement);
Wir können den zum CustomStateSet
des benutzerdefinierten Elements (this._internals.states
) hinzugefügten Bezeichner verwenden, um den benutzerdefinierten Zustand des Elements abzugleichen. Dies geschieht, indem der Bezeichner an die :state()
-Pseudo-Klasse übergeben wird. Zum Beispiel selektieren wir unten den hidden
-Zustand, der true
ist (und damit den collapsed
-Zustand des Elements), mit dem :hidden
-Selektor und entfernen den Rand.
my-custom-element {
border: dashed red;
}
my-custom-element:state(hidden) {
border: none;
}
Die :state()
-Pseudo-Klasse kann auch innerhalb der :host()
-Pseudo-Klassenfunktion verwendet werden, um einen benutzerdefinierten Zustand innerhalb des Schatten-DOMs eines benutzerdefinierten Elements zu erfassen. Zusätzlich kann die :state()
-Pseudo-Klasse nach dem ::part()
-Pseudo-Element verwendet werden, um die Schatten-Parts eines benutzerdefinierten Elements in einem bestimmten Zustand zu erfassen.
Es gibt mehrere praktische Beispiele in CustomStateSet
, die zeigen, wie dies funktioniert.
Beispiele
Im Rest dieses Leitfadens werden wir uns einige Beispiele für benutzerdefinierte Elemente ansehen. Sie finden den Quellcode für alle diese Beispiele und mehr im web-components-examples-Repository, und Sie können sie alle live unter https://mdn.github.io/web-components-examples/ sehen.
Ein autonomes benutzerdefiniertes Element
Zuerst werden wir uns ein autonomes benutzerdefiniertes Element ansehen. Das <popup-info>
-Benutzerdefinierte Element nimmt ein Bild-Symbol und eine Textzeichenfolge als Attribute und bettet das Symbol in die Seite ein. Wenn das Symbol fokussiert wird, zeigt es den Text in einem Popup-Informationsfeld an, um zusätzliche kontextbezogene Informationen bereitzustellen.
Zuerst definiert die JavaScript-Datei eine Klasse namens PopupInfo
, die die HTMLElement
-Klasse erweitert.
// Create a class for the element
class PopupInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
// Create a shadow root
const shadow = this.attachShadow({ mode: "open" });
// Create spans
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
const info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
const text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
}
const img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// Create some CSS to apply to the shadow dom
const style = document.createElement("style");
console.log(style.isConnected);
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`;
// Attach the created elements to the shadow dom
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
Die Klassen-Definition enthält den constructor()
für die Klasse, die immer mit dem Aufruf von super()
beginnt, um sicherzustellen, dass die richtige Prototyp-Kette etabliert ist.
Innerhalb der Methode connectedCallback()
definieren wir die gesamte Funktionalität, die das Element haben wird, wenn es mit dem DOM verbunden wird. In diesem Fall fügen wir dem benutzerdefinierten Element einen Schatten-DOM hinzu, verwenden einige DOM-Manipulationen, um die interne Struktur des Schatten-DOMs des Elements zu erstellen – diese wird dann am Schatten-DOM angehängt – und fügen schließlich etwas CSS hinzu, um es zu stylen. Wir erledigen diese Arbeit nicht im Konstruktor, da die Attribute eines Elements nicht verfügbar sind, bis es mit dem DOM verbunden ist.
Schließlich registrieren wir unser benutzerdefiniertes Element im CustomElementRegistry
mit der define()
-Methode, die wir vorher erwähnt haben — in den Parametern geben wir den Elementnamen und dann den Klassennamen an, der seine Funktionalität definiert:
customElements.define("popup-info", PopupInfo);
Es ist nun verfügbar, um auf unserer Seite verwendet zu werden. In unserem HTML verwenden wir es wie folgt:
<popup-info
img="img/alt.png"
data-text="Your card validation code (CVC)
is an extra security feature — it is the last 3 or 4 numbers on the
back of your card."></popup-info>
Externe Styles referenzieren
Im obigen Beispiel wenden wir Styles auf das Schatten-DOM mithilfe eines <style>
-Elements an, aber Sie können über ein <link>
-Element auf ein externes Stylesheet verweisen. In diesem Beispiel werden wir das <popup-info>
benutzerdefinierte Element anpassen, um ein externes Stylesheet zu verwenden.
Hier ist die Klassen-Definition:
// Create a class for the element
class PopupInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
// Create a shadow root
const shadow = this.attachShadow({ mode: "open" });
// Create spans
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
const info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
const text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
}
const img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// Apply external styles to the shadow dom
const linkElem = document.createElement("link");
linkElem.setAttribute("rel", "stylesheet");
linkElem.setAttribute("href", "style.css");
// Attach the created elements to the shadow dom
shadow.appendChild(linkElem);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
Es ist wie das ursprüngliche <popup-info>
-Beispiel, außer dass wir auf ein externes Stylesheet über ein <link>
-Element verweisen, das wir dem Schatten-DOM hinzufügen.
Beachten Sie, dass <link>
-Elemente das Rendern des Schatten-DOMs nicht blockieren, sodass es möglicherweise ein kurzes Aufblitzen von ungestyltem Inhalt (FOUC) während des Ladevorgangs des Stylesheets geben kann.
Viele moderne Browser implementieren eine Optimierung für <style>
-Tags, die entweder von einem gemeinsamen Knoten geklont oder identischen Text haben, um es ihnen zu ermöglichen, ein einzelnes zugrunde liegendes Stylesheet zu teilen. Mit dieser Optimierung sollte die Leistung externer und interner Styles ähnlich sein.
Angepasste eingebaute Elemente
Jetzt werfen wir einen Blick auf ein Beispiel für ein angepasstes eingebautes Element. Dieses Beispiel erweitert das eingebaute <ul>
-Element, um das Erweitern und Kollabieren der Listenelemente zu unterstützen.
Hinweis:
Bitte beachten Sie den is
-Attribut-Referenz für Hinweise zur Implementierungsrealität von benutzerdefinierten eingebauten Elementen.
Zuerst definieren wir die Klasse des Elements:
// Create a class for the element
class ExpandingList extends HTMLUListElement {
constructor() {
// Always call super first in constructor
// Return value from super() is a reference to this element
self = super();
}
connectedCallback() {
// Get ul and li elements that are a child of this custom ul element
// li elements can be containers if they have uls within them
const uls = Array.from(self.querySelectorAll("ul"));
const lis = Array.from(self.querySelectorAll("li"));
// Hide all child uls
// These lists will be shown when the user clicks a higher level container
uls.forEach((ul) => {
ul.style.display = "none";
});
// Look through each li element in the ul
lis.forEach((li) => {
// If this li has a ul as a child, decorate it and add a click handler
if (li.querySelectorAll("ul").length > 0) {
// Add an attribute which can be used by the style
// to show an open or closed icon
li.setAttribute("class", "closed");
// Wrap the li element's text in a new span element
// so we can assign style and event handlers to the span
const childText = li.childNodes[0];
const newSpan = document.createElement("span");
// Copy text from li to span, set cursor style
newSpan.textContent = childText.textContent;
newSpan.style.cursor = "pointer";
// Add click handler to this span
newSpan.addEventListener("click", (e) => {
// next sibling to the span should be the ul
const nextUl = e.target.nextElementSibling;
// Toggle visible state and update class attribute on ul
if (nextUl.style.display == "block") {
nextUl.style.display = "none";
nextUl.parentNode.setAttribute("class", "closed");
} else {
nextUl.style.display = "block";
nextUl.parentNode.setAttribute("class", "open");
}
});
// Add the span and remove the bare text node from the li
childText.parentNode.insertBefore(newSpan, childText);
childText.parentNode.removeChild(childText);
}
});
}
}
Beachten Sie, dass wir diesmal von HTMLUListElement
, anstelle von HTMLElement
erben. Das bedeutet, dass wir das Standardverhalten einer Liste erhalten und nur unsere eigenen Anpassungen implementieren müssen.
Wie zuvor befindet sich der Großteil des Codes im connectedCallback()
-Lebenszyklus-Callback.
Als nächstes registrieren wir das Element mit der define()
-Methode wie zuvor, mit dem Unterschied, dass dieses Mal auch ein Optionsobjekt enthalten ist, das angibt, welches Element unser benutzerdefiniertes Element erbt:
customElements.define("expanding-list", ExpandingList, { extends: "ul" });
Die Verwendung des eingebauten Elements in einem Webdokument sieht ebenfalls etwas anders aus:
<ul is="expanding-list">
…
</ul>
Sie verwenden ein <ul>
-Element wie gewohnt, geben jedoch den Namen des benutzerdefinierten Elements innerhalb des is
-Attributs an.
Beachten Sie, dass wir in diesem Fall sicherstellen müssen, dass das Skript, das unser benutzerdefiniertes Element definiert, ausgeführt wird, nachdem das DOM vollständig geparst wurde, da connectedCallback()
sofort aufgerufen wird, sobald die erweiterbare Liste dem DOM hinzugefügt wird. Zu diesem Zeitpunkt sind ihre Kinder noch nicht hinzugefügt worden, sodass die querySelectorAll()
-Aufrufe keine Elemente finden werden. Eine Möglichkeit, dies sicherzustellen, besteht darin, das defer-Attribut zur Zeile, die das Skript einbindet, hinzuzufügen:
<script src="main.js" defer></script>
Lebenszyklus-Callbacks
Bisher haben wir nur einen Lebenszyklus-Callback in Aktion gesehen: connectedCallback()
. Im letzten Beispiel, <custom-square>
, sehen wir einige der anderen. Das <custom-square>
autonome benutzerdefinierte Element zeichnet ein Quadrat, dessen Größe und Farbe durch zwei Attribute, genannt "size"
und "color"
, bestimmt werden.
Im Klassen-Konstruktor fügen wir dem Element ein Schatten-DOM hinzu und dann leere <div>
- und <style>
-Elemente am Schatten-DOM ein:
constructor() {
// Always call super first in constructor
super();
const shadow = this.attachShadow({ mode: "open" });
const div = document.createElement("div");
const style = document.createElement("style");
shadow.appendChild(style);
shadow.appendChild(div);
}
Die Schlüsselfunktion in diesem Beispiel ist updateStyle()
— diese nimmt ein Element, holt seinen Schatten-DOM, findet sein <style>
-Element und fügt width
, height
und background-color
zum Stylesheet hinzu.
function updateStyle(elem) {
const shadow = elem.shadowRoot;
shadow.querySelector("style").textContent = `
div {
width: ${elem.getAttribute("size")}px;
height: ${elem.getAttribute("size")}px;
background-color: ${elem.getAttribute("color")};
}
`;
}
Die eigentlichen Aktualisierungen werden alle von den Lebenszyklus-Callbacks behandelt. Das connectedCallback()
wird jedes Mal ausgeführt, wenn das Element dem DOM hinzugefügt wird — hier führen wir die updateStyle()
-Funktion aus, um sicherzustellen, dass das Quadrat so gestylt ist, wie es in seinen Attributen definiert ist:
connectedCallback() {
console.log("Custom square element added to page.");
updateStyle(this);
}
Die disconnectedCallback()
- und adoptedCallback()
-Callbacks protokollieren Nachrichten in die Konsole, um uns zu informieren, wenn das Element entweder aus dem DOM entfernt oder auf eine andere Seite verschoben wird:
disconnectedCallback() {
console.log("Custom square element removed from page.");
}
adoptedCallback() {
console.log("Custom square element moved to new page.");
}
Das attributeChangedCallback()
-Callback wird ausgeführt, wenn eines der Attribute des Elements auf irgendeine Weise geändert wird. Wie Sie aus seinen Parametern sehen können, ist es möglich, auf einzelne Attribute zu reagieren, indem man ihren Namen und die alten und neuen Attributwerte betrachtet. In diesem Fall führen wir jedoch einfach die updateStyle()
-Funktion erneut aus, um sicherzustellen, dass der Stil des Quadrats entsprechend den neuen Werten aktualisiert wird:
attributeChangedCallback(name, oldValue, newValue) {
console.log("Custom square element attributes changed.");
updateStyle(this);
}
Beachten Sie, dass, um das attributeChangedCallback()
-Callback auszuführen, wenn ein Attribut geändert wird, Sie die Attribute beobachten müssen. Dies geschieht, indem Sie eine static get observedAttributes()
-Methode innerhalb der benutzerdefinierten Elementklasse angeben - dies sollte ein Array zurückgeben, das die Namen der Attribute enthält, die Sie beobachten möchten:
static get observedAttributes() {
return ["color", "size"];
}