This blog post is also available in English
Ich zeige gleich zwei Bibliotheken, die einen anderen Ansatz verfolgen und es einfach machen, im Sinne von Progressive Enhancement Web Components dazu zu nutzen, bestehendes Markup mit mehr Funktionalität zu versehen. Dieser Teil behandelt Hotwire Stimulus und in Teil 2 werden wir einen Blick auf GitHubs Catalyst werfen. Dieser Teil behandelt Hotwire Stimulus und in Teil 2 werden wir einen Blick auf GitHubs Catalyst werfen.
This post is also available in English
Äpfel & Birnen
Web Components sind eine einfache Möglichkeit, im Browser Funktionalität in isolierten Kontexten hinzuzufügen. Custom Elements erlauben es uns unser eigenes Markup-Element zu erzeugen: <my-element>
. Dieses Element bildet den isolierten Kontext, denn wir können einfach mit seinem Inhalt interagieren und auf Ereignisse, die darin passieren, reagieren. Solange der Browser das Element nicht kennt, ignoriert er es einfach – somit ist es für uns ohne Konsequenzen, unser Markup einfach schon mal hinzuschreiben. Im Folgenden werde ich nur noch von Custom Elements sprechen, weil wir weder Templating noch Shadow DOM benutzen werden.
Schaut man sich nun aber gängige Beispiele für Custom Elements an, werden diese meist so verwendet: <my-element></my-element>
. Der eigentliche Inhalt des Elements wird erst erzeugt und gerendert, wenn das neue Element im Browser registriert worden ist – also nur, wenn der notwendige JavaScript Code korrekt läuft. Bekannte Frameworks wie Stencil und LitElement propagieren dies auch als den normalen Weg.
Nichts hält uns aber davon ab, Custom Elements zu verwenden, um Progressive Enhancement zu betreiben. Wenn wir unser Custom Element als Wrapper für bestehendes, sinnvolles Markup nutzen, lässt sich so Mehrwert hinzufügen:
Mit diesem Setup könnten wir – wenn der Browser den Code für unser Custom Element ausführt – zum Beispiel eine Möglichkeit hinzufügen, den Text in die Zwischenablage zu kopieren, so dass er einfach an einer anderen Stelle weiterverwendet werden kann.
Unser Beispiel
Wir nehmen dieses Beispiel, um exemplarisch zu zeigen, wie Stimulus (in diesem Post) und Catalyst (in Teil 2) bei der Entwicklung von Custom Elements helfen und dabei gleichzeitig Progressive Enhancement fördern – denn sie machen es einfach, mit bestehendem Markup zu interagieren, anstatt erst alles zur Laufzeit zu erzeugen.
Wir starten für unsere Experimente mit einer einfachen Struktur, in der wir vorsehen, den Inhalt eines Textfelds entweder automatisch in die Zwischenablage zu kopieren, oder den Inhalt der Zwischenablage in das Textfeld einzufügen. Das nötige JavaScript API ist vergleichsweise neu und funktioniert noch nicht in allen Browsern gleich gut. Wir nutzen dies als Chance, auch diese Unterschiede noch abzufangen und nur anzubieten, was auch funktioniert:
Die beiden Buttons sind dabei initial ausgeblendet, weil wir sie nur anbieten wollen, wenn sie auch etwas sinnvolles tun können:
Das komplette Beispiel steht als öffentliches GitHub-Repository zur Verfügung und kann damit einfach lokal nachvollzogen werden.
Progressive Enhancement
Progressive Enhancement ist eine Vorgehensweise in der Web-Entwicklung, in der der Fokus darauf liegt dem Nutzer den eigentlichen Inhalt möglichst einfach und vollständig zur Verfügung zu stellen. Dazu gehört auch die Möglichkeit auf grundlegender Art mit der Webseite zu interagieren. Konkret versucht man alle Information in Standard-Tags und Forms zu präsentieren, da somit sichergestellt ist, dass ein möglichst breiter Kreis an Nutzenden Zugriff auf die Inhalte hat.
Alle weiteren Präsentations– und Interaktionsmöglichkeiten werden dann mittels zusätzlicher Technologien (typischerweise CSS und Javascript) zum bestehenden Inhalt hinzugefügt, womit dieser verbessert, also “enhanced” wird. Das Hinzufügen von Funktionalität folgt dabei normalerweise einer Feature-Detektion, so dass die Menge der angebotenen Verbesserungen mit den unterstützten Features zunimmt (“progressive”). Hier ein paar ausgewählte Quellen zum Thema:
Custom Elements
Custom Elements sind ein Teil des “Web Components” Standards. Custom Elements definiert, wie Entwickelnde selbst Markup-Elemente definieren können, die analog den in HTML definierten Elementen in einer Webseite benutzt werden können. Die Art der Präsentation, die Inhalte und die möglichen Interaktionen können dabei frei definiert werden und haben alle APIs der Webplattform zur Verfügung.
Da das Registrieren eines Custom Elements nur über das Javascript API des Browsers möglich ist, müssen Custom Elements aus der Sichtweise von Progressive Enhancement schon als ein Enhancement zählen, weil das Vorhandensein oder Funktionieren von Javascript nicht vorausgesetzt werden kann. Somit sollten Custom Elements wichtige Inhalte nicht per Javascript erzeugen. Auch hier ein paar ausgewählte Quellen:
Stimulus
Stimulus habe ich ja schon im Artikel zu Hotwire in Teilen vorgestellt. Wie dort erwähnt, setzt Stimulus vollständig auf bestehendes Standard-Markup und hat technisch nichts mit Custom Elements zu tun. Wir werden also in diesem Teil der Vorstellung am Ende keine Web Component erstellen. Stimulus stellt aber die relevanten Bausteine zur Verfügung, um die gleiche Funktionalität zu erreichen.
Der Controller
Dreh- und Angelpunkt in Stimulus ist ein Controller
– eine normale ES6 JavaScript Klasse, die von der durch Stimulus definierten Klasse Controller
ableitet:
Sobald wir den Controller erstellt (und bei Stimulus registriert haben), können wir Ihn an ein Element im Markup binden, indem wir dem entsprechenden Element ein data-controller
Attribut geben, in dem der Name des Controllers steht. Damit erzeugt Stimulus eine Instanz der Klasse und verbindet die beiden.
Da wir auch die Lifecycle-Methode connect()
implementiert haben, die Stimulus aufruft, wenn es die Controller-Instanz an das DOM-Element gebunden hat, sollten wir nun die Meldung EnhancedInputController connected
in der Browser-Konsole sehen.
Binding von vorhandenem Markup
Wie erwähnt ist Stimulus darauf ausgelegt, bestehendes Markup zu referenzieren. Das Konzept dazu nennt Stimulus “Targets”. Targets werden als eine statische Liste von Element-Namen im Controller definiert
Die gleichen Namen lassen sich nun nutzen, um im HTML Elemente mit data
Attributen auszustatten, die nach dem Schema data-{controller-name}-target
benannt sind und deren Wert einer der Strings aus dem targets
Attribut im Controller sind:
Anders als gezeigt kann man den gleichen Namen auch mehrfach verwenden, wenn man anstelle eines einzelnen Elements eine Reihe von Ihnen adressieren will.
Stimulus nimmt die Werte aus dem static targets
und erzeugt jeweils Properties dazu, so dass man via this.{name}Target
(im Beispiel also this.copyTarget
) auf das Element zugreifen kann, respektive mit this.has{Name}Target
prüfen kann, ob ein entsprechendes Target ausgezeichnet ist. Ein Element im DOM ist dabei auch nicht darauf beschränkt, zu einem einzelnen Controller zu gehören, sondern kann mit diversen data-{controller-name}-target
Attributen ausgezeichnet sein.
Mit diesem Werkzeug können wir nun den Controller so erweitern, dass er die Buttons für die wir Support bieten können, aktiviert:
Wir prüfen neben navigator.clipboard
auch noch, ob die beiden Methoden writeText
und readText
definiert sind, weil dies wie erwähnt noch nicht einheitlich in allen Browsern ist. Bei mir gibt Firefox 85.0.2 z.B. nur Support für writeText
her, aber nicht für readText
, während Chromium beides unterstützt.
Reagieren auf Events
In diesem Zustand bieten wir natürlich noch keinen Mehrwert, weil die beiden Buttons nun zwar dynamisch eingeblendet werden, sie aber noch nichts weiter tun.
Als erstes definieren wir also zwei Methoden, die Copy und Paste mit dem Clipboard API implementieren. Da das API asynchron ist, definieren wir die Handler als async
und können dann mit await
linear das Handling hinschreiben:
Um diese Methoden nun an Events im DOM zu binden nutzen wir Stimulus “Actions”. Hierbei wird das Element, an dessen Event man interessiert ist, wieder mit einem data
Attribut erweitert. Diesmal mit data-action
.
Als Attributwert verwendet man einen String in der Form {event}->{controller-name}#{method-name}
, mit dem man definiert,
- Welches Event des Elements (
{event}
) wir - an welche Methode (
#{method-name}
) - welches Controllers (
{controller-name}
) binden will (->
)
Will man auf mehrere Events reagieren, so lässt sich in data-action
eine mit Leerzeichen getrennte Liste definieren: data-action="{event-1}->{controller-name-1}#{method-name-1} {event-2}->{controller-name-2}#{method-name-2}"
Neben dieser grundlegenden Syntax unterstützt Stimulus noch weitere Features wie Shorthand, bei dem man für ‘offensichtliche’ Bindings den Namen des Events weglassen kann. Oder weitere Features wie once
, die bei der Registrierung von Event Listenern im DOM API normalerweise zur Verfügung stehen.
Konfiguration über Attribute
Wenn wir unsere “Enhanced Input” Komponente nun flexibel einsetzen wollen, sollten wir kontrollieren können, ob Copy oder Paste verfügbar ist, anstatt uns hier nur auf den Browser zu verlassen. Dafür wollen wir natürlich auch nicht immer den Code anpassen, sondern definieren es idealerweise deklarativ direkt im HTML.
Stimulus bietet hierfür das Konzept der “Values”. Diese starten Ihr Dasein wieder als eine Definition im Controller. Unter dem Namen values
wird eine statische Objekt-Definition erzeugt, deren Keys die Namen der Attribute sind und deren Value Ihren Datentyp definiert.
Dem allgemeinen Pattern folgend erzeugt Stimulus für die so deklarierten Values wieder Properties nach dem Schema this.{name}Value
und this.has{Name}Value
– für uns also this.copyValue
und this.pasteValue
, die man einfach auslesen oder zuweisen kann.
Unser Beispiel nutzt nur Boolean
als Typ, weil wir nur Schalter brauchen. Stimulus Values unterstützen aber Array
, Boolean
, Number
, Object
und String
als Datentypen. Stimulus übernimmt dabei die Konvertierung, so dass man auf die generierten Properties zugreifen kann, als ob sie den deklarierten Datentyp haben.
Als zusätzliche Funktion kann man auch noch Methoden nach dem Namensschema {name}ValueChanged
definieren, die immer dann von Stimulus aufgerufen werden, wenn der Wert des Values ändert.
Auf der HTML-Seite werden die Values wieder über data
Attribute auf dem Controller definiert, diesmal folgt das Attribut dem Naming-Schema data-{controller-name}-{valueName}-value
.
Leider kann man über das static values
keine Defaults definieren. Will man also kein Attribut definieren, dann muss der Default, den Stimulus pro Datentyp vorgibt, passen. Was er im Beispiel hier nicht tut, da der Default für Boolean
false
ist, wir aber eigentlich gern per Default beide Buttons aktiviert haben wollen.
weitere Goodies
Als weiteren Schritt, die eigene Komponente flexibel zu gestalten, bietet Stimulus das Konzept von “CSS Classes”. Die Idee dahinter ist durch CSS gesteuertes Verhalten der Komponente von den eigentlichen CSS-Klassennamen zu entkoppeln.
Zum Beispiel könnte ich in meiner Komponente visualisieren wollen, ob der Inhalt okay ist, oder noch Validierungsfehler hat. Am einfachsten gebe ich meiner Komponente dafür eine CSS-Klasse, die die entsprechende Darstellung auslöst. In unserem Beispiel nehmen wir das initiale Verstecken der beiden Buttons als die Dynamik, die wir via CSS gestalten.
Anstatt nun den Namen der CSS-Klasse, die diese Veränderung hervorruft, im JavaScript hart zu kodieren, kann man in seinem Stimulus Controller eine weitere statische Definition hinzufügen:
Damit erzeugt Stimulus wiederum Properties für uns, deren Wert wir über this.{name}Class
auslesen und als logischen Wert im Code benutzen können.
Auf der HTML-Seite können wir nun wieder deklarativ ein data
Attribut an unser Controller-Element hinzufügen, in dem wir angeben, wie der konkrete Name der CSS Klasse für den jeweiligen Verwendungszweck in unserem konkreten Fall ist. Das Attribut folgt dabei dem Namensschema data-{controller-name}-{css-class-name}-class
und definiert in seinem Wert die tatsächlich zu nutzenden CSS-Klasse.
Wenn man alles zusammensteckt
Mit all diesen Dingen zusammen haben wir nun unser Beispiel in einer funktionierenden Form.
Wie man sieht, ist auch in Browsern, in denen sowohl Copy wie auch Paste möglich sind, noch die eine oder andere Hürde zu nehmen. Aus Sicherheitsgründen (in der Zwischenablage könnte ja auch gerade das Passwort aus dem Passwortmanager sein) muss noch die Erlaubnis erteilt werden, auch aus dem Clipboard zu lesen. Generell ließe sich diese Komponente aber nun flexibel verwenden, ohne dass der Code angepasst werden muss. Und sollte JavaScript nicht funktionieren, wäre zumindest immer noch ein <input>
Feld vorhanden.
Fazit und Ausblick
Ich habe es schon im ersten Artikel gesagt: die Tools aus dem Hotwire Bundle fühlen sich generell rund und solide an. Die gebotenen Funktionen sind umfassend und bieten Parität mit dem, was das DOM-API auch zur Verfügung stellt, wenn man es braucht. Im Normalfall kann man aber gut hinter der Stimulus Abstraktion leben und die tiefere Integration der Library überlassen. Und dann kommt man tatsächlich mit “normalem” HTML und data
Attributen aus – weiteres Wissen ist nicht notwendig.
Es braucht vermutlich eine Weile, bis man die Syntax der unterschiedlichen statischen Attribute für die magischen Bindings innerhalb des Controllers verinnerlicht hat. Die unterschiedlichen Formen (String-Arrays und Objektdefinition) helfen dabei nur mäßig – immerhin ist das Naming innerhalb der erzeugten Properties aber schlüssig. Wenn man diese Namen verinnerlicht hat, ergibt sich vermutlich auch das Naming der data
Attribute im HTML von alleine – so schön ich dieses Feature finde schaue ich doch bisher noch jedesmal nach. Und – zumindest für meinen Geschmack – sind die Namen der Attribute, die sich ergeben, wenn man seine Controller nicht nur copy
oder clipboard
nennt sehr verbose. Natürlich muss man sie nur einmal hinschreiben, aber ich finde es gleich unübersichtlich.
Am Ende reibe ich mich aber am Meisten daran, dass Stimulus einen weiteren Layer baut. Natürlich ist es super, wenn ich nichts über Custom Elements wissen muss, aber warum sollte ich eigentlich nicht? Schließlich sind diese das eigentliche Web-Fundament und damit vermutlich die stabilere Basis. Und dann tatsächlich sprechende Element-Namen im HTML zu haben anstelle von <div data-controller="foo">
klingt für mich auch eher wie ein weiterer Vorteil.
Das soll kein schlechtes Licht auf Stimulus werfen, im Gegenteil, die Library ist wie gesagt solide, die Dokumentation ist prima, und bei Fragen kann man entweder in den Code schauen oder direkt in der User-Community fragen, die Basecamp auch noch mit anbietet (und in der die Entwickler aktiv sind). Aber Ihr könnt auch genauso gut die Zeit darein investieren, zu lernen, wie richtige Web Components funktionieren.
Im nächsten Teil werden wir uns dann mit Catalyst eine weitere Library anschauen die den oben skizzierten Ansatz unterstützt. In weiten Teilen sind die Features vergleichbar, aber doch unterschiedlich genug um interessant zu bleiben. Bis dahin!
Der Autor dankt seinen Kollegen Robert Glaser und Daniel Westheide für die Kommentare zu einer früheren Version des Artikels. Das Titelbild ist von Claudio Schwarz auf Unsplash.