Web Components ist der Überbegriff einiger entstehender Standards mit dem gemeinsamen Ziel, Webentwickler mit besseren Werkzeugen für komplexe Anwendungsentwicklungen auszustatten. Insbesondere forciert man dabei gesteigerte Wiederverwendbarkeit und Interoperabilität. Worte, die sich sehr großer Beliebtheit erfreuen und nicht selten inflationär gebraucht werden, wenn Entwickler über Quelltext-Qualität sprechen. Entscheiden Sie selbst, ob es dieses Mal angebracht ist.
Web Components ist der Überbegriff einiger entstehender Standards mit dem gemeinsamen Ziel, Webentwickler mit besseren Werkzeugen für komplexe Anwendungsentwicklungen auszustatten. Insbesondere forciert man dabei gesteigerte Wiederverwendbarkeit und Interoperabilität. Worte, die sich sehr großer Beliebtheit erfreuen und nicht selten inflationär gebraucht werden, wenn Entwickler über Quelltext-Qualität sprechen. Entscheiden Sie selbst, ob es dieses Mal angebracht ist, ein Beispiel: Kennen Sie den Code auswendig, um ein YouTube-Video oder eine Google-Map in ihre Seite einzubinden? Ich auch nicht, aber stellen Sie sich vor, es wäre so einfach wie:
<youtube-video src=”https://www.youtube.com/watch?v=RFinNxS5KN4”>
und-
<google-map latitude=”51.413004” longitude=”5.543293”>
Das eröffnet eine ganze neue Welt und diese Welt ist keine Fiktion, Web Components machen sie greifbar. Um diesen Griff tätigen zu können, müssen wir uns im Detail vor Augen führen, was die einzelnen Bestandteile (oder sollte ich sagen Komponenten?) von HTML-Elementen sind, denn am Ende unserer Reise wollen wir in der Lage sein, eigene HTML-Elemente zu definieren.
Die Anatomie von HTML-Elementen
Hinter HTML-Elementen steckt wesentlich mehr als nur ihre Tags, deshalb wird es von Entwicklern auch regelmäßig moniert, wenn jemand die Begrifflichkeiten durcheinander würfelt. Kurzgesprochen sind HTML-Tags nur die Serialisierung von HTML-Elementen, also nur eine Art von Notation. Das HTML-Element selbst, das mit diesen Tags assoziert wird, hat diverse Komponenten, dazu zählt eine grafische Benutzeroberfläche, eine öffentliche Programmierschnittstelle und ein im Hintergrund agierendes, aber nicht zu unterschätzendes Bindeglied. Aber der Reihe nach, sehen wir uns zunächst mal die grafische Oberfläche des <video>-Elements an.
Es gibt Bedienelemente um die Wiedergabe zu starten, eine Fortschrittsanzeige, einen Zeitzähler und einen Lautstärkeregler – keine großen Überraschungen. Für den Moment ist es nur wichtig zur Kenntnis zu nehmen, dass solche grafischen Benutzeroberflächen existieren und dass sie vom Browser bereitgestellt werden.
Widmen wir uns als nächstes der Programmierschnittstelle. Auch hier will ich keine großen Worte verlieren und es möglichst kurz machen. Jedes HTML-Element hat so eine definierte Schnittstelle, nachlesen kann man darüber in der HTML-Spezifikation. Wer es lieber pragmatisch mag, der kann diese Schnittstellen auch direkt mit seinen Browser-Werkzeugen erkunden: Mit dem JavaScript-Snippet console.dir( document.createElement(‘video’).__proto__ );
beauftragen wir unseren Browser, eine ausführliche Erklärung der <video>-Element-Schnittstelle in die JavaScript-Konsole zu schreiben. Das Ergebnis (nachdem ich der Übersicht halber einige browserspezfische Eigenheiten entfernt habe) sieht etwa so aus:
Die Programmierschnittstelle des <video>-Elements bietet uns zum Beispiel Methoden zum Steuern der Wiedergabe (play()
und pause()
) an, sowie ein paar element-spezfische Konstanten und einige weitere Funktionen. Die Bedeutung der einzelnen Eigenschaften und Methoden interessiert uns an dieser Stelle gar nicht so sehr, es geht wieder nur um die rein quantitative Feststellung, dass so eine öffentliche Programmierschnittstelle existiert.
Endlich können wir uns dem interessanten Teil widmen. Werfen wir nochmal einen Blick auf die grafische Benutzeroberfläche des <video>-Elements und vergleichen das, was wir sehen, mit dem Quelltext, den ich für den Screenshot geschrieben habe:
<video controls>
Das sollte uns stutzig machen, zumindest wenn wir mal unsere Abgestumpfheit kurz bei Seite legen und uns die Frage stellen, woher die einzelnen Bedienelemente stammen – schon klar, das controls-Attribut, aber schauen wir noch genauer hin: Wir haben einen Abspiel-Button, einen Fortschrittsbalken, eine Fortschrittsanzeige und einen Tonregler, das alles soll in einem Attribut stecken? Wenn ich Sie bitten würde, so eine grafische Oberfläche nur mit den Mitteln von HTML nachzubasteln, dann würden sie mir vielleicht so etwas servieren:
<div class=”video-canvas”></div>
<div class
s=”video-controls”>
<button class=”play”>▶</button>
<input type=”range” class=”track” />
<div class=”clock”>0:00</div>
<button class=”mute”>🔇</button>
</div>
Das ergäbe einigermaßen Sinn, für jede grafische Komponente gibt es ein dazu passendes HTML-Element im Quelltext. Und damit liegen wir gar nicht so fern von dem, was der Browser unter der Haube unternommen hat, um uns die hübsche Oberfläche zu zaubern, die wir da oben sehen, woher ich das weiß? Ich habe meinen Browser gefragt – seine Antwort:
Was Sie hier sehen, ist das sogenannte Shadow-DOM. Das DOM (Document Object Model) ist Ihnen hoffentlich ein Begriff. Neu könnte der Prefix Shadow für Sie sein. Das Shadow-DOM ist eine Art geheim operierender Teilbaum innerhalb eines ganz gewöhnlichen DOM-Baums. Geheim deshalb, weil er von Programmcode außerhalb seines eigenen Mikrokosmos' nicht erreichbar ist. Die Versuchung liegt nahe, uns Zugriff auf den Abspiel-Button zu holen, zum Beispiel mit document.querySelector('video > div + div > input')
. Aber dieser Versuch wird scheitern. Dieser Teilbaum gehört dem Browser, nicht dem Webentwickler.
Überzeugen Sie sich selbst, sagen Sie ihrem Chromium-Browser, dass sie einen Blick auf diese verborgene Welt werfen möchten. F12 -> Einstellungen -> Show user agent shadow DOM. Anschließend können sie mit Rechtsklick -> Element untersuchen jedes Element unter die Lupe nehmen, das Ihnen in den Sinn kommt, nehmen sie zum Beispiel das <select>-Element oder das <keygen>-Element. Nur zu, ich warte hier.
Schön, dass Sie wieder da sind. Vielleicht fragen Sie sich inzwischen, was wir mit unserem neugewonnenen Wissen über geheime Teilbäume im DOM anfangen können und im Falle vom browsereigenen Shadow-DOM ist die ernüchternde Antwort: Nicht sehr viel. Aber hatte ich nicht am Anfang geschrieben, dass Web Components Entwicklern neue Werkzeuge in die Hände drücken möchten? Dies, meine Damen und Herren, ist das erste Werkzeug aus dem neuen, scheinenden Werkzeugkasten, den ich Ihnen präsentieren möchte.
Das Shadow DOM
Jetzt, da wir einige Schattenbäume bereits gesehen haben, beschäftigen wir uns mit der Frage, wie wir selber solche Schattenbäume pflanzen können, um im Vokublar der Methapher zu bleiben. Vorher aber noch zwei Begriffe, die eine große Rolle spielen, den Wurzelknoten eines Shadow-Trees nennt man Shadow-Root. Das Element, das den Shadow-Root besitzt, nennen wir Shadow-Host; in unserem Beispiel war das das <video>-Element.
Für den Anfang werden wir ein simples Greeter-Beispiel stricken. Ausgehend von folgendem HTML
<div id="greeter"></div>
möchten wir uns ein kleines Widget basteln, welches aus einem Eingabefeld für einen Namen und einem Sag-Hallo-Button besteht. Bei einem Klick auf den Button soll eine alert()-Box erscheinen, die den Nutzer begrüßt.
Warnung: Die Web Components Standards befinden sich noch in einer recht jungen Erprobungsphase und können sich jederzeit und ohne Ankündigung ändern. Benutzen Sie sie deshalb nicht im Produktiv-Einsatz.
Hinweis: Zur Zeit sind Chromium-basierte Webbrowser (Chromium selbst, Google Chrome und Opera) die einzigen Browser mit nativer Unterstützung für die Quelltext-Beispiele in diesem Blog-Artikel.
Wir gehen zunächst ganz gewöhnlich vor, und lassen das Shadow-DOM außen vor. Zu Beginn erzeugen wir uns Variablen für alle HTML-Elemente, die in unserem Beispiel eine Rolle spielen:
var greeter = document.querySelector
r('#greeter'); // Der spätere Shadow-Host
var input = document.createElement('input'); // Das Name-Eingabefeld
var button = document.createElement('button'); // Der Sag-Hallo-Button
Unser Button soll eine aussagekräftige Beschriftung erhalten:
button.innerHTML = 'Sag Hallo!';
Als nächsten schreiben wir eine Begrüßungs-Funktion, die bei einem Klick auf den Button ausgelöst werden soll:
button.onclick = function () {
var name = input.value;
alert('Hello, ' + name + '!');
};
An Programmlogik ist das schon fast alles, jetzt geht es nur noch darum, die Einzelteile zusammenzusetzen. Dafür erzeugen wir zunächst einen Shadow-Root unterhalb unseres <div>-Elements.
var shadowRoot = greeter.createShadowRoot();
Jetzt heften wir noch unseren Button und unser Eingabefeld unter diesen Shadow-Root:
shadowRoot.appendChild( input );
shadowRoot.appendChild( button );
Fertig ist unser Greeter-Beispiel: http://jsfiddle.net/zoub4e1x/. Vielleicht stellen Sie sich jetzt die Frage, was das soll, wieso man das Eingabefeld und den Button nicht direkt unter das <div>-Element tackert, sondern erst noch einen Schattenbaum dazwischen schiebt. Der Sinn dahinter ist einfach – Daten-Kapselung: Sowie man in PHP oder Java private und öffentliche Methoden innerhalb einer Klasse definieren kann, können wir mit einem Schattenbaum einen privaten Teilbaum im sonst öffentlichen DOM-Baum erzeugen. Es ist nicht möglich von außerhalb versehentlich unseren Schattenbaum zu manipulieren. Ein anderes Skript kann nicht versehentlich unseren Button löschen, indem es zum Beispiel folgenden Programmcode ausführt:
var buttons = document.querySelectorAll('button');
for ( var i = 0; i < buttons.length; i++ ) {
buttons[ i ].remove();
}
Unser Greeter-Beispiel ist noch nicht am Ende, wie ich Ihnen am Anfang versprochen habe, möchten wir am Ende ein ganz eigenes Element haben, und nicht nur einen eigenen Schattenbaum. Wir wollen ein passendes Tag haben, <hello-greeter> klingt doch angemessen. Und wir wollen eine öffentliche Programmierschnittstelle für andere Entwickler zur Verfügung stellen. Aber dafür meine Damen und Herren ist das Shadow-DOM nicht das geeignete Werkzeug, dafür präsentiere ich Ihnen das zweite Werkzeug aus dem Web Components--Werkzeugkasten: das Custom Element.
Custom Element
Wir wollen unser Greeter-Widget so erweitern, dass andere Entwickler einfacher davon Gebrauch machen können, indem sie in ihrem HTML-Quelltext folgende Tags benutzen:
<hello-greeter></hello-greeter>
Hinweis: Der Bindestrich ist obligatorisch für Custom-Elemente. Jedes benutzerdefinierte Element muss einen Bindestrich im Tag-Namen tragen, um Kollisionen mit den Standard-Namen von HTML-Elementen zu vermeiden.
Standard-Elemente beherrscht der Browser von Haus aus, der Umgang mit benutzerdefinierten Custom-Elementen muss ihm erst beigebracht werden. Zu diesem Zweck verwaltet er eine Element-Registry, dort werden die Element-Namen zusammen mit dem verknüpften Verhalten (dem Prototypen) hinterlegt. Bevor wir uns ansehen, wie wir das erreichen, sollten wir uns erst mit diesem Element-Prototypen vertraut machen. Die Eigenschaften und Methoden, die wir im Prototypen definieren, werden später von jeder Element-Instanz geteilt werden. Wenn wir im Prototyp unseres <hello-greeter>-Elements also eine Methode greet() definieren, können wir diese später von überall in unserem Programm aufrufen:
document.querySelector('hello-greeter').greet('Laura');
Der passende Prototyp dazu könnte zum Beispiel wie folgt aussehen:
var helloGreeterPrototype = {
greet : function ( name ) {
alert('Hello, ' + name + '!');
}
};
Das ist ein guter Anfang, aber wir wollen auch dass jedes <hello-greeter>-Element automatisch mit der grafischen Oberfläche ausgestattet wird, die wir bereits vorbereitet haben. Wir müssen also, immer wenn ein neues <hello-greeter>-Element erstellt wird, den entsprechenden Shadow-Baum darunter klemmen. Für diesen Zweck können wir eine Methode namens createdCallback
in unserem Prototypen definieren. Diese Methode wird automatisch vom Browser aufgerufen, immer wenn er ein neues <hello-greeter>-Element erzeugt.
var helloGreeterPrototype = {
greet : function ( name ) {
alert('Hello, ' + name + '!');
},
createdCallback : function () {
var greeter = this;
var input = document.createElement('input');
var button = document.createElement('button');
button.innerHTML = 'Sag Hallo!';
button.onclick = function () {
var name = input.value;
greeter.greet( name );
}
var shadowRoot = greeter.createShadowRoot();
shadowRoot.appendChild( input );
shadowRoot.appendChild( button );
}
};
Der Quelltext für die createdCallback()-Methode unterscheidet sich kaum von unserem vorherigen Beispiel, deshalb werde ich nur kurz auf die Unterschiede eingehen: Der Schatten-Baum wird nun nicht mehr unter ein zuvor ausgewähtes <div>-Element gehängt, sondern soll dynamisch unter dem aktuell erzeugten <hello-greeter>-Element eingefügt werden; die Variable greeter
verweist deshalb jetzt auf this
. this
enthält in diesem Kontext eine Referenz auf das jeweilige <hello-greeter>-Element. Die onclick-Methode des Buttons ist etwas einfacher geworden, weil ein Teil davon bereits in der greet()-Methode implementiert ist.
Eins noch: Unser <hello-greeter>-Element soll alle Funktionen von einem ganz gewöhnlichen HTML-Element erben. Wir benutzen dafür JavaScripts prototypisches Vererbungsmodell:
Object.setPrototypeOf( helloGreeterPrototype, HTMLElement.prototype );
Endlich können wir unser Beispiel zum Leben erwecken, dafür müssen wir es nur noch in die Element-Registry eintragen:
document.registerElement('hello-greeter',{ prototype: helloGreeterPrototype });
Herzlichen Glückwunsch, Sie haben soeben ihr erstes eigenes HTML-Element erstellt (vollständiges Beispiel: http://jsfiddle.net/qh567w68/). Noch ein nützlicher Tipp, bevor wir zu einem Ende kommen:
createdCallback ist nicht die einzige Methode, die eine Sonderrolle übernimmt. Es gibt noch weitere. Diese Funktionen fasst man allgemein als Lifecycle-Callbacks zusammen, die da wären:
- createdCallback
-
Wird immer aufgerufen, wenn ein neues Element dieser Gattung erzeugt wird. Zum Beispiel mit
document.createElement(‘hello-greeter’)
. - enteredViewCallback
- Diese Funktion wird dann ausgeführt, wenn das Element in ein Dokument eingefügt wird, zum Beispiel durch
appendChild()
. - leftViewCallback
-
Entsprechend zu viewEnteredCallback wird leftViewCallback aufgerufen, wenn das Element aus seinem Dokument entfernt wird, zum Beispiel mit
removeChild()
. - attributeChangedCallback
- Diese Callback-Funktion wird aufgerufen, wann immer ein Attribut zum Element hinzugefügt, geändert oder entfernt wird, beispielsweise mit
setAttribute()
.
Unsere gemeinsame Reise endet hier vorerst, aber es gibt noch vieles zu entdecken (HTML-Imports und HTML-Templates), vieles von dem ich Ihnen noch nicht erzählt habe. Wenn ich ihr Interesse geweckt habe und ihr Wissensdurst noch nicht gestillt sein sollte, dann lesen Sie hier weiter (die deutschsprachige Lektüre ist leider noch kaum vorhanden, deshalb verweisen die Links allesamt auf englischsprachige Seiten):
Shadow DOM 101 by Dominic Cooney
Dominic Cooney erklärt, was es mit dem Shadow-DOM auf sich hat.
Custom Elements by Eric Bidelman
Eine sehr ausführliche Erklärung zu benutzerdefinierten Elementen von Eric Bidelman.
http://webcomponents.org/
Auf WebComponets.org wird Material rund um Web Components gesammelt und veröffentlicht.
http://customelements.io/
Eine Datenbank von vorgefertigen Custom Elementen.
Shadow DOM
Offizielles W3C Working Draft
Custom Elements
Offizielles W3C Working Draft
Update: Manchmal muss man Wünsche nur laut aussprechen und sie erfüllen sich von selbst. Heute morgen noch habe ich mich über zu knappe deutsche Lektüre beschwert, dabei hat etwa um die selbe Zeit Peter Kröner von den WEBKRAUTS sich ebenfalls dem Thema Web Components angenommen.