Angular, React, Vue.js – alles bekannte und aktuelle Frameworks. Bricht man diese Frameworks auf ihre Grundfunktion herunter, dann kristallisiert sich eine gemeinsame Funktionalität heraus: das Erstellen von komponenten-basierten Single-Page Applications. Ein Blick in die Vergangenheit zeigt, dass auch Frameworks wie jQuery UI oder Bootstrap neben ihren anderen Mehrwertfunktionen die Möglichkeit baten, Komponenten zu entwickeln. Das liegt schlicht daran, dass uns der Browser bisher keine Möglichkeit bot, native Komponenten – also ohne Framework – zu entwickeln. Doch mit Web Components könnte sich das ändern.

Die Motivation hinter Web Components

Wenn wir uns wieder unsere drei großen Frameworks – Angular, React und Vue.js – vornehmen, dann arbeiten wir Entwickler uns in der Regel in genau eines dieser Frameworks intensiv ein, um all seine Features, Vor- und Nachteile zu lernen. Im Falle von Angular gibt es hier eine ganze Menge: Components, Directives, Pipes, RxJS, Dependency Injection, Routing und noch vieles mehr. Konzepte, die so nicht unbedingt in den anderen Frameworks zu finden sind. Dafür finden wir dort wieder andere Features. Als Entwickler bedeutet das, dass wir beim Wechsel des Frameworks wieder viele neue Eigenheiten kennenlernen müssen.

Der Vorteil der Web Components besteht darin, dass wir ein allgemeingültiges Komponentenmodell erhalten. Auch wenn wir den Arbeitsplatz oder die Firma wechseln, das Wissen über Web Components nehmen wir mit, und wir müssen keine neuen Eigenheiten lernen. Wir können einfach zwischen verschiedenen Projekten wechseln und finden uns problemlos zurecht.

Damit uns das ermöglicht wird, müssen wir drei neue Standards lernen:

  1. Custom Elements
  2. HTML Templates
  3. Shadow DOM

Bis vor einiger Zeit wurde noch ein vierter Standard hinzugenommen: HTML Imports. Dieser ist allerdings bereits wieder veraltet und wird stattdessen durch ECMAScript Module Imports ersetzt. Die drei genannten Standards werden wir uns anhand eines kleines Praxisbeispiels näher anschauen. Los geht’s!

Unsere erste Web Component

Bevor wir einen Blick auf diese Standards werfen, klären wir zunächst, was genau wir unter einer Komponente verstehen. Gerade in der IT ist dieser Begriff allgemein geläufig und kann je nach Kontext unterschiedliche Dinge bezeichnen. Im Web umfasst eine Komponente in der Regel drei Dinge:

  1. Template: Ein HTML-Template, was die Struktur vorgibt und die Informationen beinhaltet, die dem Benutzer präsentiert werden sollen.
  2. Styling: In Form von CSS, visuelle Aufbereitung der Informationen für den Benutzer. Die Quelle kann natürlich auch LESS, SASS oder SCSS sein. Der dazugehörige Transpiler übersetzt die Sprache nach CSS.
  3. Code-Behind: In Form von JavaScript, um Benutzereingaben zu erfassen oder der Komponente Interaktivität zu verleihen. Auch hier kann die Quelle auch in Form von TypeScript oder CoffeeScript vorliegen. Beides wird durch einen Transpiler nach JavaScript übersetzt und vom Browser verarbeitet.

Diese drei Dinge werden von einer Komponente umschlossen (engl. encapsulated). Optional ist prinzipiell das Styling, sofern man seine Komponente nicht aufhübschen möchte. Auch das Template ist prinzipiell optional, da man den Inhalt seiner Komponente komplett via JavaScript erstellen kann. Ob das allerdings praktikabel ist, muss der Entwickler für sich selbst entscheiden.

Unsere Komponente, die wir entwickeln werden, wird ein kleiner Zähler sein. Mit einer Anzeige, einem Plus- und einem Minus-Button. Anhand dieses einfachen Beispiels lassen sich viele Konzepte von Web Components zeigen.

Vorbereitung

Bevor wir mit der eigentlichen Entwicklung starten können, müssen wir uns eine kleine Entwicklungsumgebung einrichten. Dazu kann jeder Texteditor, VS Code oder auch WebStorm genutzt werden. Zusätzlich sollte Node.js und Google Chrome installiert sein. In einem neuen Ordner, in dem unsere Entwicklung stattfinden soll, starten wir eine Kommandozeile und führen folgenden Befehl aus:

npm init -y

Dieser Befehl legt eine neue package.json-Datei mit Standardinhalt an. Falls dieser Befehl fehlschlägt, wurde voraussichtlich Node.js nicht (korrekt) installiert. Danach führen wir folgenden Befehl aus:

npm install -D browser-sync

Hierdurch wird ein Tool names Browsersync installiert. Zum einen verwenden wir es als kleinen Webserver, sodass wir unsere Web Component ausprobieren können. Zum anderen lädt es bei Änderungen am Quelltext automatisch den Browser neu, sodass wir immer den aktuellsten Entwicklungsstand sehen können. Damit das funktioniert, öffnen wir die Datei package.json und fügen in der Sektion scripts ein zweites Skript hinzu:

"start": "browser-sync start --server src --files src --no-open"

Dieser Befehl wird Browsersync starten, einen Webserver im Verzeichnis src öffnen (–server) und alle dort enthaltenen Dateien überwachen (–files). Der letzte Parameter –no-open gibt an, dass beim Starten des Befehls nicht automatisch ein Browser gestartet werden soll.

Das Verzeichnis src haben wir noch nicht erstellt, das werden wir jetzt machen und dort eine HTML-Datei mit dem Namen index.html und folgendem Inhalt ablegen:

In dieser Datei wird bereits eine JavaScript-Datei referenziert, die wir noch nicht erstellt haben. Deswegen erstellen wir eine leere Datei mit dem Namen counter.js.

Um die Vorbereitungen abzuschließen, rufen wir in der Kommandozeile den folgenden Befehl auf:

npm start

Hiermit wird unser Browsersync gestartet. In der Kommandozeile wird eine URL ausgegeben, die wir in Google Chrome öffnen. In der Regel ist die URL http://localhost:3000.

1. Standard: Custom Elements

Mit Custom Elements können wir dem Browser neue HTML-Elemente lehren und mit Inhalt unserer Wahl befüllen. Dazu stellt uns der Browser eine sogenannte Custom Elements Registry zur Verfügung. Sie erlaubt uns, neue Elemente zu registrieren, abfragen, ob bestimmte Elemente bereits zur Verfügung stehen oder auf das Vorhandensein von Elementen zu warten.

Um ein eigenes Element im Browser zu registrieren, benötigen wir den folgenden Code als Grundgerüst:

Wir benötigen eine Klasse, die mindestens von der Klasse HTMLElement erbt und den super-Konstruktor aufruft. Zusätzlich muss customElements.define aufgerufen werden, dessen erster Parameter der Name des zu registrierenden Elements/HTML-Tags ist und der zweite die Klasse, die zur Erzeugung genutzt werden soll. Möchte man beispielsweise eine Web Component erstellen, die ein input erweitern soll, kann man statt von HTMLElement auch von HTMLInputElement erben und erhält somit das Standardverhalten des Elements.

Als nächstes können wir das HTML-Element in der index.html in den body einfügen:

Sehen werden wir im Browser aktuell allerdings noch nichts, denn wir haben noch kein HTML-Template hinterlegt.

2. Standard: HTML Templates

In diesem Schritt wollen wir der Komponente ihr Aussehen in Form von HTML-Templates geben. Dazu nutzen wir den folgenden Code, den wir vor die Klasse in counter.js einfügen:

Zuerst erzeugen wir via Code ein template-Element, dem wir via innerHTML CSS-Stile und HTML verpassen. Das template-Element ist eine Vorlage für den Browser. Er parst und evaluiert es, bringt es aber nicht zur Anzeige. Auch wenn wir zum Test das template-Tag via document.body.appendChild(template) in den DOM hängen würden, sehen wir in den DevTools nur, dass der Browser es verarbeitet und ein #document-fragment erzeugt, aber keine Instanz des Templates erzeugt hat (Abbildung 1).

Schauen wir uns das CSS etwas genauer an. Hier sind zwei Dinge auffällig. Zum einen verwenden wir einen :host-Selektor und eine CSS-Variable. Hier greifen wir bereits etwas vor, denn dieser Selektor selektiert den Shadow Host, den wir erst mit der Erzeugung vom Shadow DOM erhalten. Salopp gesprochen, wird damit das HTML-Tag my-counter selbst selektiert. Via CSS aktivieren wir die Flexbox und setzen eine CSS-Variable mit dem Namen --button-square-size. Diese Variable nutzen wir im Anschluss, um unsere Elemente zu stylen. So erhalten wir eine recht entwicklerhübsche Komponente.

Abbildung: HTML Template

Abbildung: HTML Template

Um eine Instanz des Templates zu erzeugen, können wir es probehalber unserer Web Component zuweisen. Dazu benötigen wir den folgenden Code im constructor unmittelbar nach dem super-Aufruf:

this.appendChild(template.content.cloneNode(true));

Wir klonen schlicht den Inhalt vom Template und fügen es als Kind unserer Komponente hinzu. Das true gibt an, dass ein deepClone stattfinden soll, also wirklich jeglicher Inhalt kopiert wird.

Schauen wir uns den aktuellen Zwischenstand an, wird unsere Komponente noch nicht wirklich nach einem Zähler ausschauen (Abbildung 2).

Abbildung: Aktueller Zwischenstand

Das liegt schlicht daran, dass wir via CSS versuchen, den Shadow Host zu selektieren, der aber noch nicht vorhanden ist. Im nächsten Schritt wollen wir genau das ändern!

3. Standard: Shadow DOM

Mit dem letzten Standard können wir unsere bisherige Komponente in eine echte Web Component ändern. Das einzige, was wir hierzu machen müssen, ist die Art und Weise des Klonens vom Template zu ändern. Daher tauschen wir den bisherigen constructor gegen den folgenden aus:

Zuerst erzeugen wir mit attachShadow einen Shadow DOM. Der Modus gibt an, ob man von außen auf die Eigenschaft shadowRoot zugreifen darf, den jedes HTML-Element besitzt. Mit dem Modus open können wir auf diese Eigenschaft zugreifen. Nutzen wir den Modus closed liefert der Browser beim Zugriff auf die Eigenschaft shadowRoot immer null zurück, auch innerhalb unserer Komponente! Das ist auch der Grund, warum wir das Ergebnis des Methodenaufrufs in der Eigenschaft shadow speichern, sodass wir unabhängig vom Modus immer Zugriff auf unseren Shadow DOM haben.

Im nächsten Schritt fügen wir die Kopie des Templates nicht direkt als Kind in unsere Komponente ein, sondern als Kind unseres Shadow DOMs. Wenn wir uns das Ergebnis jetzt im Browser anschauen, sehen wir, wie unsere Komponente tatsächlich aussieht (Abbildung 3). In den DevTools sehen wir auch, dass unsere Komponente einen #shadow-root (open) hat, auch der CSS-Selektor :host wird angewendet, da das Tag my-counter den Shadow Host darstellt.

Übrigens, JavaScript wäre nicht JavaScript, wenn man über kleine Workarounds nicht doch auf einen closed Shadow DOM zugreifen kann. Es sollte allerdings tunlichst vermieden werden, um keine ungewollten Seiteneffekte zu erzeugen.

Abbildung 3: Aktueller Stand der Komponente mit Shadow DOM - Artikel Web Components im Host Europe Blog

Abbildung: Aktueller Stand der Komponente mit Shadow DOM

Soweit, so gut! Wir haben unsere erste Web Component entwickelt. Allerdings kann sie noch nicht viel, da wir ihr noch kein Leben eingehaucht haben. Zeit, das zu ändern!

Events

In diesem Schritt wollen wir auf das Klick-Event der Buttons reagieren, um den Wert der Komponente zu ändern. Das geht mit ganz normalen JavaScript, wie man es von früher gewohnt ist:

Im constructor lesen wir einige Elemente via querySelector auf unsere shadow-Eigenschaft ein und speichern deren Referenzen in den dazugehörigen Eigenschaften. Danach fügen wir via addEventListener jeweils einen Event Listener für das Klick-Event der Button hinzu, die beim Klick entsprechend die Methoden increment oder decrement aufrufen. Innerhalb der Methoden erhöhen oder erniedrigen wir einen Wert.

Den Wert selbst bilden wir in Form von Getter und Setter ab, die wiederum den aktuellen Wert als HTML-Attribute unserer Komponente abspeichern. Zusätzlich ruft der Setter die Methode render auf, die den aktuellen Wert in unserer div zur Anzeige bringt. Wenn wir die Komponente im Browser ausprobieren, können wir den Inhalt via Klicks auf die Buttons ändern (Abbildung 4).

Abbildung 4: Komponente mit Werten - Blogartikel zu Web Components im Host Europe Blog

Abbildung: Komponente mit Werten

Übrigens, wir müssen nicht zwingend unseren Wert als Attribute abspeichern. Wir nutzen hier eine Technik mit den Namen Reflecting properties to attributes (hin und wieder auch als Reflected DOM attributes bezeichnet). Jedes HTML-Element hat einen HTML- und einen JavaScript-State. Diese müssen nicht zwingend den gleichen Wert abbilden. Bei einfachen Dingen wie einer Zahl können wir problemlos den JavaScript-State als HTML-State abbilden. Es gilt allerdings aufzupassen. JavaScript-Eigenschaft können einfache Werte in Form von String/Number sein oder auch komplexe Objekte und Arrays. HTML-Attribute können nur in Form von Strings abgebildet werden. Sprich, bei komplexen Objekten müssten wir diese als JSON-String abbilden, was sehr inperformant werden kann.

Wenn wir in unserer index.html den Wert händisch setzen, bspw. via

<my-counter value=”4”></my-counter>

werden wir nach dem Aktualisieren im Browser feststellen, dass der Wert noch nicht angezeigt wird. Das gleiche gilt auch, wenn wir via DevTools den Wert im HTML ändern, es führt nicht zur Änderung unserer Anzeige. Damit das möglich ist, benötigen wir Lifecycle Events.

Lifecycle Events

In diesem Abschnitt schauen wir uns die zwei wichtigsten Lifecycle Events an, die uns Web Components bieten. Den connectedCallback und den attributeChangedCallback. Die Implementierung sehen wir hier:

Der connectedCallback wird aufgerufen, wenn die Komponente durch den Browser im DOM platziert wird (document.body.appendChild). In unserem Fall rufen wir hier die Methode render auf, was dazu führt, dass der aktuelle Wert zur Anzeige gebracht wird.

Der attributeChangedCallback wird aufgerufen, sobald ein HTML-Attribute geändert wird. Diese Änderung kann via JavaScript erfolgt sein oder eben über die DevTools. In diesem Callback prüfen wir zuerst, ob sich der Wert geändert hat, in dem wir den alten mit dem neuen Wert vergleichen. Danach prüfen wir, ob der Name des geänderten Attributes value ist und setzen dann unseren JavaScript-State. Achtung! Hier kann es beim Synchronisieren von JavaScript- und HTML-State schnell zu einer Endlosschleife kommen.

Damit der Browser auch weiß, welche Attribute er überwachen soll, für die attributeChangedCallback aufgerufen wird, benötigt es einen statischen Getter observedAttributes. Die Rückgabe ist ein Array mit den überwachten Attribut-Namen.

Probieren wir unsere Komponente im Browser aus, sehen wir, dass sie sich wie gewünscht verhält.

Styling

Als letztes wollen wir noch einen kleinen Blick in Richtung Styling wagen. Mit den CSS-Variablen haben wir hierfür eine kleine Grundlage geschaffen, um zumindest mal die Größe der Komponente zu ändern.

Wir können hier ganz einfach in unserer index.html einen Stil definieren, der die CSS-Variable überschreibt:


Im Browser erhalten wir so drei unterschiedlich große Zählerkomponenten, die jede für sich eigenständig funktioniert (Abbildung 5).

Abbildung 5: Gestyle Komponenten - Blogartikel Web Components

Abbildung: Gestyle Komponenten

Fazit

Mit Web Components erhalten wir eine wunderbare Möglichkeit, Komponenten auch ohne Framework zu erstellen und die nativen Mittel des Browsers zu benutzen. Mit diesem Artikel haben wir uns langsam dem Thema Web Components genähert und erste Schritte der Implementierung gemacht. Es gibt noch viele spannende Dinge zu entdecken, wie bspw. Custom Events, CSS Shadow Parts oder Constructable Stylesheets.

Viel Spaß beim Ausprobieren!

Manuel Rauber
Letzte Artikel von Manuel Rauber (Alle anzeigen)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Die von Ihnen hier erhobenen Daten werden von der Host Europe GmbH zur Veröffentlichung Ihres Beitrags in diesem Blog verarbeitet. Weitere Informationen entnehmen Sie bitte folgendem Link: www.hosteurope.de/AGB/Datenschutzerklaerung/