Web Components/custom elements

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
Web Components Logo

Web Components ermöglichen es, eigene benutzerdefinierte Elemente (englisch: custom elements) zu erstellen. Dies können neue HTML-Tags mit besonderen Aufgaben oder die Erweiterung bestehender Tags sein.[1][2][3]

Gegenüber externen Plugins haben custom elements den Vorteil, dass sie …

  • als HTML-Elemente ein einheitliches Markup besitzen,
  • miteinander kombinier- und schachtelbar sind sowie
  • ihren inneren Aufbau (HTML, CSS, JS) kapseln und so Konflikte mit anderen Plugins oder Scripts auf der Webseite verhindern.

Dieses Tutorial zeigt erst, wie custom elements semantischeres HTML ermöglichen und führt dann zu Widgets, die mehr Fähigkeiten als vorhandene native HTML-Elemente haben.


Regeln

Benutzerdefinierte Elemente (englisch: custom elements) folgen einigen Regeln, damit Parser sie von regulären HTML-Elementen unterscheiden können:

  • Namen von benutzerdefinierten Elementen müssen immer …
    • mit einem Kleinbuchstaben beginnen
    • einen Bindestrich enthalten.
      Beispiele: <x-line>, <mein-element>, <self-toggle>
      Dies sichert auch eine Vorwärts-Kompatibilität eventueller neuer Tags.
      (Google trackt die Verwendung von custom elements und plant, „erfolgreiche“ Elemente in den Standard zu übernehmen.)
  • Ein Tag kann nur einmal registriert werden, sonst gibt es einen DOMException-Fehler
  • Benutzerdefinierte Elemente müssen immer mit einem schließenden Tag geschlossen werden.
    Beispiel: <x-line>…</x-line>

Gedicht mit Zeilen

benutzerdefiniertes Zeilen-Element ansehen …
<blockquote>
	<p>
		<x-line>Someday girl I don't know when we're gonna get to that place</x-line>
		<x-line>Where we really want to go and we'll walk in the sun</x-line>
		<x-line>But till then tramps like us baby we were born to run</x-line>
	</p>
	<footer><cite>Bruce Springsteen, “Born to Run”</cite></footer>
</blockquote>

Anstelle von hier semantisch nicht passenden Zeilenumbrüchen (br) sind die Zeilen in einem x-line-Element notiert.

Beispiel
x-line {
	display: block;
	--indent: 0.8em;
	padding-left: var(--indent);
	text-indent: calc(-1 * var(--indent));
}

Diese erhalten mit CSS ein display:block und werden so untereinander dargestellt.[4]

Ein custom element hat per se dieselbe Semantik wie ein span oder div – nämlich gar keine. Jedenfalls für eine Maschine (Browser); für menschliche Code-Leser ist x-line aussagekräftiger sein als span .

Ein [weiterer] Grund für das custom element ist Faulheit: <x-line> ist kürzer als <span class="line">. Was auch der Lesbarkeit des Codes zugutekommt.

Ein anderer Grund ist, dass AFAIK Google die Häufigkeit der Verwendung von Elementtypen (also auch von custom elements) trackt und damit Zahlen in der Hand hat, die für eine mögliche Einführung neuer Elementtypen in den HTML-Standard sprechen könnten. Vielleicht kommt ja doch noch in HTML irgendwann ein l-Element wie es in XHTML 2 vorgesehen war.

SELF-Forum Semantische Verwendung von br von Gunnar Bittersmann am 21.09.2018

Listen im Tabellen-Look

Im Forum wurde gefragt, wie man Listenelemente mit Tabulatoren formatieren könnte. Mit custom elements können die einzelnen Teile der Adresse erkannt und passend formatiert werden:[5]

benutzerdefinierte Elemente für Adresselemente ansehen …
<ul>
	<li>
		<x-postal-code>40233</x-postal-code>
		<x-locality>Düsseldorf</x-locality>
		<x-location>Capitol-Theater</x-location>
	</li>
	<li>
		<x-postal-code>10117</x-postal-code>
		<x-locality>Berlin</x-locality>
		<x-location>Admiralspalast</x-location>
	</li>
</ul>
ul {
	display: table;
	padding-left: 0;
	font: 1.5em Candara;
}

li {
	display: table-row;
}

li > * {
	display: table-cell;
	padding: 0 0.5em;
}

li > :first-child::before {
	content: '•';
	margin-right: 0.5em;
}

x-postal-code {
	font-feature-settings: "tnum", "lnum";	
}

Die Ziffern sollten bei den PLZ gleichbreit sein (tabular figures "tnum"). Zur Verdeutlichung wurde im Beispiel die Microsoft-Schriftart Candara gewählt. Ohne "tnum" in der font-feature-settings-Angabe wären die Ziffern unterschiedlich breit. (Und ohne "lnum" wären es Mediävalziffern.)

Die Custom elements mit einem - im Bezeichner verwendeten sprechende Namen wie <x-postal-code> und <x-locality>. Allerdings gibt es hier keinen Vorteil gegenüber <span class="postal-code"> bzw. <span class="locality">.

Empfehlung: Besser wäre eine semantische Auszeichnung mit RDFa, die ohne Klassen auskommt: <span property="postalCode"> und <span property="addressLocality">.

Customized Elements - bestehende Elemente erweitern

Die beiden oberen Beispiele erweiterten unser Repertoire an HTML-Elementen um „semantischere“ Auszeichnungen, die dann mit CSS entsprechend gestylt werden können.

Ihre volle Macht entfalten solche Komponenten aber erst im Zusammenhang mit Javascript, wenn jetzt interaktive Widgets gebaut werden können, die Standardverhalten von HTML-Elementen verwenden und durch eigenes JavaScript erweitert werden.

Ein Weg wäre es, bestehende Elemente um neue Fähigkeiten erweitern, so z. B. ein p-Element um eine Eigenschaft, die die Anzahl der Wörter zählt oder ein Shadow DOM an das Element hängt.[6]

 customElements.define('word-count', WordCount, { extends: 'p' });

Ein solches Element hätte dann ein is-Attribut, welches das Custom Element referenziert.

<p is="word-count"></p>
Beachte: Safari hat klar erklärt, dies nicht zu implementieren. Deshalb sind solche Beispiele nur proof-of-concept. Mittlerweile werden andere Wege gesucht.

neue Elemente

Um einem benutzerdefinierten Element eine Funktion zu geben, muss diese Funktion in einer Klasse definiert werden:

HTML:
<my-element></my-element>
Javascript:
class myElement extends HTMLElement {
 ...
}
customElements.define('my-element', myElement);

Diese Klasse muss einen Konstruktor haben, sowie bedarfsabhängig den Getter

  • observedAttributes: legt fest, auf welche Attributänderungen reagiert werden soll,

und die Methoden

  • connectedCallback: Wird aufgerufen, wenn das Element zu ersten Mal erstellt wird.
  • disconnectedCallback: Wird aufgerufen, wenn das Element entfernt wird.
  • adoptedCallback: Wird aufgerufen, wenn das Element in ein anderes Dokument geschoben wird.
  • attributeChangedCallback: Wird aufgerufen, wenn ein Element-Attribut hinzugefügt, geändert oder entfernt wird. Übergabeparameter sind der Attributname, der Wert vor und der Wert nach der Änderung. Beim ersten Aufruf hat oldValue den Wert null.

Das Grundgerüst für ein benutzerdefiniertes Element kann dann so aussehen:

Javascript:
// Custom-Element my-element anlegen
class myElement extends HTMLElement {
	
	// Festlegen, welche Attribute überwacht werden sollen
	static get observedAttributes() {
		return ['attribut1', 'attribut2'];
	}
	
	constructor() {
		// Element wird anlgelegt
		
		// super muss als erstes im constructor aufgerufen werden, super ruft den construcor der Elternklasse auf
		super(); 
		
		// Schatten-Dom anlegen
			// mode: 'open' : Vom Dokument aus ist der Zugriff auf das Schatten-Dom möglich.
			// mode: 'closed' : Der Zugriff auf das Schatten-Dom ist nicht möglich.
		const shadow = this.attachShadow({mode: 'open'});

		// Element für Inhalt anlegen und ins Schatten-Dom einhängen
		const contend = document.createElement('div');
		contend.className = "contend";
		shadow.appendChild(contend);
		
		// CSS anlegen und ins Schatten-Dom einhängen
		// :host selektiert das Custom Element
		const style = document.createElement('style');
		style.textContent = `
			:host { … }	
			.contend { … }
		`;
		shadow.appendChild(style);
		
		// Weiterer Code
	}

	connectedCallback() {
		// Element wurde ins DOM eingehängt
	}
	
	disconnectedCallback () {
		// Element wurde entfernt
	}
	
	adoptedCallback() {
		// Element ist in ein anderes Dokument umgezogen
	}

	attributeChangedCallback(name, oldValue, newValue) {
		// Elementparameter wurden geändert
		// Achtung attributeChangedCallback wird vor connectedCallback aufgerufen
	}

}
customElements.define('my-element', myElement);

toggle-switch mit Custom Elements

Bestehende HTML-Elemente zu erweitern ist schwierig, neue HTML-Elemente mit eigener Semantik und Verhalten sind jedoch einfach und funktionieren in allen modernen Browsern:

Es gibt viele Anwendungsfälle, bei denen man etwas ein- oder ausschalten möchte, z.B den Dark Mode einer Webseite, eine mute-Funktion bei audio, oder das Einschalten einer Systemeinstellung. Dabei gibt es einen klaren Defaultwert und die Möglichkeit ihn auszuschalten.

Da es (noch) kein passendes natives HTML-Element[7] dafür gibt, wurde so etwas früher mit einer Checkbox oder einem Button realisiert. Der Unterschied zwischen einem Switch und einer Checkbox ist aber, dass man nicht unbedingt ein Formular mit einem submit benötigt, wenn man etwas einschalten will. Beim Absenden eines Formulars würde der ausgewählte Zustand der Checkbox mitübertragen werden.

Das Interessante ist, dass ARIA (Accessible Rich Internet Applications) einen Switch schon vorsieht[8], es aber noch kein natives Switch-Element in HTML gibt. Einen solchen Kippschalter, einen <toggle-switch> wollen wir nun zusammenbauen:


HTML: Markup unseres toggle-Switches ansehen …
<label for="style-changer" id="toggle-label">
	Halloween Style einschalten
	<toggle-switch id="style-changer"></toggle-switch>
</label>

Unser <toggle-switch>-Element soll dem Nutzer ermöglichen, etwas an- oder auszuschalten. Diese Änderung soll dann ohne weitere Bestätigung (submit; Sind sie sicher?) sofort erfolgen.

Implementierung

Um einem benutzerdefinierten Element eine Funktion zu geben, muss diese Funktion in einer Klasse definiert werden:

custom element einrichten
class toggleSwitch extends HTMLElement {
 ...
}
customElements.define('toggle-switch', toggleSwitch);

Element ohne Eigenschaften - Funktionalität durch ARIA

Unser Kippschalter soll ja eben den Schalter (engl. thumb) erhalten, der umgekippt oder gedrückt werden kann. Eigentlich hätte man das ja mit einem <button is="switch" aria-pressed="false"> realisieren können, wenn der Safari mitspielte. Dieser Button hätte mit seinem Standardverhalten bereits die Funktionalität und auch die Zugänglichkeit mitgebracht.

Dies trifft auf unser neues Element noch nicht zu und so müssen wir die Funktionalität nachbauen:

class ToggleSwitch extends HTMLElement {
    css = `
        :host {
            display: block;            
            ...
        }`;
    
    template = () => `<div class="thumb"></div>`;

    constructor() {
        super();
        this.attachShadow({mode: "open"});
        this.render();
        this.setAttribute('role', 'switch'); // Set ARIA role to 'switch'
        this.setAttribute('aria-checked', 'false'); // Initial ARIA state
        this.setAttribute('tabindex', '0'); // Make element focusable
    }

    render() {
        this.shadowRoot.innerHTML = `
        <style>${this.css.trim()}</style>
        ${this.template().trim()}
        `;
        this.addEventListener('click', () => {
            this.changeToggle();
        });
        this.addEventListener('keydown', (event) => {
            if (event.key === ' ' || event.key === 'Enter') {
                event.preventDefault(); // Prevent scrolling when space is pressed
                this.changeToggle();
            }
        });
    }

Der Switch erhält nun, damit er wie ein Formularelement mit der Tastatur antabbar ist, einen tabindex.

Damit Screenreader seine Funktion erkennen, kommt ein role="switch" hinzu.[9] Diese landmark role ist funktional identisch mit role="checkbox", wobei anstelle der Zustände checked und unchecked die allgemeineren Zustände on und off repräsentiert werden. Der Zustand wird über das aria-checked-Attribut mit den booleschen Werten true und false notiert.

Beachte: Es ist wichtig, dass sich das label eines Schalters nicht ändert, wenn sich sein Zustand ändert.

CSS macht den Button zum Schalter

Bis jetzt haben wir nur ein Element, das nach nichts aussieht. Für den Schalter benötigen wir einen Knopf (engl. thumb) und einen Schiebeweg (engl. track).

	<toggle-switch id="style-changer" role="switch" aria-checked="false" tabindex="0">
		#shadow-root
		<style></style>
		<div class="thumb"></div>
	</toggle-switch>

Unser custom element kann Kindelemente enthalten. Allerdings sollte man beachten, dass manche Elemente trotz vermeintlich passendem Namen bereits andere Rollen haben. So ist ein track-Element ein inhaltsleeres Kindelement von audio und video, ein thumb-Element müsste als neues custom element registriert werden. So verwenden auch die Browser (z.B. für den type = "range"-Schieberegler) im Shadow DOM div- und Pseudo-Elemente, die dann mit CSS gestaltet werden.


class ToggleSwitch extends HTMLElement {
    css = `
        :host {
            display: block;            
            ...
        }`;
    
    template = () => `<div class="thumb"></div>`;

    render() {
        this.shadowRoot.innerHTML = `
        <style>${this.css.trim()}</style>
        ${this.template().trim()}
        `;
        this.addEventListener('click', () => {
            this.changeToggle();
        });
        this.addEventListener('keydown', (event) => {
            if (event.key === ' ' || event.key === 'Enter') {
                event.preventDefault(); // Prevent scrolling when space is pressed
                this.changeToggle();
            }
        });
    }

Dann werden die CSS-Styles und ein template mit einem div-Element ins Shadow DOM geladen. Dort sind sie gekapselt und vor Zugriffen von außen geschützt.

<label for="style-changer" id="toggle-label">zu Halloween Style umschalten
	<toggle-switch id="style-changer" role="switch" aria-checked="false" tabindex="0">
		#shadow-root
		<style></style>
		<div class="thumb"></div>
	</toggle-switch>
</label>

CSS sorgt für das Aussehen

Eigentlich müsste man erwarten, dass unser toggle-Button nun mit JavaScript ausgelesen wird. In diesem Fall kann das allein durch CSS erfolgen:

body:has(#halloween-style:state(checked)) {
  background: black;
	color: white;
	
	h1,h2,h3 {
		color: orange;
		border-bottom: medium solid;
		width: max-content;
	}
	img {
		opacity: 1;
	}
}

Mit dem has()-Selektor wird überprüft, ob #halloween-style:state(checked) zutrifft. Falls ja, erhalten background und color andere Farbwerte und die Kindelemente h1,h2,h3 und img andere Regelsätze.

Ergebnis: style-changer

Hier findet sich unser toggle-Button nun im LiveBeispiel:

<toggle-switch> als Stylechanger ansehen …
<label for="style-changer" id="toggle-label">Halloween Style einschalten
	<toggle-switch id="style-changer"></toggle-switch>
</label>

Dieses Beispiel dient nur zur Demonstration. Natürlich müsste man in einem Produktionseinsatz die Benutzerwünsche mit prefers-color-scheme abfragen und bereits getroffene Einstellungen mit localStorage speichern. Wie so etwas umgesetzt wird, zeigen wir in unserem Dark Mode-Tutorial.

Siehe auch:

  • Dark Mode

    Lass' die Nutzer entscheiden:

    Vorschau-01-groß.png

Interessant wäre auch ein tristate-Schalter.


Siehe auch

In Tutorials wimmelt es von Beispielen, die nur das Prinzip veranschaulichen, aber keinen Nutzen bringen, wie das <x-treehouse>-Element im ebenso benannten Blog oder der <hello-greeter> im Self-Blog von 2014[10].

Die <howto-checkbox> bringt sogar Nachteile mit sich: das Standardverhalten der Tastaturbedienung muss erst noch nachgebaut werden, innerhalb von form-Elementen sind keine custom elements erlaubt, sodass Eingaben über ein versteckes input-Element importiert werden müssen. (Dies wäre heute nicht mehr nötig: form-Elemente dürfen custom elements enthalten!)

Trotzdem gibt es einige Beispiele, die einen zweiten Blick lohnen:

  • <osm-map>

    Einbinden einer OSM-Karte als Beispiel für Custom Elements
    Self-Blog-Artikel von Jürgen Berkemeier

  • <self-slider>

    Custom-Element mit synchronisierten Range- und Number-Inputs

  • <progress-circle>
    Kreisdiagramme mit custom elements


Weblinks

  1. WHATWG: Custom elements
  2. MDN: Using Custom Elements
  3. Custom Elements v1: Reusable Web Components (dev.to)
  4. SELF-Forum: semantische Verwendung von br (und doch nichts für Screenreader) von Gunnar Bittersmann am 21.09.2018
  5. SELF-Forum: Dürfen HTML-Tags (z. B. Tabulatoren) erfunden werden? von Gunnar Bittersmann am 30.03.2019
  6. Customized built-in elements (MDN)
  7. Die open-ui.org entwickelt Vorschläge für ein Switch-Element, wobei noch nicht klar ist, welchen Weg man dort gehen wird.
    Bei Popover und dem „neuen“ select-Element ging eine Implementierung sehr schnell, sobald der entsprechende Vorschlag bei der WhatWG gelandet ist.
  8. W3C: Switch Pattern (ARIA Authoring Practices Guide (APG) Home)
  9. [MDN: ARIA: switch role
  10. Web Components – eine Einführung von 1UnitedPower vom 09.12.2014