Stephan Böni - Web-Komponenten

Enten

Web-Komponenten

Als Web-Entwickler wissen wir alle, dass es eine gute Idee ist, Code so oft wie möglich wiederzuverwenden. Bei benutzerdefinierten Markup-Strukturen war dies traditionell nicht so einfach. Spätestens seit Januar 2020 bieten uns Web-Komponenten die lang ersehnten Möglichkeiten, solche Probleme zu lösen. Sie bestehen aus mehreren Haupttechnologien, die zusammen verwendet werden können, um vielseitige benutzerdefinierte Elemente mit gekapselter Funktionalität zu erstellen, die überall wiederverwendet werden können, ohne dass du Code-Kollisionen befürchten musst. Gehen wir dies also Schritt für Schritt an.

Benutzerdefinierte Elemente

Damit ein benutzerdefiniertes Element gültig ist, muss es einen Bindestrich enthalten, z.B. <info-item>. Wird es direkt im HTML eingebunden, verhält es sich wie ein <span>.

Es lässt sich auch per Javascript deklarieren: customElements.define("info-item", InfoItem);.

Benutzerdefinierte Elemente sollen die Flut von <span class="xyz"> und <div class="xyz"> in Quellcode reduzieren und diesen so lesbarer machen. Zudem machen sie den Code unabhängier.

Der Einfachheit halber verzichten wir zunächst auf solche benutzerdefinierte Elemente und werden erst am Schluss darauf zurück kommen.

Schatten-DOM

Ein wichtiger Aspekt ist die Kapselung, denn ein einzufügendes Code-Snippet ist per Definition ein Stück wiederverwendbarer Funktionalität: Es kann in jede beliebige Webseite eingefügt werden und man erwartet, dass es funktioniert. Daher ist es wichtig, dass Code, der in der Seite ausgeführt wird, nicht versehentlich bestehende Definitionen zerstören kann, indem er seine interne Implementierung ändert. Mit Shadow DOM kannst du einen unabhängigen DOM-Baum an ein Snippet anhängen und die Interna dieses Baums vor JavaScript und CSS auf der Seite verbergen. Auf den ersten Blick ähnelt dies also einem <iframe>, aber statt der gesamten HTML-Seite wird nur das benötigte Snippet eingebunden.

Üblicherweise wird ein Shadow DOM zusammen mit Vorlagen (<template> und <slot>) verwendet. Beachte dazu das folgende Beispiel.

Vorlagen

Das <template>-Element wird mit all seinen Kindern im Frontend nicht angezeigt. Es gilt als Vorlage zum späteren Gebrauch.

HTML <template id="example1"> <span>Das ist ein Test.</span> </template>

Per Javascript wird es vervielfältigt, mit passendem Inhalt versehen und an geeigneter Stelle eingefügt.

Javascript const templateContent = document.getElementById('example1').content; const placeholders = document.querySelectorAll('p.placeholder'); for (const placeholder of placeholders) { const shadowRoot = placeholder.attachShadow({ mode: "open" }); shadowRoot.appendChild(templateContent.cloneNode(true)) }

Das fügt den Inhalt des <template>-Elements an den Stellen ein, wo sich ein (leeres) <p class="placeholder"> befindet. Das Ergebnis mit zwei solchen Elementen sieht dann folglich so aus:

Das würde auch ohne Shadow DOM funktionieren, solange keine Slots verwendet werden.

Slots

Slots machen die Vorlagen flexibler. Das <slot>-Element ist ein Platzhalter innerhalb einer Vorlage, den du mit deinem eigenen Markup füllen kannst.

HTML <template id="example2"> <span>Das ist ein Test:</span> <slot name="slot1"></slot> <slot name="slot2"></slot> </template> <p class="item"><strong slot="slot1">Slot1 Text</strong></p> <p class="item"><strong slot="slot2">Slot2 Text</strong></p>

Per Javascript wird die Vorlage vervielfältigt, mit passendem Inhalt versehen und an geeigneter Stelle eingefügt.

Javascript const template = document.getElementById('example2').content; const items = document.querySelectorAll('.item'); for (const item of items) { const shadowRoot = item.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.cloneNode(true)) }

Das Ergebnis mit zwei solchen Elementen sieht dann folglich so aus:

Slot1 Text

Slot2 Text

Standard-Styles in Shadow DOM übernehen

Damit das CSS des Dokuments auch im Shodow DOM greift, müsste dieses nochmals eingebunden werden.

Javascript const template = document.getElementById('example2').content; const items = document.querySelectorAll('.item'); for (const item of items) { const shadowRoot = item.attachShadow({ mode: "open" }); const sheet = new CSSStyleSheet(); for (documentCSS of document.styleSheets) { for (rule of documentCSS.cssRules) { sheet.replaceSync(rule.cssText); } } shadowRoot.adoptedStyleSheets = [sheet]; shadowRoot.appendChild(template.cloneNode(true)) }

Nun ist aber zu bedenken, dass der Shadow DOM ein Root-Knoten ist. Bei verschachteltem CSS muss also die gesamte Kaskade innerhalb des Shadow DOM erfüllt sein. In obigem Beispiel würden folglich Styles auf main p nicht greifen, da kein <main>-Element im Shadow DOM enthalten ist. :root p funktioniert jedoch problemlos.

Weiter ist zu bedenken, dass dies den Code unnötig aufbläht. Als Alternative kann man über ::part aus dem Standard-CSS in den Shadow DOM einwirken.

Server-Abfrage

Im nächsten Beispiel bilden wir eine Liste mit drei Items. Wir gehen dabei davon aus, dass wir vom Server die relevanten Daten zu einzelnen Listen-Elemente per JSON-Abfrage erhalten. Dies kann ein Latenz-Problem erzeugen, wenn die Server-Abfrage möglicherweise zu lange dauert. Wir müssen uns also Gedanken machen, was initial angezeigt werden soll und was nachgeladen werden kann. Damit z.B. Crawler-Bots die Links verfolgen, sollten diese initial geladen werden. Bauen wir folglich zuerst ein Placeholder-Item, das mit allen initialen Werten befüllt wird und laden dann erst den restlichen Inhalt nach.

Beginnen wir mit dem Ankerpunkt für den Shadow-Root. Die Verwendung eines bestehenden Elements ist gefährlich, da dieses bereits mit Styles versehen sein kann. Damit die Vorlage bei jeder Website eingebunden werden kann und auch immer gleich aussieht, muss der Ankerpunkt neutral sein. Dazu verwenden wir nun ein benutzerdefiniertes Element names <product-list>. Darin erzeugen wir ein <product-list-item> für jedes Item in der künfigen Liste. Dieses enthält das Linkziel und den Linktext.

HTML <product-list hidden> <product-list-item id="item1" data-href=".">Beispiel 1</product-list-item> <product-list-item id="item2" data-href=".">Beispiel 2</product-list-item> <product-list-item id="item3" data-href=".">Beispiel 3</product-list-item> </product-list>

Die Vorlage haben wir etwas erweitert und ein ausgelagertes CSS eingebunden. Ebenfalls haben wir das Javascript in eine seperate Datei ausgelagert.

HTML <template id="example4"> <style> @import url('component.css'); </style> <li class="loading"> <article> <img loading="lazy" src="placeholder.webp" onclick="this.parentNode.querySelector('a').click();"> <a href="#"></a> <span>&nbsp;</span> </article> </li> </template> <script src="component.js"></script>

Die folgenden Dateien werden in obigem HTML eingebunden.

component.css component.js


Die JSON-Abrage findet dann je Item statt, sobald dieses in den Viewport gelangt. Mit den JSON-Werten modifizieren wir das bestehende Platzhalter-Item. Im untenstehenden Beispiel liefert die Server-Abfrage beim dritten Item ein leeres JSON, weswegen der Platzhalter bestehen bleibt.

Die folgenden Dateien liefert die JSON-Abfrage.

item1.json item2.json item3.json


Und so sieht das Ergebnis aus.


Die folgende Grafik fasst den obigen Ablauf zusammen.

Konzept

Durch die Kapselung ist dieses Beispiel auf jeder Website einbindbar. Dazu musst du aber alle Pfade vollqualifiziert im Quellcode hinterlegen und im CSS die Farbvariabeln deklarieren. Diese werden nämlich in unserem Beispiel von :root geerbt.

Dran bleiben

Du hast es geschafft. Abonniere meine Benachrichtigungen, um weitere News und Anleitungen von mir zu erhalten.

Feed einbinden