Web Components

Um Web Components frühzeitig für Entwickler zugänglich zu machen, veröffentlichte Google 2013 das Framework Polymer und bezeichnete es als „Zucker für Web Components“. Die noch fehlende native Browser-Unterstützung wird dabei durch Polyfills hinzugefügt. Somit lassen sich Web Components bereits heute in allen gängigen Browsern nutzen. Darüber hinaus bietet Polymer eine Vielzahl fertiger Komponenten, um das Erstellen von Webapplikationen einfacher und die Entwicklung produktiver zu gestalten. Sie ermöglichen grundlegende Funktionalitäten, wie Unterstützung für AJAX Requests, Toolbars, weitere Navigationselemente, Eingabefelder, Dropdowns und vieles mehr.

Wir zeigen hier die grundlegenden Konzepte zur Verwendung von Polymer in der aktuellen Version 1.0.

Polymer Base

Polymer.Base dient als Prototyp für alle Polymer Komponenten. Über Polymer.Base wird grundsätzliche Funktionaltität wie das Feuern von Events oder die Handhabung von Arrays zur Verfügung gestellt. Letzteres ist vor allem notwendig, um Arrays observierbar zu machen. Außerdem bildet Polymer.Base den Prototypen aller Polymer Komponenten.

Typischerweise besteht eine Polymer Komponente aus drei Teilen: Stylesheets, einem Template und natürlich JavaScript. Ein einfaches Beispiel könnte wie folgt aussehen:

<dom-module id="my-component">
    <style> h1 { color: blue; } </style>
    <template><h1>Eine Überschrift</h1></template>
    <script>Polymer({ is: 'my-component' });</script>
</dom-module>

Der oben gezeigte Code definiert bereits eine vollständige Komponente. Sie kann mit <my-component> direkt im HTML eingesetzt werden. In diesem einfachen Beispiel würde lediglich eine blaue Überschrift gezeichnet werden.

Properties

Eine Property kann, in erster Linie, auf Änderungen überwacht werden. Darauf werden wir aber später in diesem Artikel noch eingehen. Außerdem kann mit Properties eine Schnittstelle über HTML Attribute definiert werden. Um in unserer Beispiel-Komponente (<my-component>) den Text der Überschrift von außen definierbar zu machen, wären folgende Änderung nötig.

/*...*/
<template><h1>[[headline]]</h1></template>
<script>
Polymer({
    /*...*/
    properties: { headline: String }
});
</script>
/*...*/

Die Komponente <my-component> interpretiert jetzt das Attribut „headline“ und setzt damit den Wert der gleichnamigen Property. Außerdem ersetzt sie den Platzhalter [[headline]] im Template durch den Wert der entsprechenden Property. Im HTML Dokument würde das wie folgt benutzt werden:

<my-component headline="Eine Überschrift"></my-component>

Die Definition der Property headline: String ist eine Kurzform und bedeutet, diese Property ist vom Typ String. Properties haben aber weitaus mehr Eigenschaften. Die lange Variante für die Definition von headline sähe daher so aus:

Polymer({
    /*...*/
    properties: {
        headline: {
            type: String,
            value: '',
            readOnly: false,
            notify: false,
            reflectToAttribute: false
        }
    }
});

Da, abgesehen von type, alle Eigenschaften dem Default entsprechen, müssen diese nicht explizit gesetzt werden.

Während readOnly und value eindeutig sind, benötigen die anderen Eigenschaften ein paar mehr Worte. „notify“ definiert, ob eine Änderung dieser Property nach außen hin kommuniziert wird. Sollte die Property zum Beispiel den Inhalt eines Textfeldes repräsentieren, der von einer anderen Komponente gespeichert werden soll, müsste notify auf true gesetzt werden. Eine Änderung des Wertes dieser Property feuert dann ein Event, das von außen sichtbar ist.

reflectToAttribute wird nötig, wenn der Wert der Property auch im HTML sichtbar sein soll. Das ist zum Beispiel dann notwendig, wenn ein CSS-Selektor mit diesem Attribut arbeitet. Andernfalls wird nur das unterliegende Datenmodell aktualisiert, was natürlich schneller ausgeführt wird, als noch zusätzlich das DOM mit zu ändern.

Observer

Wie oben erwähnt, können Properties auf Änderungen überwacht werden. Ein Tool dafür ist der Observer. Ein Observer verweist auf eine Funktion, die bei Änderungen einer oder mehrerer Properties aufgerufen wird.

Polymer({
    /*...*/
    properties: { headline: String },
    observers: [ '_headlineChanged(headline)' ]
    _headlineChanged: function(headline) { /*...*/ }
});

Möchte man nur eine einzelne Property überwachen, können Observer auch direkt an selbiger definiert werden. In dem Fall wird der alte Wert als zweiter Parameter übergeben.

Polymer({
    /*...*/
    properties: {
        headline: { type: String, observer: '_headlineChanged' }
    },
    _headlineChanged: function(newH, oldH) { /*...*/ }
    /*...*/
});

Computed Bindings

Computed Bindings sind Properties, die andere Properties kombinieren. So könnte zum Beispiel die Property headline aus zwei Teilen zusammengesetzt werden. Einem Prefix und einem Suffix:

Polymer({
    /*...*/
    properties: {
        prefix: String,
        suffix: String,
        headline: {
            type: String,
            computed: ' _computeH(prefix, suffix)'
        }
    },
    _computeH: function (pref, suf) {
        return pref + " - " + suf;
    }
});

Computed Bindings können wie andere Properties mit Observern auf Änderungen überwacht werden oder zu neuen Computed Bindings kombiniert werden. Events Neben Properties können auch Events als Schnittstelle einer Komponente nach außen dienen. Polymer.Base stellt dafür die Funktion fire zur Verfügung. Im folgenden Beispiel wird beim Click des Buttons ein spezielles Event gefeuert.

<template>
    <button on-click="_handleClick"></button>
</template>
<script>
    Polymer({
        /* … */
        _handleClick: function () {
            this.fire('something-happened', { data: 'some' });
        }
    });
</script>

In Polymer gibt es zwei Möglichkeiten auf Events zu reagieren. Die erste Option ist im obigen Beispiel zu sehen, on-click registriert einen Event-Listener für das „click“-Event des Buttons. Für den Fall, dass ein Event-Listener nicht auf ein spezielles HTML Element gelegt werden kann, kann man außerdem generelle Listener definieren. Eine andere Polymer Komponente kann beispielsweise wie folgt auf das Event something-happened hören.

Polymer({
    /* .. */
    listeners: { 'something-happened': '_someListener' }
});

Vererbung und Behaviors

Seit der Version 1.0 erlaubt Polymer keine Vererbung von Custom Elements mehr. Um zentrale Funktionalität trotzdem wieder verwendbar machen zu können, gibt es neben der Komposition auch die Möglichkeit sogenannte Behaviors zu verwenden. Behaviors sind JavaScript Objekte, die bei der Definition einer Polymer Komponente referenziert werden können. Die Implementierung wird dann, wie üblich bei prototypischer Vererbung, für die Polymer Komponente übernommen. Eine Polymer Komponente kann auch mehrere Behaviors referenzieren.

Polymer({
    is: 'my-element',
    behaviors: [MeinBehavior, MeinZweitesBehavior]
});

Nach wie vor können native HTML Elemente erweitert werden. Würde man das native <input> Element beispielsweise so erweitern wollen, dass es einen roten Rand hat, kann das wie folgt realisiert werden.

Polymer({
    is: 'my-input',
    extends: 'input',
    created: function () { this.style.border = '1px solid red' }
});
#### Templating Polymer macht Gebrauch vom neuen HTML Template Element. Folgend ist ein einfaches Polymer Template:
<template><div>[[article]]</div></template>

Im Zusammenhang mit Properties, weiter oben in diesem Artikel, sind wir schon Platzhaltern wie [[article]] begegnet. Er wird durch den Wert der gleichnamigen Property des Polymer Elements ersetzt. Bei Änderung eines Property Werts wird das Template automatisch neu ausgeführt. Bei einer Liste von Artikeln sieht das Ganze so aus.

<template>
    <template is="dom-repeat" items="[[articles]]">
        <div>[[item.text]]</div>
    </template>
</template>

dom-repeat ist eine Erweiterung des Template Elements. Das Template wird so für jeden Listeneintrag erneut ausgeführt. Das aktuelle Element der Liste wird auf die Variable item gebunden. Für verschachtelte Schleifen kann dieses Default-Verhalten natürlich überschrieben werden.

In den vorangegangen Beispielen war die Fließrichtung der Daten unidirektional. Daher, wenn sich das Datenmodell ändert, wird das Template erneut ausgeführt. Um aber beispielsweise Nutzereingaben in das Datenmodell zurückzuspielen, muss ein Two-Way-Binding verwendet werden. Das wird erreicht, indem anstelle eckiger, geschweifte Klammern verwendet werden.

<template>
    <h1>[[titel]]</h1>
    <paper-input value="{{titel}}"></paper-input>
</template>

Shady DOM vs Shadow DOM

Shadow DOM führt das Konzept des Tree Scoping ein. Tree Scoping ermöglicht es, einen Unterbaum des DOMs vor dem Hauptdokument zu verstecken. So kann ein Web Entwickler Elemente wie das <video> Tag selbst implementieren. Genau wie beim <video> Tag würden dem Anwender des Elements die Implementierungsdetails verborgen bleiben. Der Unterbaum hat seinen eigenen Geltungsbereich. In Browsern, die Shadow DOM unterstützen, ist dieser Unterbaum auch im Web-Inspektor unsichtbar, kann aber auf Wunsch eingesehen werden.

Bislang hat eine Implementierung der Shadow DOM Spezifikation noch nicht den Weg in alle Browser gefunden. In früheren Releases besaß Polymer deshalb ein vollständiges Polyfill. Das wurde allerdings zu kompliziert, umfasste viele Zeilen Code und war zu langsam. Daher hat das Polymer Team eine alternative Implementierung zur Verfügung gestellt und nannte es Shady DOM.

Shady DOM implementiert das Tree Scoping, wie der ursprüngliche Polyfill auch. Im Web-Inspektor wird jedoch auch der Unterbaum dargestellt. Das Tree Scoping ist nur noch in Form einer API abgebildet. Die API ist dabei gleich der des Shadow DOM, die unterliegenden Implementierungen können daher ausgetaucht werden. Auf Wunsch kann Shadow DOM also eingeschaltet und auf Shady DOM verzichtet werden.

CSS Custom Properties

Custom Properties sind Variablen für CSS. Eine Property wird folgendermaßen deklariert,

<style>
    :root {
        --primary-color: #3f51b5;
    }
</style>

und anschließend so benutzt,

<style>
    button { background-color: var(--primary-color); }
</style>

Der :root Selektor gibt der Property globale Gültigkeit. Um eine Property nur für einen bestimmten Geltungsbereich zu setzen, kann jeder beliebige Selektor verwendet werden.

Mit Hilfe von Custom Properties kann dem Benutzer eines Custom Elements aber auch eine Art Styling Schnittstelle verfügbar gemacht werden.

<style>
    button {
        background-color: var(--btn-bg-color, --primary-color);
    }
</style>
<template>
    <!— some content >
    <button>OK</button>
</template>

Der Benutzer einer solchen Komponente kann dann die Property --btn-bg-color definieren, um <button> eine Farbe zu geben. Wird die Property nicht gesetzt, gilt der zweite Parameter der Funktion var().

Da die Werte für jede CSS Anweisung einzeln exponiert werden müssen, hat der Anwender mit diesem Mechanismus nur sehr beschränkte Veränderungsmöglichkeiten. Polymer löst dieses Problem mit einer experimentellen Erweiterung names CSS Mixins. Mixins werden mit @apply auf einen Selektor angewendet, und können beliebig viele CSS Anweisungen beinhalten.

button { @apply(--my-button-theme); }

my-special-component {
    --my-button-theme {
        background-color: red;
        color: white;
    }
}

Fazit

Zusammenfassend lässt sich sagen, dass Polymer eine gute Idee ist. Es deckt alles ab was nötig ist, um einen grundlegenden Elemente-Katalog zu implementieren. Bei Custom Elements kann völlig frei das Aussehen durch Templates und CSS, und das Verhalten durch Javascript definiert werden. Mit WebComponents als Grundlage und der daraus entstehenden Kapselung der einzelnen Komponenten können unabhängige Basis-Module erzeugt werden, die sehr einfach zu benutzen sind. Es muss lediglich das Element importiert und wie ein normales HTML-Tag benutzt werden. Außerdem lassen sich Custom Elements weiter kombinieren, womit HTML-Tags mit höherer Semantik erzeugt werden können (die daraus resultierenden tief-verschachtelten Abhängigkeits-Bäume lassen sich mit Tools wie Bower gut beherrschen).

Ein Problem das leider weiterhin bleibt ist die Geschwindigkeit. Auch mit der Version 1.0, die gegenüber 0.5, die Performance stark verbessert hat, werden komplexe Seiten selbst in Browsern mit hoher nativer Unterstützung schnell träge. Es bleibt zu hoffen, dass sich dieses Problem löst, denn tatsächlich wird Polymer bereits vermisst, wenn das erste „Partial“ gebaut werden muss, weil CSS nicht mehr ausreicht um seine Wunschvorstellung zu realisieren.