ECMAScript 6: Promises

Veröffentlicht am 24. Juni 2014

Eine vergleichsweise neue Entwicklung im Reich von ECMAScript 6 ist die Einführung von nativen Promises. Nachdem es schon lange Promises in JavaScript-Libraries gab, werden die praktschen Async-Helferlein in ES6 zum Sprachfeature erhoben. Die gute Nachricht dabei: die neuen Standard-Promises verhalten sich genau wie die alten JS-Promises (und auch wirklich nur wie die guten) und sie sind auch schon in einigen aktuellen Browsern implementiert.

Warum Promises? Warum native Promises?

Promises sind Objekte, die asynchrone Operationen kapseln. Sie zwängen asynchrone Operationen in eine einheitliche API, können verarbeitet werden bevor die Operation abgeschlossen ist und es gibt zahlreiche Möglichkeiten Promises zu kombinieren oder in Sequenzen zu verwenden. Sie haben sich (in Browser-JavaScript) als die Alternative zu Callback-Gewurschtel herausgebildet und werden unter anderem von jQuery unterstützt:

// Herkömmlicher Callback
$.get('/answer', function(data){
  console.log(data);
});


// Promise-Objekt
var ajaxPromise = $.get('/answer');

// Promise-Objekt mit Callbacks und Verkettung
ajaxPromise
  .then(function promiseCallback(result){
    return JSON.parse(result);
  })
  .then(function erfolgCallback(parsed){
    window.alert(parsed.answer); // > 42
  }, function fehlerCallback(err){
    console.error(err);
  });

Die Details zu Promises kann man an anderer Stelle in epischer Breite nachlesen. Die eigentlich interessante Frage ist, warum man überhaupt mit ECMAScript 6 native Promises einführen sollte, wenn es doch jQuery (und viele andere Libraries für Promises) gibt. Mindestens vier gute Gründe lassen sich nennen:

  1. Kompatibilität: Die meisten Promise-Libraries produzieren Promises, die weitgehend kompatibel zueinander sind, aber es gibt Ausnahmen. Besonders jQuery tut sich hier negativ hervor und fabriziert Promises, die sehr andere Eigenschaften haben als in den meisten anderen Libraries. Außerdem hat jede Library eine ganz eigene API für die Erstellung von Promises. Vereinheitlichung an beiden Fronten wäre wünschenswert.
  2. Performance: Wenn der Browser nativ Promises spricht, braucht man keine Extra-Library hierfür mehr in die Seite zu laden.
  3. Browser-APIs: Native Promises können vom Browser direkt durch die divseren HTML5-APIs erzeugt und konsumiert werden. Funktionen wie geolocation.getCurrentPosition() könnten Geolocation-Promises erzeugen und die ulta-asynchronen Funktionen der Indexed DB könnten mit Promises statt des üblichen Callback-Krawalls viel übersichtlicher werden.
  4. Gute Webstandards-Praxis: Es ist im Reich der Webstandards üblich, etablierte Patterns in den Standard aufzunehmen und so Hacks durch native Features zu ersetzen. So gibt es heute <video> statt Flash-Filmchen, document.querySelectorAll() statt $() und eben native Promises anstelle von JavaScript-Promises.

Da Promises vom Prinzip her sehr einfache Konstruktionen sind, gibt es bereits nennenswerte Browserunterstützung für dieses ES6-Feature und für uns einen guten Grund, native Promises unter die Lupe zu nehmen.

Die Promise-API in ES6

Die API für die Erzeugung von ES6-Promises gleicht der API von rsvp.js:

var promise = new Promise(function(resolve, reject){
  resolve(42);
});

promise.then(function erfolgCallback(x){
  window.alert(x);
});

Die Promises verhalten sich wie jene aus den zu Promises/A+ kompatiblen JS-Libraries; sie sind garantiert asynchron, fangen in Then-Callbacks geworfene Fehler ein und haben generell keine der aus jQuery bekannten Merkwürdigkeiten:

function fail(){
  throw new Error('Epic fail!');
}

var promise = new Promise(function(resolve, reject){
  resolve(42); // Garantiert asynchron
});

promise
  .then(fail) // Error wird sauber eingefangen und nicht geworfen
  .then(function(){
    window.alert('Epic win!');
  }, function(err){
    window.alert(err.toString());
  });

window.alert('Test'); // Passiert garantiert VOR Promise-Auflösung

Neben dem altbekannten then(erfolgCb, fehlerCb) haben Promise-Objekte noch eine catch()-Methode, die das Definieren eines Fehler-Callbacks ohne Angabe eines Erfolgs-Callbacks erlaubt; catch(cb) ist syntaktischer Zucker für then(undefined, cb).

Der Promise-Constructor bietet außerdem noch die folgenden statischen Methoden:

  1. Promise.all(list) nimmt als Parameter eine Liste von Promises an. Die Funktion gibt ein Promise zurück, das mit einem Array der Werte der augelösten Promises aufgelöst wird. Wird eins der Elemente in der Liste abgelehnt, wird das Promise mit dem entsprechenden Wert ebenfalls abgelehnt.
  2. Promise.race(list) nimmt als Parameter eine Liste von Promises an. Die Funktion gibt ein Promise zurück, das mit den Wert des ersten aufgelösten bzw. abgelehnten Promise aufgelöst bzw. abgelehnt wird.
  3. Promise.resolve(x) gibt ein Promise zurück, das umgehend mit x aufgelöst wird.
  4. Promise.reject(x) gibt ein Promise zurück, das umgehend mit x abgelehnt wird.

Da ECMAScript 6 noch längst kein fertiger Standard ist, sind Änderungen an diesen APIs natürlich nicht auszuschließen. So lange sich die ES6-Promises wie A+-Promises verhalten wäre das aber kein großes Problem, denn dann helfen wieder die guten alten Promise-Libraries.

Eine Zukunft für Promise-Libraries

Q, RSVP und all die anderen Promise-Libraries sind mit der Einführung von nativen Promises keinesfalls reif für die Rente. Alles, was ES6 in Sachen Promises liefert, ist eine Implementierung des Features „Promise“ und ein recht kleines Set an Promise-Tools (Promise.all(), Promise.race()). Alle nennenswerten Promise-JavaScript-Libraries sind hingegen schwerpunktmäßig Tool-Sammlungen und haben nur nebenher auch eine Promise-Implementierung an Bord. Zu den Features von Q zählt z.B.:

  • Promise-Inspektions-Methoden wie promise.isFulfilled() und promise.isRejected()
  • promise.spread(), das wie then() funktioniert, aber den Inhalt eines als Array übergebenen Parameters als Einzel-Parameter an den Callback übergibt
  • Die Möglichkeit, NodeJS-Callbacks und jQuery-„Promises“ bequem in richtige Promises zu verwandeln

Promise-Poweruser werden auf derartige Tools auch in Zukunft nicht verzichten wollen, womit die Promise-Libraries weiterhin eine Existenzberechtigung haben. Sie werden vielleicht schlanker, aber sie werden uns erhalten bleiben.

Browserunterstützung und Fazit

Obwohl Promises als ES6-Feature noch gar nicht so alt sind, gibt es bereits Unterstützung in einigen Browsern. Opera und Chrome (auch mobile) haben das Feature schon länger im Angebot, Firefox (auch mobile) kann seit Version 29 damit aufwarten. Internet Explorer, Safari und der Android-Browser unterstützen Promises erwartungsgemäß nicht. Dieser löchrige Support ist jedoch kein Problem. Selbst wenn jeder Browser schon Promises unterstützen würde, würde man, wie schon erwähnt, aufgrund der vielen Komfort-Funktionen kaum auf die diversen Promise-Libraries verzichten wollen. Da diese Libraries ihre eigenen Promise-Implementierungen mitbringen, ist jetzt wie auch in Zukunft für flächendeckende Unterstützung gesorgt.

Das eigentliche Feature der ES6-Promises ist die Möglichkeit, dass sie jetzt auch native Browser-APIs verwendet bzw. angeboten werden können. Insofern ist die Einführung von nativen Promises vor allem eine Infrastruktur-Maßnahme und vielleicht gar nicht so sehr ein Features für die JavaScript-Entwickler selbst.

Fragen zu HTML5 und Co beantwortet 14 - File APIs, hgroup-Element, Text messen, CSS und Shadow DOM

Veröffentlicht am 17. Juni 2014

Nach gar nicht mal so langem Sammeln von Leserfragen musste ich diesmal sehr lange schreiben um die teilweise ausgesprochen kniffligen Fragen zu beantworten. Aber es ist geschafft! Und falls ihr meint, dass ihr für den nächsten Teil dieser Serie noch schwierigere Fragen auf Lager habt, schreibt sie mir per E-Mail oder gebt die Frage per Twitter ab.

HTML5 File APIs

Kann man mit der HTML5 File API Verzeichnisse auf dem Client auslesen?

Die File API definiert nur Funktionen, die mit Datei-Objekten im Browser arbeiten, aber dort ist kein Mechanismus festgehalten, wie diese Dateien in den Browser gelangen. Aktuellen Standards zufolge gibt es hierfür folgende Möglichkeiten:

  • Drag & Drop
  • Input-Elemente mit type="file" (DOM-Eigenschaft files)
  • Scripts, die selbst mit dem Blob-Constructor Datei-Objekte erzeugen

Das Auslesen eines Verzeichnisses ist nicht dabei. Die File System API sollte etwas derartiges ermöglichen, ist allerdings aktuell nur in Browsern mit Blink-Engine (Chrome und Opera) zu finden und wird vom W3C auch nicht mehr als Standard weiterentwickelt. Für den Fall, dass man dringend in einer Webapp ein simuliertes Dateisystem braucht, wird allgemein empfohlen, ein solches auf Basis von Indexed DB (die Blobs speichern kann) selbst zu bauen.

hgroup-Element - tot oder lebendig?

Ich versuche gerade mir HTML5 und CSS3 anhand deines Videotrainings von 2011 beizubringen. Ich bin dabei auf das <hgroup>-Element gestoßen, das laut einigen Meldungen seit Mitte letzten Jahres nicht mehr gültig sein soll. Auch du bestätigst diese Aussage auf deiner Webseite im August 2013, obgleich das Element auch heute noch – also ca. ein Jahr später – in den Spezifikationen der WHATWG auftaucht. Meine Fragen:

  1. Existiert <hgroup> nun endgültig nicht mehr oder ist sein Status weiterhin ungeklärt? Klar, kaum jemand benutzt es und deshalb kann von einer Nicht-Existenz gesprochen werden, aber ist es auch offiziell?
  2. Im Netz geistern die Alternativen zu <hgroup> herum. Inwiefern sind diese aktuell, bzw. offiziell?
  3. Wie gehst du in HTML mit auftauchenden Untertiteln, Zusatztiteln etc. um?

Das ist ausgesprochen kompliziert.

  1. Die Frage an dieser Stelle ist, was bei semantischen Elementen „Existenz“ ausmacht. Wie du richtig sagtest steht <hgroup> weiterhin in den Spezifikationen der WHATWG, fehlt aber beim W3C. Die Browser unterstützen meinen Tests zufolge das Element auch insofern, als dass es zumindest oberflächlich betrachtet implementiert ist (document.createElement('hgroup').toString() !== '[object HTMLUnknownElement]'). Andererseits macht das Element nur im Kontext des Outline-Algorithmus richtig viel Sinn und dass dieser Algorithmus ein reiner Papiertiger ist, ist eigentlich Konsens. Wenn man das Element in eine Webseite einbaut, macht es nichts. Man könnte also sagen, dass das Element zwar da ist, aber eigentlich nicht wirklich zu etwas gut ist. Das <hgroup>-Element ist Stand Mitte 2014 der Blinddarm von HTML5.
  2. Es gibt keinen in Stein gemeißelten 1:1-Ersatz. Die Spezifikationen von W3C und WHATWG haben je einen Abschnitt "Common idioms without dedicated elements" in dem es beim W3C auch Empfehlungen zu Untertiteln und derartigem abgibt. Das sind allerdings auch nur Vorschläge, denen man folgen kann, aber nicht muss.
  3. Ich persönlich folge den W3C-Empfehlungen, gruppiere also Überschriften mit Untertiteln usw. in <header>-Elementen, wobei die Untertitel keine Überschriften-Elemente, sondern irgend etwas anderes sind. Ich mache das nicht, weil es das W3C so empfiehlt, sondern weil die dortigen Vorschläge meiner Meinung nach einfach eine sehr gute Lösung darstellen. Würde man <hgroup> verwenden, wäre das eine (recht riskante) Wette darauf, dass sich dieses Element im Laufe der Zeit doch noch durchsetzt. Im Vergleich zu z.B. einem <header>-Element gibt es bzgl. Lesbarkeit des Quelltextes, Barrierefreiheit oder Browserunterstützung keinen Vorteil für <hgroup>. Der einzige Unterschied liegt in der Behandlung der enthaltenen Überschriften unter den Bedingungen des Outline-Algorithmus, der, wie erwähnt, keine Rolle spielt. Alles in allem: man könnte <hgroup> zwar benutzen ohne größere Katastrophen auszulösen, aber man hat nichts davon und geht dafür das Risiko ein, in Zukunft ein ungültiges Dokument haben (ohne dass die dann bestehende Ungültigkeit irgendwelche negativen Folgen hätte, denn jeder Browser auf diesem Planeten würde es ohne zu klagen verarbeiten).

Zwei zusätzliche Anmerkungen möchte ich zu diese Fragen noch loswerden. Erstens: Webstandards sind keine Wissenschaft. Der Prozess, in dem neue Regeln entstehen, ist unübersichtlich und obwohl es im Prinzip Regeln für den Prozess gibt, ist das Dehnen dieser Regeln an der Tagesordnung. Wird dann irgendwann mal eine neue definitive Regel aufgeschrieben, ist immer noch nicht auszuschließen, dass kurze Zeit später wieder die Kehrtwende kommt. Oder es kann passieren, dass das, was aufgeschrieben irgendwo steht, aus guten Gründen ignoriert wird. Es ist in letzter Konsequenz Politik.

Zweitens: „Offiziell“ ist überbewertet. Eigentlich ist etwas „offiziell“ ist etwas, sobald ein Dokument ein fertiger Webstandard (Recommendation) ist. Das allein muss aber auf die gelebte Praxis nicht unbedingt Auswirkungen haben. So war z.B. CSS 2 lange Zeit Recommendation, wurde aber dann effektiv zurückgezogen, weil sich kein Browser an das hielt, was in dem Dokument stand. Ein wie auch immer gearteter „offizieller“ Status ist nicht so wichtig wie die Frage, ob ein Feature funktioniert und sinnvolle Dinge ermöglicht. Ich würde behaupten, dass <hgroup> nicht offiziell existiert (auch wenn auch nicht explizit abgeschafft ist), nicht funktioniert (es macht in Browsern schließlich nichts) und sinnvolle Dinge ermöglicht es auch nicht. Also weg damit!

Text messen ohne Browser

Wie kann man in Node.js herausfinden, welche Abmessungen ein Text bei einer bestimmten Schriftgröße mit einer bestimmten Schriftart hat? Wenn möglich ohne einen headless browser wie PhantomJS o.Ä. bemühen zu müssen …

Auch hier hilft, man glaubt es kaum, HTML5! Das Canvas-Element bietet eine Funktion, die genau das Gewünschte macht: measureText(x) nimmt einen Text x und schaut nach, welche Ausmaße er bei den aktuellen Schrift-Settings hat. Und natürlich gibt es eine Canvas-Implementierung für Node.js. Diese ist zwar wegen diverser Abhängigkeiten nicht ganz so einfach zu installieren wie die meisten anderen Node-Module, aber wenn das geschafft ist, kann man ganz normalen Canvas-Code zum Textvermessen schreiben …

var Canvas = require('canvas');
var canvas = new Canvas(200, 200);
var ctx = canvas.getContext('2d');

// Schrift-Einstellungen
ctx.font = 'bold 16px Georgia';

// Text ausmessen
var textMetrics = ctx.measureText('Wie breit wird das hier?');

console.log(textMetrics);

 … und ihn in Node ausführen:

$ node node-canvas.js 
> { width: 199,
    actualBoundingBoxLeft: 1,
    actualBoundingBoxRight: 198,
    actualBoundingBoxAscent: 12,
    actualBoundingBoxDescent: 1,
    emHeightAscent: 15,
    emHeightDescent: 4,
    alphabeticBaseline: 0 }

Kein PhantomJS, keine ungewöhnlichen APIs: es geht einfach!

CSS und Shadow DOM

Wie verhält es sich mit Styles und JavaScript im Zusammenspiel mit Shadow DOM? Muss davon ausgegangen werden, dass Styles und Scripts aus dem Haupt-Dokument in das Shadow DOM von Web Component überschwappen und dort dann recht umfangreiche Resets nötig werden?

Das ist eine nicht ganz einfach zu beantwortende Frage. Vor allem darf man nicht vergessen, dass Shadow DOM noch eine sehr experimentelle Technologie ist und Änderungen durchaus im Bereich des möglichen sind. Entsprechend vorsichtig sollten die folgenden Aussagen behandelt werden.

Grundsätzlich ist die Antwort auf die Frage ein ganz klares Jain. Einerseits liegt um einen Shadow-DOM-Baum eine Grenze. Diese Grenze kann nicht von Selektoren (CSS oder JavaScript) überschritten werden. Befindet sich im Eltern-Dokument eine Style-Regel für z.B. <h1>-Elemente, wird diese Regel nur die Elemente betreffen, die direkt im Dokument stehen – eventuell in Shadow DOM vorhandene <h1>-Elemente bekommen von dieser Regel nichts mit. Umgekehrt sind in einem Shadow Tree definierte Styles auch nur innerhalb dieses Shadow Trees gültig und beeinflussen das Dokument, in das der Tree eingebunden ist, nicht.

Andererseits unterliegen mit einem Shadow DOM ausgestattete Elemente den Regeln der ganz normalen CSS-Vererbung. Gibt es in einem Shadow Tree beispielsweise keine Festlegung auf eine Schriftfarbe, so wird die Schriftfarbe verwendet, die das Elternelement des Shadow Trees hat. Ein kombiniertes Beispiel für beide Fälle könnte so aussehen:

<!doctype html>
<meta charset="utf-8">
<title>Shadow DOM</title>
<style>
  body {
    color: red;
  }
  span {
    font-weight: bold;
  }
</style>

<p>Im nächsten Element steht <span>Shadow DOM</span></p>
<p>Wenn du das hier lesen kannst, kann dein Browser kein Shadow DOM</p>

<script>
  var shadowHost = document.querySelectorAll('p').item(1);
  var shadowRoot = shadowHost.createShadowRoot();
  var content = '<style>span { letter-spacing: .5em; }</style>' +
                'Hallo <span>Welt!</span>';
  shadowRoot.innerHTML = content;
</script>

In einem Browser, der Shadow DOM korrekt umsetzt (z.B. Chrome 37) sollte das Ergebnis wie folgt aussehen:

Das Erscheinungsbild ergibt sich wie folgt:

  1. Der Text ist überall rot, auch im Shadow DOM, da diese Regel vom <body>-Element weitervererbt wird und das Shadow DOM keine eigene Schriftfarbe festlegt
  2. Nur <span>-Elemente außerhalb des Shadow DOM werden fett, da der Selektor nicht die Grenze zwischen Elterndokument und Shadow DOM überschreiten kann
  3. Nur das <span>-Element im Shadow DOM ist von der letter-spacing-Deklaration betroffen, da auch hier der Selektor an der Grenze hängen bleibt
  4. Fragt man mit JavaScript alle <span>-Elemente im Dokument ab, so wird nur ein einziges zurückgegeben – das Element außerhalb des Shadow DOM.

Zwischenzeitlich waren einige weitere Möglichkeiten geplant, um Einfluss auf die CSS-Balance zwischen Elterndokument und Shadow DOM zu nehmen. So sollte es einen Vererbungs-Reset für Shadow Trees geben, was in unserem Beispiel keine rote Farbe im Shadow DOM zur Folge hätte. Außerdem stand auch mal die Einführung eines Flags zur Diskussion, das CSS-Regeln aus dem Elternelement in das Shadow DOM durchgelassen hätte. Aktuell sind beide Features weder im neuesten Spezifikationsdraft noch in irgendwelchen Browsern zu finden.

Weitere Fragen?

Weitere Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich natürlich gerne! Sendet mir eure Fragen einfach per E-Mail oder Tweet und ich antworte so schnell ich kann. Und ansonsten kann man mich natürlich auch als Erklärbär zu sich kommen lassen.

Web Components erklärt, Teil 2: Web Components mit Polymer erstellen und verwenden

Veröffentlicht am 2. Juni 2014

Im ersten Teil der Serie rund um Web Components haben wir gesehen, dass wir es mit einer Sammlung verschiedener Technologien zu tun haben, die so orchestriert werden können, dass man mit ihnen eigene HTML-Elemente entwickeln kann. Das Ganze hat nur zwei Knackpunkte: erstens fehlen die meisten der neuen Features in den meisten modernen Browsern und zweitens ist es nicht ganz einfach, allein mit den neuen APIs eigene Elemente zu entwerfen. Die Web-Component-Technologien sind vielseitig und können für alles mögliche benutzt werden; selbsterfundene HTML-Elemente sind nur ein Use Case von vielen.

Die aktuell populärste Möglichkeit den Knackpunkten Browserunterstützung und API-Komfort zu begegnen besteht in der Verwendung von Polymer. Polymer ist ein von Google vorangetriebenes Open-Source-Projekt, das die Arbeit mit Web Components auf vielfältige Weise vereinfacht.

Was ist Polymer?

Polymer wird oft für einen reinen Polyfill für Web Components gehalten, ist aber erheblich mehr als das. Man kann grob von drei Teilen im Polymer-Projekt sprechen:

  1. eine Sammlung von Polyfills (platform.js) bringt Browsern die für Web Components nötigen Features (HTML Imports, Shadow DOM etc.) bei
  2. die Polymer-Library (polymer.html) stellt eine komfortable API für den Bau von eigenen HTML-Elementen bereit.
  3. eine Sammlung von fertigen Polymer-Elementen sorgt dafür, dass man nicht jeden Kleinkram selbst schreiben muss

Die Polyfills sind ein Teil des Gesamtprojekts, aber im Prinzip unabhängig vom Rest. Man kann auch aus dem ganzen Projekt nur die Polyfills nutzen und eigene Elemente auf eigene Faust entwickeln. Wenn man das nicht machen möchte, greift man zur Polymer-Library, mit der sich in Windeseile neue Elemente aufsetzen lassen. Angesichts der aktuellen Browser-Situation ist es sinnvoll, die Polyfills und die Library zu kombinieren, doch in (ferner) Zukunft wird es den Polyfill-Layer natürlich nicht mehr brauchen. Ist die Polymer-Library erst mal eingebaut, so kann man auf ein beachtliches Arsenal von fertigen Elementen zurückgreifen – es gibt Elemente für allerlei UI-Standardsituationen, aber auch für Low-Level-Operationen wie AJAX-Calls stehen fertige Tags parat.

Ja, HTML-Elemente für Ajax-Calls. Und für Media Queries und Single-Page-App-Router und viele andere Dinge, für die ein HTML-Element als API zunächst etwas seltsam anmutet. Im Kontext von Polymer sind solche Elemente allerdings häufig sehr sinnvoll, da sie sich gut mit anderen Elementen verdrahten lassen. Die Polymer-Library stellt nämlich sehr viel mehr Funktionen als nur ein überzuckertes document.register() zur Verfügung.

Ein erstes selbstgebautes Element mit Polymer

Das Polymer-Gesamtpaket (Polyfills und Library, ohne Fertig-Elemente) beschafft man sich am einfachsten via Bower:

$ bower install Polymer/polymer

Um Library und Polyfill zu nutzen sind nur zwei Dateien von Bedeutung: platform.js für die Polyfills und polymer.html für die Library. Letztere Datei ist für den Einsatz in selbstgebauten Elementen gedacht, erstere gehört in den <head> der Index-Seite:

<!doctype html>
<meta charset="utf-8">
<title>Index</title>
<script src="bower_components/platform/platform.js"></script>

Es ist absolut notwendig, platform.js möglichst weit oben in der Seite zu platzieren, da nur dann die Polyfills rechtzeitig in den Aktion treten können – ansonsten läuft man Gefahr, dass der Browser eventuell eingebaute Polymer-Elemente völlig falsch versteht.

Die kleinstmögliche Vorlage für ein Eigenbau-Element namens <x-foo>, die wir in die HTML-Datei x-foo.html stecken, sieht wie folgt aus:

<link rel="import" href="bower_components/polymer/polymer.html">

<polymer-element name="x-foo" attributes="">

  <template>
    Hier steht das <em>shadow dom!</em>
  </template>

  <script>
    Polymer('x-foo', {});
  </script>

</polymer-element>

Der HTML-Import ganz oben lädt die eigentliche Polymer-Library, die sich darum kümmert den restlichen Inhalt der Datei zu interpretieren. Das <polymer-element>-Element enthält alles, um unser Eigenbau-Element und z.B. dessen Namen und Attribute zu beschreiben. Im <template>-Element landet das Shadow-DOM und das Script meldet unser Element nebst APIs bei Polymer an. Und das war es im Prinzip schon! Wenn wir nun etwas Markup in das <template>-Element schreiben und wir die Datei x-foo.html per HTML Import in unsere Indexseite laden, können wir das Element verwenden:

<!doctype html>
<meta charset="utf-8">
<title>Index</title>
<script src="bower_components/platform/platform.js"></script>
<link rel="import" href="x-foo.html">

<x-foo></x-foo>

Das Ergebnis ist ein kleines HTML-Element, das Markup im Shadow DOM verbirgt und auch in heutigen Browsern ganz wunderbar funktioniert:

Das Element ist ansonsten ein HTML-Element wie jedes andere auch. Es kann mit document.createElement() erstellt werden und auch Click-Events und ähnliches können registriert werden. Das einzige Problem ist, dass das Element ziemlich nutzlos ist und kaum die wahren Fähigkeiten von Polymer demonstriert. Also bauen wir doch mal ein erstzunehmendes, nützliches und komplexes Element: ein Wetter-Widget! Folgende API ist unser (vorläufiges) Ziel:

<x-weather city="Berlin"></x-weather>

Eine konfigurierbare Wetter-Anzeige klingt nach etwas, das man benutzen möchte, das aber normalerweise aber mit viel JavaScript-Fummelei einzubauen wäre. Wir machen einfach ein HTML-Element draus.

Attribute und Data Binding

Wie wir ein Polymer-Element anlegen können, wissen wir bereits: man nehme eine Datei x-weather.html, kopiere dort die Element-Vorlage von oben hinein und ändere den Namen unseres neuen Elements im name-Attribut des <polymer-element>-Elements sowie im Script auf x-weather:

<link rel="import" href="bower_components/polymer/polymer.html">

<polymer-element name="x-weather" attributes="">

  <template>
  </template>

  <script>
    Polymer('x-weather', {});
  </script>

</polymer-element>

Wie wir unserem Element das Attribut city hinzufügen können, ist auch klar: wir tragen es einfach in das attributes-Attribut des <polymer-element>-Elements ein (mehrere Attribute kann man durch Leerzeichen getrennt eintragen). Aber was kann man dann mit dem Attribut machen? Im ersten Schritt könnte man es vielleicht einfach anzeigen. Polymer bringt sein eigenes Data Binding mit, d.h. man muss keine Extra-Libraries wie Knockout.js in Polymer-Elementen benutzen (obwohl man das natürlich könnte).

Um die Werte des Attributs city im Shadow DOM unseres Elements anzuzeigen, wird der Name des Attributs diese einfach in der Data-Binding-Syntax in das <template>-Element eingebunden:

<link rel="import" href="bower_components/polymer/polymer.html">

<polymer-element name="x-weather" attributes="city">

  <template>
    Das Wetter in <strong>{{city}}</strong>
  </template>

  <script>
    Polymer('x-weather', {});
  </script>

</polymer-element>

Das funktioniert soweit ganz gut: was immer wir auch in das Attribut hineinschreiben, das Shadow DOM spiegelt es wieder. Problematisch wird es nur, wenn das Attribut fehlt, denn dann steht plötzlich null im Shadow DOM. Wir brauchen also noch Default-Werte für die Attribute und hierbei hilft uns das bisher so stiefmütterlich behandelte <script>-Element in unserer Elementdefinition.

Der Aufruf von Polymer('x-weather', {}); meldet einerseits einfach das Element bei Polymer und damit beim Browser an, definiert aber auch den Prototype des Elements! Der Prototyp ist der ideale Ort für Attribut-Standardwerte. Fragt irgendwas (z.B. das Data Binding) das city-Attribut eines <x-weather>-Elements ab, das dieses Attribut nicht hat, so wird die Anfrage an den Prototypen delegiert und der Standardwert wird verwendet. Also tragen wir den Standardwert für city dort doch einfach ein:

<link rel="import" href="bower_components/polymer/polymer.html">

<polymer-element name="x-weather" attributes="city">

  <template>
    Das Wetter in <strong>{{city}}</strong>
  </template>

  <script>
    Polymer('x-weather', {
      city: 'Berlin'
    });
  </script>

</polymer-element>

Das kann sich für den Moment schon mal sehen lassen. Man könnte an dieser Stelle noch einiges an Detailarbeit verrichten (was passiert z.B. wenn Attribute nachträglich entfernt oder hinzugefügt werden?) aber für den Moment belassen wir das Element wie es jetzt ist. Wichtiger ist, dass wir auch tatsächlich das Wetter und nicht nur irgendwelche Attributwerte anzeigen. Hierfür brauchen wir natürlich eine Wetter-API, so wie die von openweathermap.org, und ein bisschen AJAX. Dieses bisschen AJAX werden wir bei dieser Gelegenheit auch mit einem Polymer-Element umsetzen.

Polymer-Elemente in Polymer-Elementen verwenden

Das Polymer-Projekt enthält neben Polyfills und der Polymer-Library auch eine ganze Sammlung gebrauchsfertiger Web Components (und wenn dort mal ein Element fehlen sollte, kann man noch bei Component Kitchen und anderen Seiten vorbeischauen). Eine dieser Fertig-Komponenten ist das <core-ajax>-Element, über das wir unsere API-Calls abwickeln werden. Installieren lässt sich das Element ganz einfach via Bower:

$ bower install Polymer/core-ajax

Polymer-Elemente wie unser <x-weather>-Element können andere Polymer-Elemente verwenden. Hierzu wird das eine Element einfach via HTML Import in das andere Element geladen und dann das neue Element einfach wie jedes andere Element im Template benutzt:

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-ajax/core-ajax.html">

<polymer-element name="x-weather" attributes="city">

  <template>
    Das Wetter in <strong>{{city}}</strong>
    <core-ajax
      auto
      url="http://api.openweathermap.org/data/2.5/weather"
      params='{"q":"{{city}}", "mode":"json", "units": "metric", "lang": "de"}'
      handleAs="json"
      response="{{weather}}"></core-ajax>
  </template>

  <script>
    Polymer('x-weather', {
      city: 'Berlin'
    });
  </script>

</polymer-element>

Das <core-ajax>-Element hat einen Haufen interessanter Attribute:

  • auto startet automatisch einen Request wenn das Element lädt oder wenn sich die Werte von url oder params ändern
  • url ist das Ziel unseres Requests
  • params enthält die Parameter des Requests in Form von JSON. Wichtig hier: wir können Data Binding in den Attributen verwenden und notieren deshalb für die Ort-Parameter q einfach {{city}}. Polymer kümmert sich darum, dass dort jeweils der Wert eingetragen wird, der auch im city-Attribut des Elements selbst steht.
  • handleAs legt fest, wie wir die Antwort verarbeiten möchten (als Text, JSON, XML etc.)
  • response legt den Namen der Variable fest, in dem das Ergebnis des Requests gespeichert wird

Standardmäßig setzt das Element GET-Requets ab, was ganz in unserem Sinne ist, weitere Attribute sind nicht nötig. Das wichtigste Attribut ist in unserem Fall das response-Attribut, denn dessen Wert (weather) können wir jetzt wiederum benutzen um die Wetterdaten anzuzeigen – ganz einfach mit Data Binding:

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-ajax/core-ajax.html">

<polymer-element name="x-weather" attributes="city">

  <template>
    Das Wetter in <strong>{{city}}</strong>:
    {{weather.main.temp}} ° C, {{weather.weather[0].description}}
    <core-ajax
      auto
      url="http://api.openweathermap.org/data/2.5/weather"
      params='{"q":"{{city}}", "mode":"json", "units": "metric", "lang": "de"}'
      handleAs="json"
      response="{{weather}}"></core-ajax>
  </template>

  <script>
    Polymer('x-weather', {
      city: 'Berlin'
    });
  </script>

</polymer-element>

Jetzt erscheint die Idee, für einen AJAX-Request ein HTML-Element zu verwenden doch gleich weniger schräg, oder? Dank Polymers Data Binding ist es ein Kinderspiel, Elemente miteinander zu verzahnen. Außerhalb von Polymer wird man vielleicht doch beim althergebrachten $.get() bleiben wollen, aber im Polymer-Kontext können auch nicht-sichtbare Dinge unter Umständen am besten als Elemement umgesetzt werden.

Und damit haben wir uns einen einigermaßen akzeptablen Wetter-Widget-Prototypen fertig gebaut:

Dieser Prototyp ist noch nicht wirklich tauglich für den Produktiveinsatz – unter anderem haben wir noch keinen einzigen Gedanken an Fehlerbehandlung oder eine JavaScript-API für das Element verschwendet. Aber immerhin zeigt das Element grob, was Polymer so zu leisten im Stande ist.

Fazit und weitere Schritte

Der Bau von <x-foo> und <x-weather> hat uns gezeigt, was Polymer kann und wie es sich zu Web Components allgemein verhält. Während „Web Components“ ein Dachbegriff für diverse neue Browser-Technologien ist, verwendet das Polymer-Projekt diese Technologien. Die Polyfills bringen ältere Browser auf den neuesten Stand, die Library ergänzt die Web-Component-Standardfeatures (HTML Imports, document.register()) um bequemere JavaScript-APIs und Bonusfunktionen wie Data Binding. Und mit der Sammlung vorhandene Elemente ist es kein Problem, in Windeseile auch auf den ersten Blick komplizierte Elemente wie das Wetter-Widget zusammenzuschustern.

Das Wetter-Widget in seinem aktuellen Zustand hat noch viel Verbesserungspotenzial und wir haben auch noch längst nicht die Möglichkeiten von Polymer ausgeschöpft. Das Widget könnte noch einen konfigurierbaren Update-Intervall gebrauchen, souveräner auf fehlschlagende Requests oder ein plötzlich entferntes city-Attribut reagieren und es könnte viel viel flexibler sein! Es will ja schließlich nicht jeder nur eine Zeile Text mit der Temperatur und dem Beschreibung haben. Das ist alles machbar und alles kein Hexenwerk, wie wir im nächsten Teil der Serie sehen werden.

Web Components erklärt, Teil 1: Was sind Web Components?

Veröffentlicht am 28. Mai 2014

Bei der Aufnahme der letzen Folge Working Draft (Revision 173) ging es um die neuen Features in Chrome 36, der aktuell im Beta-Stadium ist. Die Neuerungen sind vor allem für Web Components relevant … aber was sind „Web Components“ eigentlich? Was machen Sie, kann man sie schon benutzen und wie verhalten sich sich zu Projekten wie Polymer und X-Tag? Zeit das alles mal aufzuschreiben.

Welches Problem lösen Web Components?

Es gibt heutzutage kein Plugin-System für Frontend-Entwicklung. Es gibt allerlei Plugins für verschiedene Dinge, aber ein übergreifendes System steckt nicht dahinter. Einer Webseite eine neue Funktion beizubringen ist eine recht unangenehme Aufgabe, da es so viele verschiedene Wege gibt und all diese Wege irgendwelche Knackpunkte haben. Kommt die neue Funktion als jQuery-Plugin daher, braucht man jQuery. Wird sie in Form eines AMD-Moduls geliefert, muss man auch hierfür eine Library zur Hand haben und sich obendrein einen Wolf konfigurieren. NPM-Module müssen durch Browserify geheizt werden, „normale“ Libraries rümpeln irgendwelche Variablen in den globalen Scope (wenn man richtig Glück hat ist auch CSS dabei) und egal welchen Weg man beschreitet: am Ende muss man sich mit einer idiosynkratischen JavaScript-API herumschlagen. So baut man zum Beispiel eine Google-Map ein:

<script src="http://maps.googleapis.com/maps/api/js?key=APIKEY&amp;sensor=true">
</script>

<script>
  new google.maps.Map(document.getElementById('Map'), {
    center: new google.maps.LatLng(-52.033, 8.533),
    zoom: 8,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  });
</script>

Man darf also sowohl etwas HTML als auch zwei Scripts manuell irgendwie in die Seite bringen und man muss dafür sorgen, dass das JavaScript zum richtigen Zeitpunkt ausgeführt wird. Und das schlimmste ist: dieses Vorgehen und diese API funktionieren so nur für Google-Maps, auf andere Plugins kann man sein Wissen nicht anwenden. Ein ziehbares Element mit jQuery UI erstellt man völlig anders:

$('#draggable').draggable({ 
  containment: "parent" 
});

Die API ist einerseits völlig anders, allerdings hat man auch hier das Vergnügen, die Abhängigkeiten und das Element selbst einzubauen, die JavaScript-Ausführung zu timen und das Ganze auf eine Art und Weise mit CSS zu versehen, die hoffentlich weder im Rest der Webseite noch in dem Plugin selbst etwas falsch macht.

Mit Web Components verändert sich der Plugin-Einbau so, dass die Google-Map der Zukunft so eingebunden werden wird:

<link rel="import" href="google-map-plugin.html">

<google-map
  latitude="-52.033"
  longitude="8.533"
  zoom="8"
  type="roadmap"></google-map>

Das Plugin ist ein selbstdefiniertes HTML-Element, das über ein <link rel="import"> in die Ziel-Seite geladen wird. Das Element enthält intern allerlei Markup, CSS und JavaScript, das vom Rest der Seite isoliert ist – externes CSS kann nicht ohne weiteres das interne Markup verunstalten und internes CSS kann die Außenwelt nicht beeinflussen. Die Konfiguration des Plugins erfolgt vor allem über HTML-Attribute, aber auch JavaScript-Methoden und DOM-Events können bereitgestellt werden.

Web Components haben gegenüber dem heutigen Plugin-Wirrwarr eine Reihe von Vorteilen:

  1. Sie sind einheitlich, denn alles ist ein HTML-Element
  2. Sie sind einfach zu benutzten, denn jeder kann schon HTML. Selbst für technisch nicht sonderlich bewanderte CMS-Redakteure sollten Web Components zu meistern sein.
  3. Sie sind kombinierbar, denn sie sind ja nur HTML-Elemente. Eine Konstruktion wie <light-box><google-map></google-map></light-box> funktioniert auch ohne dass die beiden Elemente aufeinander abgestimmt sein müssen, so wie auch <div><p></p></div> einfach funktioniert
  4. Sie kapseln ihren inneren Aufbau (HTML, CSS, JS) und verhindern so Konflikte mit anderen Plugins oder Scripts auf der Webseite.

Web Components lösen also das Problem, dass Frontend-Entwicklung kein Plugin-System hat. Das Webseiten-Plugin der Zukunft ist ein selbstdefiniertes HTML-Element mit komplexem, aber von der Außenwelt sauber abgekapseltem Innenleben, das sich nach außen verhält wie jedes andere Element auch. Damit all das aber in jedem Browser funktioniert, muss eine ganze Menge zusammenkommen …

Was konkret sind Web Components?

Ähnlich wie „HTML5“ keine konkrete Technologie, sondern ein Dachbegriff für viele verschiedene Technologien ist, gibt es auch keine direkte „Web-Component-API“ im Browser. Stattdessen erlaubt ein Set von neuen APIs das Erstellen von Web Components, doch diese APIs können auch für viele andere Dinge verwendet werden. Die wichtigsten APIs aus dem Web-Component-Universum sind die folgenden:

Custom Elements

Wer eigene HTML-Elemente definieren möchte, braucht dafür die entsprechende Browser-Schnittstelle, die von der Custom Elements Specification beschrieben wird. Vom Prinzip her ist das Ganze kein Hexenwerk; man meldet sein neues Element einfach per document.registerElement() an und schon klappt es:

document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    // ...
  })
});

Das hier erstellte Element <x-foo> ist in dieser Form nicht mehr als ein etwas anders benanntes <div> ohne besondere Fähigkeiten. Den neuen Elementen spezielle Funktionen beizubringen ist schon etwas komplizierter, geht aber natürlich auch. Aber Stichwort Name: selbstgebaute Elemente müssen im Unterschied zu normalen Elementen einen Bindestrich im Namen haben (z.B. <google-map> oder <x-widget>), genießen aber sonst ziemlich die gleichen Rechte wie normale, eingebaute HTML-Elemente.

HTML Templates

HTML Templates sind genau das, was der Name vermuten lässt: Templates. Das Markup in einem <template>-Element wird vom Browser verarbeitet, aber nicht angezeigt – nur über eine JavaScript-API kann man den Inhalt (ein gebrauchsfertiges DocumentFragment) auslesen, eventuell vorhandene Platzhalter ersetzen und das Endergebnis dann irgendwo einfügen:

<template>
  <p>Ich bin Eintrag Nr. {{num}}</p>
</template>

<script>
  var tpl = document.querySelector('template').content;
  for(var i = 0; i < 3; i++){
    var p = tpl.querySelector('p').cloneNode(true);
    p.innerHTML = p.innerHTML.replace('{{num}}', i + 1);
    document.body.appendChild(p);
  }
</script>

Mit HTML Templates gibt es einen bequemen Weg, unsichtbar Markup für die Verwendung in Scripts überall in einer Webseite (oder einer Komponente) unterzubringen.

Shadow DOM

Das Shadow DOM eines Elements ist eine Art Sub-DOM-Baum in einem Element, der nicht für normale Scripts und andere Einflüsse zugänglich ist – der Sub-Baum versteckt sich eben im Schatten. Nimmt man es ganz genau, so ist der Shadow-Baum nicht wirklich ein Sub-Baum des Elements, sondern steht komplett außerhalb des normalen DOM und wird nur mit dem Element verbunden.

Ein bekanntes Beispiel für Shadow DOM sind die Bausteine des <progress>-Elements. In den meisten Browsern ist der Fortschrittsbalken über normales HTML umgesetzt, aber da es sich eben um Shadow DOM handelt, sieht man von diesem Unterbau nichts, wenn man das Element inspiziert. Und auch selbstgebautes Shadow DOM ist möglich! Im folgenden Codeschnipsel wird ein Shadow-DOM-Baum in ein ganz normales Element eingehängt. Dieses Shadow DOM erscheint dann wie ein fester Bestandteil des Elements, so wie die Steuerungselemente in einem HTML5-Videoplayer:

var widget = document.createElement('div');
var shadowRoot = widget.createShadowRoot();
var content = document.createElement('p');
content.innerHTML = 'Hallo Welt!';
shadowRoot.appendChild(content);
document.body.appendChild(widget);

Es ist klar, dass für Web Components, die ja ein komplexes Innenleben in selbstgebauten HTML-Elementen umsetzen wollen, Shadow DOM eine ganz zentrale Technologie darstellt.

HTML Imports

HTML Imports erlauben das Laden von HTML-Dokumenten in andere HTML-Dokumente:

<link rel="import" href="plugin.html">

Das ganze funktioniert nicht direkt wie z.B. ein include() in PHP o.Ä., sondern eher ein bisschen wie <template>-Elemente. Der Browser lädt das verlinkte Dokument herunter, liest es ein, lädt alle dort referenzierten Ressourcen (CSS, JS etc.) und verarbeitet das HTML. Das Resultat wird aber nicht einfach an Ort und Stelle in die Seite gekippt, sondern in der DOM-Eigenschaft import bereitgestellt. Hier findet man ein komplettes Document-Objekt für die geladene Ressource vor und kann sich nach Herzenslust daran bedienen: enthaltene Templates klonen, eingebundene Scripts ausführen  – was auch immer gerade anliegt.

Im Web-Component-Kontext kann ein solcher Import alle Daten für ein Element enthalten: die nötigen Templates, das Script zum Anmelden eines neuen Tags und was sonst noch so gebraucht wird. Für alle, die neue Elemente nur benutzen und nicht unbedingt selbst schreiben wollen, reduziert sich also der gesamte Arbeitsaufwand auf das Einbinden eines einzigen <link>-Elements (und natürlich das Einbinden des Importierten Custom Elements selbst).

Diverse weitere Technologien

Es gibt noch diverse weitere neue Browser-APIs, die oft mit Web Components assoziiert werden – Object.observe(), Scoped Style Sheets und viele weitere. Inwiefern man das alles braucht, kommt ein bisschen darauf an, wie genau man an Web Components herangeht, denn theoretisch führen viele Wege nach Rom – im Prinzip auch heute schon.

Was kann man heutzutage schon mit Web Components anfangen?

Den meisten Browsern fehlen noch die meisten Technologien für Web Components. In Chrome wird ab Version 36 so gut wie alles fast komplett implementiert sein, aber auch nur fast – und von den anderen Browsern wollen wir lieber gar nicht reden. Allerdings lässt sich mit einer Mixtur aus Polyfill und Abstraktion das Meiste durchaus schon hinkriegen. Dinge wie ein <google-map>-Element sind keine Papiertiger, sondern tatsächlich heute schon exakt so benutzbare Web Components. Und die Benutzung und Erstellung solcher Components passiert heute schon auf eine Weise, die recht nah an der zukünftigen Alltagspraxis sein dürfte. Natürlich braucht man heutzutage Polyfills und Libraries, doch die wird man auch in 5 Jahren noch brauchen – selbst wenn jeder Browser alle Web-Component-APIs vollständig unterstützt.

Nicht vergessen: „Web Components“ ist nur ein Überbegriff für viele verschiedene Technologien, die das Erstellen eigener Komponenten ermöglichen. Diese Technologien sind aber nicht allein auf den Komponenten-Bau ausgerichtet, sondern man kann sie für viele verschiedene Dinge benutzen. Es braucht immer noch einen JavaScript-Layer, der die Einzelteile zu einer komfortablen Component-API zusammenknotet. Entsprechend wird man auch in fernster Zukunft eine JavaScript-Library brauchen, die das Erstellen eigener Elemente mit Shadow DOM und Konsorten vereinfacht, eine Art jQuery für Eigenbau-Elemente. Heutzutage kommt auf diese Library einfach noch ein Haufen Polyfills obendrauf. Hat man aber einerseits die Polyfills, andererseits die Library und keine Angst vor experimenteller Software, so kann man heutztage durchaus schon seine eigenen Elemente bauen und in den meisten modernen Browsern herumspielen. Wie genau das konkret funktioniert, sehen wir dann in Teil 2 dieser kleinen Serie.