So genannte „Web Components“ sind eine Möglichkeit, wiederverwendbare UI-Komponenten für Web-Anwendungen zu bauen. Anders als bei etablierten Single-Page-App-Frameworks wie React oder Angular basiert das Komponenten-Modell aber auf Web-Standards. Da SPA-Frameworks aber weit mehr leisten als nur Komponenten zu bauen, stehen Web Components nicht in unmittelbarer Konkurrenz zu den etablierten Frameworks. Sie können diese aber sinnvoll ergänzen. Insbesondere dann, wenn Komponenten über Anwendungen mit verschiedenen Technologie-Stacks hinweg wiederverwendet werden sollen, können Web Components einen guten Dienst leisten.
Im Detail verbergen sich aber doch einige Tücken, wenn es um den Einsatz von Web Components in Single-Page-Anwendungen geht: Während die Einbindung in Angular-Anwendungen relativ einfach funktioniert, gibt es insbesondere bei React-Anwendungen einiges zu beachten.
Ob die „Schuld“ hierfür nun bei React oder dem Web-Components-Standard liegt, kommt auf die Perspektive an und ist nicht ganz so leicht zu beantworten. Es gibt aber auch Aspekte, bei denen Web Components auch in ihrer Kernkompetenz, dem Bauen von Komponenten den Kürzeren ziehen. Denn manches ist im Vergleich, z. B. mit React, unnötig kompliziert oder unflexibel.
In dieser Artikelreihe soll es um diese und weitere Aspekte beim Zusammenspiel von Web Components und SPA-Frameworks, insbesondere React, gehen. Im ersten Teil der Reihe liegt der Fokus aber zunächst nur auf Web Components, was sich hinter dem Begriff verbirgt und wie man Web Components baut.
Was sind Web Components und wie baut man eigene Komponenten?
Hinter dem Begriff „Web Components“ verbergen sich mehrere separate HTML-Spezifikationen, die verschiedene Aspekte beim Entwickeln eigener Komponenten behandeln. Es gibt also nicht „den einen“ Standard für Web Components, sondern es handelt sich um eine Kombination von mehreren Spezifikationen.
Die beiden wichtigsten sind „Custom Elements“ und „Shadow DOM“. Die Custom-Elements-Spezifikation beschreibt u. a. die JavaScript-Basis-Klasse „HTMLElement“, von welcher eigene Komponenten abgeleitet werden müssen. Diese Klasse stellt zahlreiche Lifecycle-Methoden bereit, mit denen auf diverse Ereignisse im Lebenszyklus der Komponente reagiert werden kann. Beispielsweise lässt sich programmatisch darauf reagieren, dass die Komponente in einem Dokument eingehangen oder Attribute der Komponente gesetzt wurden. Entwickler und Entwicklerinnen einer Komponente können daraufhin die Darstellung der Komponente aktualisieren. Außerdem gehört zu Custom Elements die Möglichkeit, eigene Komponenten-Klassen unter einem bestimmten HTML-Tag zu registrieren, damit die Komponente anschließend im gesamten Dokument zur Verfügung steht.
Hinter „Shadow-DOM“ verbirgt sich eine Technik, mit der für eine Komponente ein eigener DOM-Baum angelegt werden kann, der vom restlichen Dokument weitestgehend isoliert ist. Das bedeutet, dass zum Beispiel CSS-Eigenschaften, die global im Dokument gesetzt wurden, nicht im Shadow-DOM wirksam sind und in die andere Richtung auch CSS-Definitionen innerhalb einer Komponente keine Auswirkungen auf sonstige Elemente im Dokument haben. Das Ziel ist eine bessere Kapselung der Komponenten und die Vermeidung von unerwünschten Seiteneffekten beim Einbinden von fremden Webkomponenten.
Im folgenden Code-Block ist eine einfache Hallo-Welt-Komponente zu sehen, die ein Property für den Namen der zu grüßenden Person enthält.
class HelloWorld extends HTMLElement { person = "" constructor() { super(); this.attachShadow({mode: "open"}) this.shadowRoot.innerHTML = ` <div> <p>Hello <span id="personGreeting"></span></p> </div> ` } static get observedAttributes() { return ['person'] } attributeChangedCallback(name, oldValue, newValue) { if(name === "person") { if(this.person !== newValue) { this.person = newValue this.update() } } } update() { this.shadowRoot.querySelector("#personGreeting").textContent = this.person } } window.customElements.define("app-hello-world", HelloWorld)
Im Konstruktor der Komponente wird zunächst für die Komponente ein eigener Shadow-DOM-Baum angelegt. Die Angabe „mode: open“ bewirkt, dass trotz der Shadow-DOM-Barriere von außen mittels JavaScript auf den DOM-Baum der Komponente zugegriffen werden kann.
Anschließend wird der „shadowRoot“, also der Root-Knoten des Shadow-DOM, entsprechend unserer Wünsche gestaltet – hier mittels „innerHTML“.
Mit „observedAttributes“ erklären wir, welche Attribute die Komponente haben soll bzw. bei welchen Attributen wir benachrichtigt werden möchten (wir können hier also auch Standard-Attribute wie „class“ angeben).
Die Benachrichtigung findet über die Methode „attributeChangedCallback“ statt, die als Parameter den Namen des geänderten Attributs sowie den alten und neuen Wert erhält. Da wir in unserem Fall nur ein einziges Attribut in „observedAttributes“ angegeben haben, wäre eine Prüfung auf den Namen des Attributs eigentlich nicht notwendig. Bei mehreren Attributen muss aber stets geschaut werden, welches Attribut gerade geändert wurde.
In unserem Fall prüfen wir zunächst, ob sich der neue Wert tatsächlich vom bisherigen unterscheidet (wir werden später noch sehen, wie das zustande kommen kann). Anschließend setzen wir die Property „person“, die wir als Klassenvariable angelegt haben, auf den Wert des übergebenen Attributs.
Um die Darstellung der Komponente zu aktualisieren wurde im Beispiel die Methode „update“ angelegt. Diese gehört nicht zum Custom-Elements-Standard, sondern dient hier nur dazu, die Update-Logik an einer Stelle zu sammeln. Darin holen wir das zuvor angelegte Span-Element mit der ID „person“ aus dem Shadow-DOM und setzen dessen Text auf den Wert der „person“-Property.
Als letzten Schritt sieht man im Code-Beispiel, wie unsere Komponenten-Klasse mit dem Tag-Namen „app-hello-world“ registriert wird. Wichtig ist hier, dass der Name mindestens ein Minus-Zeichen enthält. Diese Regel wurde geschaffen, um mögliche Namens-Kollisionen mit zukünftigen Standard-HTML-Tags zu vermeiden. Es hat sich daher als zweckmäßig erwiesen, ein sinnvolles Präfix für eigene Komponenten zu wählen, um so auch Kollisionen mit anderen Komponenten-Bibliotheken möglichst zu vermeiden (das im Beispiel gewählte Präfix „app“ dürfte in dieser Hinsicht kein gutes Vorbild sein). Ein wirklich sicherer Mechanismus zur Vermeidung von Konflikten existiert jedoch nicht.
Mittels Attribute haben wir nun also die Möglichkeit, einfache Daten in die Komponente hineinzureichen. Beim Thema „Attribute“ gibt es noch einige Besonderheiten und Fallstricke, die wir aber für den nächsten Teil dieser Artikelreihe aufheben wollen. Für diese allgemeine Einführung wollen wir es erst einmal dabei belassen.
Slots
Ein weiteres wichtiges Feature von Web Components, welches uns ebenfalls in einem späteren Teil der Reihe nochmal beschäftigen wird, sind die sogenannten Slots. Damit lassen sich HTML-Schnipsel an eine Komponente übergeben. Die Komponente entscheidet dann, wie sie die übergebenen Elemente darstellt. Wollen wir beispielsweise eine Hinweisbox bauen, die neben einem Text auch ein Icon darstellt und mit einem Rahmen umgibt, bietet es sich an, den Hinweistext nicht als Attribut, sondern mit einem Slot an die Komponente zu geben. Auf diese Weise sind wir nicht nur auf reinen Text beschränkt, sondern können beliebigen HTML-Content nutzen.
In der Anwendung kann das beispielsweise so aussehen:
<app-notification-box> <p>Some Text with additional <strong>tags</strong></p> </app-notification-box>
Wir müssen also nur die gewünschten HTML-Tags als Kindelemente schreiben. Innerhalb der Komponente muss dafür ein <slot>-Element im Shadow-Root auftauchen. Anstelle des Slot-Elements wird beim Rendering der Komponente dann der übergebene Content angezeigt.
<div> <div>Icon</div> <div id="content"> <slot></slot> </div> </div>
Eine Komponente kann auch mehrere Slots enthalten. Damit der Browser aber entscheiden kann, welche HTML-Elemente er welchem Slot zuordnen soll, müssen in diesem Fall sogenannte „Named Slots“ benutzt werden, d. h. die Slots bekommen ein spezielles Name-Attribut. Nur höchstens ein Slot darf innerhalb einer Komponente ohne Name-Attribut vorkommen. Bei diesem spricht man vom „Default Slot“. In der Komponente kann das z. B. so aussehen:
<div> <div id="header"> <h1> <slot name="header"></slot> </h1> </div> <div id="icon"> <slot name="icon"></slot> </div> <div id="content"> <slot><slot> </div> </div>
Die Benutzung könnte dann z. B. so aussehen:
<app-notification-box> <p>Some Content</p> <span slot="header">A meaningful Header</span> <img slot="icon" src="..." alt="notification icon"/> </app-notification-box>
Hier sieht man die Nutzung des „slot“-Attributs. Die Werte müssen zu den „name“-Attributen an den Slots innerhalb der Komponente passen. Folglich gehört dies zum Teil der öffentlichen API einer Komponente und muss entsprechend dokumentiert werden.
Events
Bisher haben wir nur gesehen, wie Daten in Komponenten hineingereicht werden können, jedoch noch nicht den umgekehrten Weg skizziert. Denn um wirklich interaktiv zu sein, müssen Entwickler und Entwicklerinnen auch die Möglichkeit haben, auf bestimmte Ereignisse zu reagieren und Daten von der Komponente entgegenzunehmen.
Für diesen Zweck dienen bei HTML Events. Und auch diesen Aspekt wollen wir uns in diesem Artikel nur kurz anschauen und später genauer unter die Lupe nehmen.
Web Components können sowohl Standard Events als auch Custom Events erzeugen.
Standard-Events sind dann nützlich, wenn die Art des Events so auch schon bei Standard-HTML-Elementen vorkommt und daher nicht neu erfunden werden muss, beispielsweise ein KeyboardEvent. Custom-Events sind dann sinnvoll, wenn zusätzliche Daten als Payload dem Event mitgegeben werden sollen. Wenn wir beispielsweise eine eigene interaktive Tabellenkomponente bauen, in der die Nutzenden einzelne Zeilen selektieren können, bietet es sich möglicherweise an, ein Event bei der Selektion auszulösen , welches als Payload die Daten der gewählten Zeile enthält.
Der Mechanismus zum Auslösen von Events ist für alle Arten von Events gleich. Dies ist im folgenden Code-Block zu sehen:
class InteractiveComponent extends HTMLElement { triggerStandardEvent() { const event = new Event("close") this.dispatchEvent(event) } triggerCustomEvent() { const event = new CustomEvent("some-reason", { detail: { someData: "..." }} ) this.dispatchEvent(event) } }
Zur Erzeugung eines Events wird entweder direkt eine Instanz von „Event“ oder eine der anderen Event-Klassen (zu denen auch „CustomEvent“ gehört) erzeugt. Alle Event-Konstruktoren erwarten als ersten Parameter den Type des Events. Dieser Typ wird später auch benutzt, um Listener für diese Events zu registrieren.
Der zweite Parameter ist optional und stellt ein JavaScript-Objekt dar, welches das Event konfiguriert. Für CustomEvent ist beispielsweise das Feld „detail“ vorgesehen, um beliebige Payload-Daten zu übergeben.
Fazit
Der Artikel gibt eine kurze Einführung in das Thema „Web Components“ und mit den gezeigten Techniken können bereits eigene Komponenten gebaut werden. Natürlich gibt es noch zahlreiche weitere Aspekte, die bei der Entwicklung von Web Components beachtet werden müssen. Nicht umsonst füllt das Thema so manches Fachbuch. In dieser Artikelreihe wollen wir vor allem auf einige Fallstricke eingehen, die bei einzelnen Themen auftreten können und genauer beleuchten, wie diese umgangen werden können. Auch eine kritische Auseinandersetzung mit der Web-Components-API soll Teil dieser Serie werden. Insbesondere das Zusammenspiel mit SPA-Frameworks wird uns in den nächsten Artikeln beschäftigen.