Web Components erlauben in modernen Browsern, eigene Tags mit beliebig komplexen Verhalten zu definieren. Und wie der Name „Komponente“ schon nahelegt, kann man diese kleinen Bibliotheken beliebig oft weiterverwenden, sobald diese einmalig geladen wurden.

Mit einigen kleinen Kniffen kann die Entwicklung solcher Komponenten noch schneller von der Hand gehen.

Wie starte ich mit Web Components?

Für den Einstieg in das Thema Web Components empfiehlt sich die Lektüre der ausgezeichneten Einführung von Google und CSS-Tricks. Andere Anleitungen basieren teilweise auf älteren Ideen, die getrost ignoriert werden können.

Außerdem lohnt es sich immer, moderne Beispiele anzuschauen. Ich für meinen Teil habe entsprechend versucht, einen Horizontal Situation Indicator als Vanilla-JavaScript Web Component mustergültig zu bauen. Der daraus resultierende Quellcode der „Horizontal Situation Indicator“ Web Component wird im Rahmen dieses Artikels immer wieder als Beispiel herangezogen.

Wie denkt man eine Web Component?

Die größte Hürde für den angehenden Komponenten-Bauer ist das Verständnis, wie all die schönen Teile zusammenpassen.

Da eine Web Compoment gekapselt ist, kann nur über eine vorher definierte Schnittstelle von außen Zugriff auf ihr Verhalten genommen werden. Es ist also sehr sinnig, die Konzeption und den Bau einer Web Component wie den Bau einer Schnittstelle bzw. eines Interfaces zu verstehen.

Als Beispiel nehmen wir einfach die fertige Implementation der HSI-Web-Component:

<horizontal-situation-indicator id="hsi" heading="45.0" heading-select="0.0"></horizontal-situation-indicator>

Attribute bzw. Properties

Beim Bau einer Web Component müssen neben dem Namen der Web Component auch die Attribute der Web Component und ihre möglichen Werte definiert werden.

Diese Attribute verwandeln sich innerhalb der JavaScript-Repräsentation der Web Component in Properties, die mit den Attributen synchronisiert sind:

const el = document.getElementById('hsi');
el.getAttribute('heading'); // "45.0"
el.heading; // "45.0"

el.setAttribute('heading', '60.0');
el.getAttribute('heading'); // "60.0"
el.heading; // "60.0"

el.heading = '135.0';
el.getAttribute('heading'); // "135.0"
el.heading; // "135.0"

Bei dem Zugriff auf eine Property mit einem - im Namen funktioniert der Zugriff leicht anders:

el.getAttribute('heading-select');
//el.heading-select existiert nicht
el['heading-select']; // korrekte Schreibweise in `[]`

Die Schreibweise mit [] erlaubt auch den dynamischen Zugriff auf Properties:

let attrName = 'heading-select';

el.getAttribute(attrName);
el[attrName];

Methoden

Darüber hinaus kann der Entwickler einer Web Component noch festlegen, dass die Komponente JavaScript-Methoden anbietet. Diese erlauben zum Beispiel von außen der Komponente zu befehlen, komplexe Prozesse innerhalb der Komponente zu erledigen.

el.synchronizeHeading();

Da JavaScript keine Sichtbarkeiten wie public und private für Methoden hat, hat sich als Konvention herausgebildet, private Methoden mit einem _ zu beginnen.

Events

Außerdem kann eine Web Component noch JavaScript-Events erzeugen, die außerhalb der Komponente registriert werden können. So emittiert z.B. das <video>-Tag Events, wenn ein Video beendet wurde, was ohne dieses Event außerhalb des Tags keiner wissen könnte.

Was kann eine Web Component alles darstellen?

Web Components können fast alles darstellen, was auch regulär in einem Browser dargestellt werden kann. Besonderes Augenmerk muss aber darauf gelegt werden, dass alle benötigten Bestandteile in der einen JavaScript-Datei enthalten ist, die die Web Component definiert.

Am Einfachsten einzubinden sind die folgenden Dinge:

  • HTML
  • CSS für die Web Component
  • JavaScript-Interaktion für die Web Component

Alle anderen Ressourcen (Bilder, Videos, Töne) können mit einem Trick eingeschmuggelt werden: Mittels Data URLs können Binär-Dateien in Base64-Zeichenketten umgewandelt werden, die dann z.B. im src-Attribut eines <img> eingebunden werden können.

Besonders spannend: SVG-Bilder sind nicht nur schön kompakt in Bezug auf ihren Speicherplatz, sondern können auch direkt in das HTML eingebunden werden – benötigen also den Base64-Trick nicht.

Gibt es Build Tools für Web Components?

In der Tat gibt es eine ganze Menge Tools, um das Zusammensetzen der einzelnen Teile einer Web Component zu unterstützen. In der Regel reicht aber ein kleiner flotter Node.js-Mehrzeiler als Web Component Build Tool, der aus einzelnen Dateien die eigentliche Web Component zusammensetzt. Die Kurzfassung:

const fs = require('fs');

let source      = fs.readFileSync(`horizontal-situation-indicator.js`).toString();
let templateCss = fs.readFileSync(`src/horizontal-situation-indicator.css`).toString();
let templateSvg = fs.readFileSync(`src/horizontal-situation-indicator.svg`).toString();

source = source.replace(/(<style>).*(<\/style>)/ms, templateCss);
source = source.replace(/(<\/style>).*(`)/ms, templateSvg);
fs.writeFileSync(`horizontal-situation-indicator.js`, source);

Das tatsächliche Skript ist zwar etwas komplexer, das Grundprinzip ist aber ein denkbar einfaches: Die Entwicklung von SVG und CSS (oder jedem anderen Dateitypen) findet in separaten Dateien statt, die mit dem obigen Skript einfach in das JavaScript der Web Component hineinkopiert werden. Unter anderem könnte hier auch die Konvertierung von Binär-Daten in ihre Base64-Entsprechung durchgeführt werden.

Kann man das Erzeugen von get und set für jede Property abkürzen?

Da jeder Web Component eine Liste der zu synchronisierenden Attribute / Properties mit der Methode observedAttributes bekannt gemacht werden muss, kann genau diese Liste im constructor auch zum programmatischen Erzeugern von Gettern / Settern verwendet werden.

this.constructor.observedAttributes.forEach((attrName) => {
  Object.defineProperty(this, attrName, {
    get() {
      return this.getAttribute(attrName);
    },
    set(attrValue) {
      this.setAttribute(attrName, attrValue);
    }
  });
});

Diese Methode hat in einigen Web-Components-Frameworks möglicherweise Nachteile – für die Vanilla-Nutzung ist sie aber weitestgehend ungefährlich.

Wie löse ich Styling in der Web Component?

Da Web Components sowieso nur in aktuellen Browsern zuverlässig funktionieren, kann man sich gleichzeitig auch auf fortgeschrittene CSS-Möglichkeiten verlassen. Um CSS innerhalb der Komponente von außen zu beeinflussen, verwende ich CSS-Variablen bzw. CSS-Custom-Properties. Innerhalb des CSS' des Komponente definiere ich sie direkt an der DOM-Wurzel der Komponente:

:host {
  --background-color: black;
  --foreground-color: white;
  --heading-select-color: cyan;
  --stroke-width: 0.5;
}
/*…und verwende diese CSS-Custom-Properties dann später in Variablen - bei mir z.B. als SVG-CSS-Eigenschaften:*/

#background {
  fill: var(--background-color);
}
* {
  fill: var(--foreground-color);
}
*[stroke] {
  stroke-width: var(--stroke-width);
}

#heading-select {
  fill: var(--heading-select-color);
}

Wer nun auch immer diese Komponente verwendet, kann diese CSS-Custom-Properties von außen beeinflussen:

horizontal-situation-indicator {
  --heading-select-color: red;
  --stroke-width: 1;
}

Bei der Beispiel-Implementation von <horizontal-situation-indicator> kann auch bewundert werden, wie durch JavaScript diese CSS-Custom-Properties am lebenden Objekt verändert werden, und in der Komponente sich alles fröhlich verfärbt.

Ganz nebenbei haben wir für die Komponente eine weitere Schnittstelle geschaffen – in diesem Fall eine Styling-Schnittstelle.

Update: Andererseits können aber auch einzelne DOM-Knoten ohne CSS-Properties zum expliziten Styling freigegeben werden. Eine Anleitung zum Freigeben von DOM-Knoten aus dem Shadow-DOM zum CSS-Styling bei CSS-Tricks zeigt die notwendigen Anpassungen im HTML:

<div part="style-me">…</div>

…und dem CSS im Eltern-Dokument:

horizontal-situation-indicator::part(style-me) {
  font-weight: bold;
}

Auch hier hat wieder der Autor der Komponente die Herrschaft über die Elemente, die er nach außen freigibt – wie bei einer Schnittstelle.

Wie animiere ich SVGs in Web Components?

Der eigentliche Clou der HSI Web Component ist die generelle Fähigkeit von JavaScript, DOM-Elemente und ihre Eigenschaften zu verändern. Dies können sowohl CSS-Eigenschaften als auch generelle Attribute von DOM-Elementen sein.

Bei SVG bieten sich die folgenden Operationen an:

  • Änderung von stroke und stroke-width zur Beeinflussung von Linien
  • Änderung von fill zur Veränderung der Füllfarbe
  • Änderung von opacity zur Veränderung der Durchsichtigkeit eines Elements
  • Veränderung von Position, Größe und Rotation mittels transform
  • Austausch des Text-Inhalts von <text>-Knoten mittels .textContent

Bei SVG gibt es dabei die Möglichkeit, nicht nur via CSS diese Eigenschaften zu beeinflussen, sondern auch durch das Setzen von Attributen innerhalb des SVGs an einzelnen SVG-DOM-Knoten.

Auch das ist in der Beispiel-Implementation von <horizontal-situation-indicator> zu bestaunen – hier sind die Attribute der Komponente mit Animationsmethoden verknüpft, so dass Änderungen an den Attributen bzw. Properties der Web Component zeitgleich die Darstellung des eingeschlossenen SVGs ändert.

Wie sollte eine Dokumentation für eine Web Compontent aussehen?

Da eine Web Component im Endeffekt eine Schnittstelle ist, muss es dazu eine Schnittstellen-Dokumentation geben. Ohne diese Dokumentation können andere Entwickler, die die Komponente verwenden möchten, nicht zuverlässig wissen, wie die Komponente zu bedienen ist.

Als Minimum muss eine Dokumentation enthalten:

## Properties

| Name           | Type    | Default | Description  |
| -------------- | ------- | ------- | ------------ |
| `heading`      | `float` | `null`  | Lorem ipsum… |

## Methods

| Name           | Parameters | Description         |
| -------------- | ---------- | ------------------- |
| `heading`      | none       | Lorem ipsum…        |

## Events

| Name           | Description                      |
| -------------- | -------------------------------- |
| `synchronized` | Lorem ipsum…                     |

## Styling

```css
component-name {
  --background: color; /* Lorem ipsum… */
}

component-name::part(part-name) {} /* Lorem ipsum… */
```

Fazit

Der fertige Horizontal Situation Indicator als Web Component ist in einem GitHub-Repository gelandet, und einen Blick auf die fertige Implementation der HSI-Web-Component erlaubt einen interaktiven Blick auf die Zusammenhänge in der Komponente.