Ich hatte schon vor einiger Zeit mit React experimentiert und finde das Konzept davon auch gut. Jetzt bin ich aber auf den Beitrag Web Components will replace your frontend framework gestoßen und dadurch mit Web Components in Berührung gekommen.
Kurz gesagt, handelt es sich bei Web Components um selbstdefinierte HTML-Tags, deren Inhalt und Verhalten man mit Javascript beeinflussen kann. Somit bietet schon HTML die entsprechenden Möglichkeiten, wie man sie bei React mit den Components bekommt.
- Kapselung
- eigene Attribute/Methoden
- <template>: für eine saubere Trennung von HTML, CSS und Javascript und einer Nutzung von HTML-Generatoren
Grundsätzlicher Aufbau
Die Definition von Web Components erfolgt mittels Javascript über die
CustomElementRegistry,
welche über window.customElements erreichbar ist. Die Methode
define
muss mit dem Namen des neuen HTML-Tags und einer Javascript-Klasse für die
Funktionalität aufgerufen werden. Der Name muss mit einem Buchstaben beginnen
und mindestens einen Bindestrich enthalten. Abgesehen von diesen Beschränkungen
sind alle Spielereien wie <math-α>
und <emoji-🙂>
möglich.
Die neue Klasse muss von der Klasse HTMLElement abgeleitet werden und im
Konstruktor muss als erster Aufruf super()
stehen. Danach kann der neue
HTML-Tag ganz normal verwendet werden1:
-
Die Beispiele hier im Dokument sind so komplakt, dass man ein neues Fenster mit der URL
data:text/html,
(mit abschließendem Komma) öffnen und den Code in der Konsole (Strg+K
) ausführen und damit experimentieren kann. MitF5
kann man jederzeit alles zurücksetzen und über die Historie die gewünschten, alten Befehle aufrufen. ↩
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.innerHTML = 'This is a <strong>new</strong> component';
}
}
window.customElements.define('my-component', MyComponent);
document.body.innerHTML = '<my-component></my-component>';
document.body.appendChild(document.createElement('my-component'));
Elemente, die im HTML bereits für der Registrierung enthalten sind, werden mit dem Aufruf von define in eine Web Component umgewandelt. Davor werden sie wie unbekannte HTML-Tags wie ein <span> verarbeitet.
An den Konstruktur werden einige Anforderungen gestellt: Zu beachten ist, dass keine neuen Elemente erstellt werden, was die Veränderungen von innerHTML und dem Style sehr einschränkt. Diese Änderungen müssen zu einem späteren Zeitpunkt (siehe Elemente der Klasse) erfolgen, ansonsten führt dies zu merkwürdigen und nichtssagenden Fehlermeldungen meist beim Erzeugen des Elements. Auf den ShadowDOM wiederum darf zugegriffen werden und es ist auch sinnvoll, ihn gleich im Konstruktor zu initialisieren, wenn man damit arbeiten will. Außerdem wird empfohlen, die Eventlistener im Konstruktor zu registrieren.
Shadow DOM oder innerHTML
Ziel der Web Components ist es, eine Kapsel zu bieten, sodass es mit der Außenwelt zu keinen Konflikten bzgl. IDs oder CSS kommen kann. Hierzu muss man für den Inhalt des neuen Elements einen Shadow DOM einrichten, der unabhängig vom Rest des HTMLs operiert. Auf diese Weise gibt es keine Konflikte, wenn mehrere Instanzen des neuen Elements innerhalb immer die selben IDs verwenden. CSS von außerhalb wird nicht innerhalb angewandt und umgekehrt.
Den Shadow
DOM
erstellt man mit dem Aufruf this.attachShadow({mode: 'open'})
, kann dann mit
this.shadowRoot
darauf zugreifen und damit wie mit document arbeiten.
Man muss nicht mit einem Shadow DOM arbeiten, sondern kann auch innerHTML
verwenden. Wenn man bestehende HTML-Tags erweitert, kann man auch nicht für
alle Elemente einen Shadow DOM
nutzen
und this.innerHTML
muss verwendet werden. Damit fehlt allerdings die Kapselung
bzgl. IDs und CSS.
CSS im Shadow DOM
Für den Shadow DOM gibt es einige spezielle
CSS-Selectoren.
Nützlich ist davon :host
, um innerhalb des Elements das Element selbst
anzusprechen. Dies ist insbesondere notwendig, wenn man bestehende HTML-Tags
erweitert.
window.customElements.define('my-component', class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.innerHTML = `
<style>
:host { background: blue; }
p { background: orange; }
</style>
<p>inner</p>`;
}
});
document.body.innerHTML = `
<style>
p { background: green; }
my-component { background: red; }
</style>
<p>outer</p>
<my-component></my-component>
`;
CSS-Variablen für anpassbare Gestaltung
Um eine gewisse Flexibilität bei der Gestaltung zu erreichen, dann man CSS-Variablen nutzen:
window.customElements.define('my-component', class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.innerHTML = `
<style>
:host { --bg: blue; } /* Default value, if not set outside */
p { background: var(--bg); }
</style>
<p>inner</p>`;
}
});
document.body.innerHTML = `
<style>
p { background: green; }
my-component { --bg: red; }
</style>
<my-component></my-component>
`;
Elemente im Shadow-DOM mit ::part addressieren
TODO: ::part() - CSS: Cascading Style Sheets
Externe Style-Sheets in Web Components
Da der Shadow DOM eine Barriere für das CSS darstellt, kann man Style-Sheets nicht in <head> laden, sondern muss diese innerhalb des Shadow DOMs einbinden. Dabei kann man aber bei der Initialisierung in <head> die Datei vorladen lassen.
const polyfillCss = document.createElement('link');
const link = document.createElement('link');
link.rel = "preload";
polyfillCss.rel = link.as = "stylesheet";
polyfillCss.type = link.type = "text/css";
polyfillCss.href = link.href = 'style.css';
document.head.append(link);
window.customElements.define('my-component', class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.appendChild(polyfillCss.cloneNode(true));
}
});
Warum preload und nicht prefetch der richtige Wert ist, ist hier erklärt.
Event-Target über Shadow-DOM-Grenze hinweg
TODO: Event.composedPath()
Erweitern von bestehenden HTML-Tags
Ein wenig anders sind die Aufrufe, wenn man bestehende HTML-Tags erweitert.
Hierbei muss man eine andere Elternklasse verwenden, bei define() als dritten
Parameter ein Objekt mit extends: '…'
mit dem Ursprungselement angeben und die
Elemente mit dem Attribut is erstellen, das angibt, welches das neue Element
ist.
class MyButton extends HTMLButtonElement {
connectedCallback() {
this.innerHTML = '<strong>inner</strong>';
}
}
customElements.define('my-button', MyButton, { extends: 'button' });
document.body.innerHTML = '<button is=my-button>';
class MyP extends HTMLParagraphElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.innerHTML = `
<style>
:host { border: 2px dashed; }
</style>
inner`;
}
}
customElements.define('my-p', MyP, { extends: 'p' });
document.body.appendChild(document.createElement('p', { is: 'my-p' }));
Anmerkung: <button> ist auch eines der Elemente, das keinen Shadow DOM haben darf, weshalb erst im connectedCallback auf innerHTML zugegriffen werden kann.
Elemente der Klasse
constructor()
: zum Initialisieren des Elements, jedoch düfen keine Änderungen an document passieren; shadowRoot statt innerHTML nutzenstatic get observedAttributes() { ['…']; }
: Liefert die Liste der Attributnamen, für die attributeChangedCallback aufgerufen wirdattributeChangedCallback(name, oldVal, newVal)
: wird für Änderungen der Attributwerte aufgerufen; Attribute lassen sich mit get/set/removeAttribute anpassen:myDialog.setAttribute('modal', 'true')
connectedCallback()
: wird aufgerufen, wenn das Element im DOM eingefügt wirddisconnectedCallback()
: wird aufgerufen, wenn das Element vom DOM entfernt wirdadoptedCallback()
: wird aufgerufen, wenn das Element verschoben wird
Eigene Methoden und Eigenschaften
Die Methoden und Attribute der Klasse sind über das DOM-Element verfügbar. Dies
ermöglicht es, eigene Methoden für komplexe Aktionen (z. B.
updateStateBySelection()
) und (ggf. nur lesbare) Attribute mit get name()
und set name(val)
zu definieren.
Private Variablen
Javascript wurde nicht mit dem Gedanken der objektorientierten Programmierung entwurfen und die OOP-Elemente wie Klassen wurden erst im Nachhinein hinzugefügt. Daher fühlen diese Konstrukte sich auch teilweise eigenartig an und unterstützen (noch) nicht alle Elemente von OOP.
So haben zum Beispiel die Klassen keine privaten Attribute, weil hierfür Javascript schon von Anfang an Kapseln (Closures) biete. Die Erstellung ist auch simpel – außer für Programmierer, die das Closure-Konzept nicht kennen.
const MyComponent = (function() {
let priv = 0;
return class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'})
.innerHTML = `<p>This is number <strong>${++priv}</strong>.</p>`;
}
}
})();
window.customElements.define('my-component', MyComponent);
document.body.innerHTML = '<my-component></my-component><my-component>';
document.body.appendChild(document.createElement('my-component'));
<template> oder HTML-String
Den Inhalt des Shadow DOMs (oder direkt des Elements) kann man entweder mit
innerHTML = '…'
bestimmen oder man nutzt das
Template-Element
im HTML. Mit beiden Wegen lässt sich auch mit <style> entsprechend CSS für
die Teile der Web Component definieren.
Für die Kapselung ergeben sich damit zwei Wege: * Innerhalb einer Javascript-Bibliothek kann das notwendige HTML und CSS für die Komponente in einem String hinterlegt werden, sodass für den Einsatz (und Updates) nur eine Datei angefasst werden muss. Diesen String kann man auch mit entsprechenden Werkzeugen aus einzelnen Dateien beim Zusammenstellen der Bibliothek generieren, um eine saubere Trennung zwischen HTML/CSS/Javascript im Quelltext zu haben. * Die andere Möglichkeit ist den Inhalt in einem Template im HTML zu definieren. Somit kann man die Teile während der Erstellung beeinflussen und Texte und CSS passend zur Sprache und anderen Einstellungen des Nutzers erstellen. Der Code in der Javascript-Bibliothek bleibt statisch und alle variablen Teile sind im HTML und können so generiert werden.
Slots
TODO
Weiter Informationsquellen
- gute Anleitung bei javascript.info
- Anleitung und Beispiele von Mozilla
- Anleitung von Google mit einigen Best-Practices-Vorschlägen