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.