Code mehrfach zu verwenden – oder Code von anderen Entwicklern zu kopieren und in der eigenen App zu verbauen –, stellt sich oft genug als endloser Struggle heraus. JS-Boilerplates und CSS-Klassen passen nicht und am Ende macht ihr das dann doch lieber selber. In React oder Vue geht das mit dem Wiederverwenden innerhalb des jeweiligen Frameworks zwar etwas besser und mit HTML5 gesellten sich zum Beispiel nützliche Elemente wie <video>
oder <input type="date">
zu den Standardelementen. Man braucht aber nunmal oft mehr als die Standardelemente. Am liebsten hättet ihr diese selbstgebauten Codebits dann in wiederverwendbar und das am besten plattformübergreifend und nicht nur innerhalb eines Projektes. Außerdem kann einfach nicht jedes mehr oder minder häufig benötigte Element ein HTML-Standardelement werden.
Web Components sollen genau diese Lücke schließen. Der Web-Components-Standard setzt sich aus den Standards HTML Templates, Shadow DOM und Custom Elements zusammen. Kombiniert ermöglichen die zugrundeliegenden Standards es, von euch definierte HTML-Elemente an allen Stellen zu nutzen, an denen sonst nur native HTML-Elemente erlaubt sind. Frühe Versionen der Standards gab es schon vor gut fünf Jahren, sie waren aber noch nicht in allen Browsern verfügbar. Mittlerweile werden Web Components aber von allen großen Browsern unterstützt.
HTML-Templates
Das Template-Element gibt es schon länger. Wrappt ihr HTML-Code zwischen zwei template
-Tags, versteckt ihr ihn vor dem Client – und er wird nicht gerendert.
Das template
-Element sorgt dafür, dass der HTML-Code zwar geparst wird, das geparste DOM wird aber nicht eurem Dokument hinzugefügt, sondern davon abgeteilt in einem Document-Fragment gespeichert. Dieser Teil eures Codes „löst sich quasi auf“, sobald ihr ihn einem anderen DOM anhängt:
let template = document.querySelector('template');
document.body.appendChild(template.content);
Beim ersten Rerender bekommt ihr eine Fehlermeldung, weil template.content
weg ist – ihr erinnert euch: Es hat sich aufgelöst. Deshalb müsst ihr zunächst eine Kopie davon machen:
Document.body.appendChild(template.content.cloneNode(true));
Indem ihr den Parameter auf true
setzt, stellt ihr sicher, dass auch alle Kindelemente der Node mitkopiert werden. Der Template-Tag ist ideal für Situationen, in denen ihr eine HTML-Struktur mehrfach verwenden wollt, sie aber nicht zum aktuellen DOM hinzugefügt werden soll.
Custom Elements
Mit dem Custom-Elements-Standard könnt ihr eure eigenen HTML-Elemente definieren. Die ganze Geschichte basiert im Wesentlichen auf der Klassen-Syntax von ES6, das sieht ungefähr so aus:
class GreetingElement extends T3N {
// class definition goes here
}
Für die Custom Elements würde das Ganze so aussehen:
class GreetingElement extends HTMLElement{}
Vor den Web Components würde euer Browser jetzt einen Error ausspucken, man konnte HTMLElement
nicht extenden. Euer Browser weiß, dass ein h1
-Tag zur HTMLHeading1-Klasse gehört, aber woher soll er wissen, welchen Tag er einer selbstdefinierten Element-Klasse zuordnen soll? Zusätzlich den Klassen der nativen HTML-Elemente gibt es extra dafür ein „Custom Element Register“, euer Element könnt ihr so hinzufügen:
customElements.define(‘greeting-element‘, GreetingElement);
So stellt ihr sicher, dass der Browser jeden geparsten greeting-element
-Tag mit einer neuen Instanz von GreetingElement
assoziiert. Übrigens: Eure Custom Elements müssen einen Bindestrich enthalten, um sie in der Benennung von den nativen HTML-Elementen abzugrenzen. Vergesst also am besten die Idee, eine h7
zu machen – das geht nicht. Außer, dass der Constructor jedes Mal, wenn euer Custom Element gerendert werden soll, aufgerufen wird, gibt es noch eine Reihe zusätzlicher Lifecycle-Methoden:
- connectedCallback – wird aufgerufen, wenn ein Element an ein Dokument angefügt wird. Das kann öfter als ein Mal vorkommen, zum Beispiel, wenn es bewegt oder entfernt und wieder hinzugefügt wird.
- disconnectedCallback – ist das Gegenstück zu connectedCallback.
- attributeChangeCallback – feuert immer dann, wenn sich Attribute ändern.
Würdet ihr das Element auf einer Seite nutzen wollen, sähe das so aus:
Und native HTML-Elemente?
Wollt ihr ein natives HTML-Element extenden, sieht das Markup etwas anders aus. Wenn ihr zum Beispiel wollt, dass euer GreetingElement
ein Button ist, würde das so aussehen:
class GreetingElement extends HTMLButtonElement
Dann müsst ihr noch festlegen, dass es sich hierbei um ein natives Element handelt, das geht so:
customElements.define('hello-there', GreetingElement, { extends: 'button' });
Weil es sich um ein natives HTML-Element handelt, nutzt ihr an dieser Stelle den button
-Tag anstelle eures Custom-Tags. Außerdem braucht ihr das is
-Attribut, sonst weiß der Browser nicht, dass euer Custom-Element eine Art Button ist.
Klingt vielleicht erstmal kompliziert, muss aber so. Auch assistive Technologien orientieren sich zum Beispiel an diesem Markup. Jetzt könnt ihr Styling und EventHandler hinzufügen oder das Ganze in template
-Tags wrappen, wenn ihr wollt. Andere können euer Custom Element in ihrem eigenen Code verwenden – über HTML-Templating, DOM-Calls und sogar ziemlich problemlos in bekannten Frameworks wie zum Beispiel AngularJS.
Zusammengefasst bringen die Custom Elements folgende Vorteile:
- Die Möglichkeit, die HTMLElement-Klasse und ihre Unterklassen zu extenden.
- Ein Register für die Custom Elements – über
customElements.define()
werden CustomElements dort abgelegt. - Eigene Lifecycle-Callbacks, über die ihr zum Beispiel Attributveränderungen, Element-Bildung und das Hinzufügen zum DOM verfolgen könnt.
Shadow DOM
Die Besonderheit an euren Custom Elements ist, dass ihr sie auch in anderen Anwendungen wiederverwenden wollt und eigentlich auch andere Entwickler sie wiederverwenden können sollen. Den Shadow DOM gibt es, um Konflikte zwischen Custom Element und CSS anderer Websites oder Apps zu umgehen.
Alle Elemente und Styles innerhalb eines HTML-Dokuments und innerhalb des DOM befinden sich in einem globalen Scope. Auf jedes Element auf einer Seite könnt ihr über document.querySelector()
zugreifen und auf das Dokument angewandte CSS-Styles können jedes Element auswählen, egal, wo es sich befindet. Das ist cool, wenn ihr Styles auf das gesamte Dokument anwenden wollt – zum Beispiel, um an alle Elemente das gleiche box-sizing
zu vergeben.
Manchmal ist das aber auch nicht cool. Manchmal braucht ihr Elemente, die nicht von globalen Styles betroffen sind, wie zum Beispiel ein Third-Party-Widget wie der Teilen-Button für Facebook oder ein Follow-Button für Twitter. Über die Verwendung eines iframe
kann sichergestellt werden, dass das Widget nicht vom CSS des Dokuments behelligt wird, in das ihr es einbinden wollt.
Genau an dieser Stelle kommt das Shadow DOM ins Spiel. Das Shadow DOM ist quasi ein DOM innerhalb des DOM – ein eigenes DOM mit eigenen Elementen und Styles, komplett unabhängig vom DOM eures Dokuments. Das Shadow DOM liegt auch Form-Elementen wie zum Beispiel dem range-input-Element oder auch Radio-Buttons zugrunde: Für ein range-input müsst ihr zum Beispiel einfach input type=“range“
schreiben und bekommt dann einen Slider. Das input
-Element selbst besteht allerdings aus mehreren kleineren div
s, über die Track und Slider kontrolliert werden. Euer Dokument und alles, was sich darin befindet, kann nur das input
-Element sehen, nicht die Elemente und Styles, die für Aussehen und Funktionalität sorgen. Erreicht wird das über den Shadow DOM.
Und wie funktioniert das Ganze konkret?
Um zu illustrieren, wie der Shadow DOM funktioniert, bauen wir einen Twitter-Button – ohne iframe
– mit dem Shadow DOM. Dafür braucht ihr zuerst den Shadow Host – das native HTML-Element innerhalb des globalen DOM, an das wir unseren Shadow DOM anhängen wollen.
Im Beispiel hängen wir den Shadow DOM an ein span
. Den Shadow DOM einfach an den Link zu hängen, was das Naheliegende wäre, funktioniert nicht, weil manche HTML-Elemente kein Shadow Host sein dürfen. Diese Restriktion betrifft vor allem interaktive Elemente. Um den Shadow DOM an den span
anzuhängen, nutzen wir die attachShadow()
-Methode.
const shadowEl = document.querySelector(".shadow-host");
const shadow = shadowEl.attachShadow({mode: 'open'});
So entsteht eine leere Shadow Root, die das Kindelement des Shadow Host ist. Die Shadow Root ist quasi der Anfang des Shadow DOM, so wie das -Element der Beginn des regulären DOM ist. Dann wollen wir unseren Shadow-Komponentenbaum ausbauen. Der Shadow-Tree ist genau wie ein DOM-Tree, nur für das Shadow-DOM. Für einen Twitter-Button braucht ihr nur ein neues
<a>
-Element – eigentlich genau wie der Link im vorherigen Beispiel. Stylen könnt ihr den Button, indem ihr ein Style-Element erstellt, die gewünschten Styles definiert und es dann über die appendChild()
-Methode dem Shadow DOM hinzufügt. Das gesamte Beispiel findet ihr bei Codepen.io.
Kombiniert ihr die Standards, sieht der Workflow für die Erstellung und Verwendung der Web Components ungefähr so aus:
- Ihr erstellt eine JavaScript-Klasse (oder -Funktion), in der ihr die Funktionalität eurer Web Components spezifiziert.
- Ihr registriert euer neues Custom Element über die
CustomElementRegistry.define()
-Methode, definiert das name-Attribut und die Klasse oder Funktion, die seine Funktionalität spezifiziert und optional, von welchem Element es vererbt. - Falls benötigt, fügt ihr mit der
Element.attachShadow()
-Methode einen Shadow DOM an euer Custom Element an. Kind-Elemente, Event-Listener und alles weitere fügt ihr wie gehabt mit regulären DOM-Methoden hinzu. - Anschließend könnt ihr ein HTML-Temlate mittels
<template>
und<slot>
definieren, wenn ihr wollt. Zum Klonen des Templates verwendet ihr, wie in Beispiel 4 ersichtlich, reguläre DOM-Methoden. - Jetzt lässt sich euer Custom Element verwenden wie jedes reguläre HTML-Element, für euch und für alle anderen, die Verwendung dafür finden.
Weitere detaillierte Tutorials und Definitionen findet ihr zum Beispiel bei MDN oder in den Web Fundamentals Guidelines von Google.