Eigene Custom Elements genauso formularfaehig machen wie ein natives input-Element
Ohne ElementInternals konnte ein selbst gebautes Custom Element nie wirklich an einem form-Element teilnehmen: kein automatischer Eintrag in FormData, keine native Validierung, kein :invalid-Styling. Die ElementInternals API schliesst diese Luecke ueber attachInternals(), setFormValue() und setValidity() und macht aus einem gewoehnlichen Custom Element ein vollwertiges Formular-Steuerelement.
Inhaltsverzeichnis
- 1. Das Formular-Problem klassischer Custom Elements
- 2. attachInternals(): Die Bruecke zwischen Custom Element und Formular
- 3. setFormValue(): Den uebermittelten Wert festlegen
- 4. setValidity(): Eigene Validierungsregeln in die Constraint Validation API einklinken
- 5. Eigene CSS-Zustaende mit internals.states und der :state()-Pseudoklasse
- 6. Barrierefreiheit: ARIA-Reflection ueber ElementInternals
- 7. Reset, Disabled-Vererbung und weitere Formular-Callbacks
- 8. Vollstaendiges Beispiel: eine formularfaehige Sternebewertung
- 9. Grenzen der API und Vergleich mit nativen Formular-Elementen
- 10. Zusammenfassung
- 11. FAQ
1. Das Formular-Problem klassischer Custom Elements
Ein Custom Element ohne besondere Vorkehrungen ist fuer ein umgebendes form-Element schlicht unsichtbar. Sein Wert landet nicht automatisch in einem FormData-Objekt, es nimmt nicht an der nativen Constraint-Validation teil, und CSS-Pseudoklassen wie :invalid oder :required greifen nicht, weil der Browser das Element schlicht nicht als Formular-Steuerelement kennt, egal wie sehr es optisch wie ein input aussieht.
Die frueher uebliche Loesung bestand darin, innerhalb des Custom Elements ein verstecktes natives input-Element zu erzeugen und dessen value synchron zu halten, ein fehleranfaelliger Workaround mit doppelter Zustandshaltung und zusaetzlichem DOM-Gewicht. Die ElementInternals API ersetzt diesen Trick durch eine direkte, native Anbindung des Custom Elements selbst an die Formular-Infrastruktur des Browsers.
2. attachInternals(): Die Bruecke zwischen Custom Element und Formular
Der erste Schritt ist die statische Eigenschaft static formAssociated = true auf der Element-Klasse, mit der ein Custom Element dem Browser signalisiert, dass es an Formularen teilnehmen moechte. Erst danach liefert this.attachInternals() im Konstruktor ein ElementInternals-Objekt zurueck, das als zentrale Schnittstelle fuer alle folgenden Formular-Operationen dient und typischerweise einmalig in einer privaten Instanzvariable gespeichert wird.
attachInternals() darf pro Element nur einmal aufgerufen werden, ein zweiter Aufruf wirft einen Fehler. Ab diesem Zeitpunkt erkennt der Browser das Element automatisch als Bestandteil des umschliessenden form-Elements, einschliesslich der Aufnahme in die form.elements-Sammlung, ganz ohne dass ein zusaetzliches natives Element im DOM noetig waere.
class BewertungsFeld extends HTMLElement {
static formAssociated = true;
#internals;
constructor() {
super();
this.#internals = this.attachInternals(); // nur einmal pro Element erlaubt
}
}
customElements.define('bewertungs-feld', BewertungsFeld);
3. setFormValue(): Den uebermittelten Wert festlegen
Die Methode internals.setFormValue(value) legt fest, welcher Wert beim Absenden des Formulars unter dem name-Attribut des Custom Elements in ein FormData-Objekt eingetragen wird. Der Parameter kann ein einfacher String, ein File-Objekt oder ein ganzes FormData-Objekt sein, was auch komplexere Steuerelemente wie einen Datei-Upload oder ein Mehrfachauswahl-Element erlaubt, ohne mehrere name-Attribute jonglieren zu muessen.
setFormValue() akzeptiert optional einen zweiten Parameter state, der vom uebermittelten Wert getrennt gespeichert wird und beim Formular-Reset ueber formStateRestoreCallback wiederhergestellt werden kann. Das ist etwa bei einem Steuerelement nuetzlich, dessen sichtbarer Anzeige-Zustand von der eigentlichen Datenrepraesentation abweicht, zum Beispiel bei einer Sternebewertung, die intern sowohl die Zahl als auch die Zwischenzustaende beim Hovern verwaltet.
set value(neuerWert) {
this.#wert = neuerWert;
// Wert wird beim Absenden des form-Elements uebermittelt
this.#internals.setFormValue(String(neuerWert));
}
get value() {
return this.#wert;
}
4. setValidity(): Eigene Validierungsregeln in die Constraint Validation API einklinken
internals.setValidity(flags, message, anchor) registriert das Custom Element bei der nativen Constraint Validation API des Browsers, mit denselben Zustands-Flags, die auch natives input verwendet, etwa valueMissing, tooShort oder eine eigene customError. Sobald ein Flag gesetzt ist, gilt das Element als ungueltig, form.reportValidity() zeigt automatisch die uebergebene message als Sprechblase an der uebergebenen anchor-Position an.
Ein Aufruf von internals.setValidity({}) ohne Flags markiert das Element wieder als gueltig. Weil diese Validierung Teil derselben API ist, die auch native Elemente nutzen, greifen CSS-Selektoren wie :invalid, :valid oder :user-invalid auf dem Custom Element genauso zuverlaessig wie auf einem regulaeren input-Element, ganz ohne manuelle Klassenzuweisung im JavaScript.
#validieren() {
if (this.hasAttribute('required') && !this.#wert) {
this.#internals.setValidity(
{ valueMissing: true },
'Bitte eine Bewertung auswaehlen.',
this.#stern1 // Anker-Element fuer die Sprechblase
);
return;
}
this.#internals.setValidity({}); // wieder gueltig
}
5. Eigene CSS-Zustaende mit internals.states und der :state()-Pseudoklasse
Ueber internals.states, ein CustomStateSet, kann ein Custom Element eigene, benutzerdefinierte Zustaende definieren, etwa checked oder pressed, die von aussen per CSS ueber die Pseudoklasse :state(checked) angesprochen werden koennen, ganz ohne dass dafuer eine class oder ein data-Attribut noetig waere.
Der Vorteil gegenueber einem klassischen CSS-Klassennamen ist die echte Kapselung: Ein per internals.states.add('checked') gesetzter Zustand ist von aussen ausschliesslich ueber die :state()-Selektor-Syntax lesbar, niemals ueber classList oder getAttribute, was verhindert, dass externer Code den internen Darstellungszustand versehentlich direkt manipuliert, statt die oeffentliche API des Elements zu nutzen.
// Zustand setzen, sobald der Nutzer eine Bewertung waehlt
this.#internals.states.add('checked');
this.#internals.states.delete('checked');
/* Passendes CSS ausserhalb der Komponente:
bewertungs-feld:state(checked) .stern { color: #ca8a04; }
*/
6. Barrierefreiheit: ARIA-Reflection ueber ElementInternals
ElementInternals stellt eine ganze Reihe von ARIA-Reflection-Eigenschaften bereit, etwa internals.role, internals.ariaLabel oder internals.ariaRequired, mit denen ein Custom Element seine semantische Rolle festlegen kann, ohne dass Entwickler, die das Element einsetzen, manuell passende aria-Attribute im Markup ergaenzen muessten.
Das ist besonders wertvoll, weil es die Verantwortung fuer korrekte Accessibility-Semantik dort verankert, wo sie hingehoert, naemlich in der Implementierung der Komponente selbst, statt sie jedem einzelnen Verwendungsort aufzuerlegen. Ein Screenreader erkennt das Element dadurch zuverlaessig als das, was es tatsaechlich ist, etwa als radiogroup oder als slider, unabhaengig vom gewaehlten HTML-Tag-Namen.
7. Reset, Disabled-Vererbung und weitere Formular-Callbacks
Neben setFormValue und setValidity definiert die formularassoziierte Custom-Element-Spezifikation eine Reihe von Lifecycle-Callbacks: formResetCallback wird aufgerufen, wenn das umschliessende Formular zurueckgesetzt wird, formDisabledCallback informiert das Element, wenn ein umschliessendes fieldset per disabled deaktiviert wird, und formStateRestoreCallback stellt bei Browser-Navigation gespeicherten Zustand wieder her.
Diese Callbacks sorgen dafuer, dass ein formularassoziiertes Custom Element sich in Randfaellen wie Formular-Reset oder verschachtelten disabled-fieldsets exakt so verhaelt wie ein natives Element, ohne dass die Komponente selbst auf Events wie reset am umschliessenden form lauschen muesste, was fehleranfaellig waere, sobald das Element aus dem DOM entfernt und neu eingefuegt wird.
8. Vollstaendiges Beispiel: eine formularfaehige Sternebewertung
Eine Sternebewertung eignet sich gut als durchgaengiges Beispiel, weil sie sowohl einen numerischen Wert uebermitteln als auch eine Pflichtfeld-Validierung unterstuetzen soll. Die Klasse kombiniert formAssociated, attachInternals im Konstruktor, setFormValue bei jeder Bewertungsaenderung und setValidity, um eine leere Bewertung als valueMissing zu markieren, sobald das required-Attribut gesetzt ist.
Zusaetzlich nutzt das Beispiel internals.states, um den Zustand hover waehrend des Mauszeigers ueber einem Stern zu setzen, sodass CSS allein per :state(hover) die Vorschau-Sterne einfaerben kann, ganz ohne zusaetzliche Klassenmanipulation im Event-Handler. Das Ergebnis verhaelt sich in jeder Hinsicht wie ein natives Formular-Steuerelement.
class SternBewertung extends HTMLElement {
static formAssociated = true;
#internals;
#wert = 0;
constructor() {
super();
this.#internals = this.attachInternals();
this.attachShadow({ mode: 'open' });
}
setStern(zahl) {
this.#wert = zahl;
this.#internals.setFormValue(String(zahl));
this.#pruefeValiditaet();
}
#pruefeValiditaet() {
if (this.hasAttribute('required') && this.#wert === 0) {
this.#internals.setValidity({ valueMissing: true }, 'Bitte bewerten.');
} else {
this.#internals.setValidity({});
}
}
}
customElements.define('stern-bewertung', SternBewertung);
9. Grenzen der API und Vergleich mit nativen Formular-Elementen
ElementInternals macht ein Custom Element formularfaehig, ersetzt aber nicht automatisch jede Tastatur-Interaktion, die native Elemente kostenlos mitbringen, etwa das Umschalten einer Checkbox per Leertaste. Solche Interaktionen muessen weiterhin explizit per Event-Listener implementiert werden, die API kuemmert sich ausschliesslich um die Formular-Anbindung, nicht um Tastatur-Semantik.
In aelteren Browsern ohne Unterstuetzung fuer ElementInternals faellt ein solches Custom Element vollstaendig aus der Formular-Uebermittlung heraus, eine Feature-Detection ueber 'attachInternals' in HTMLElement.prototype vor dem produktiven Einsatz ist deshalb empfehlenswert, mit einem versteckten nativen input als Fallback fuer den seltenen Fall fehlender Unterstuetzung.
| Merkmal | Natives input | Custom Element + ElementInternals | Custom Element ohne Internals |
|---|---|---|---|
| Eintrag in FormData | Automatisch | Automatisch ueber setFormValue | Fehlt komplett |
| Native Validierung | Eingebaut | Ueber setValidity nachgebildet | Nicht vorhanden |
| CSS-Pseudoklassen :invalid/:required | Funktionieren | Funktionieren ueber setValidity | Funktionieren nicht |
| Aufnahme in form.elements | Automatisch | Automatisch nach attachInternals | Fehlt komplett |
Mironsoft
Moderne Browser-APIs, Performance und wartbares JavaScript
JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?
Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.
Code-Review
Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.
Performance-Optimierung
Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.
Modernisierung
Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.
10. Zusammenfassung
ElementInternals API: Das Wichtigste auf einen Blick
Kernidee
attachInternals() verbindet ein Custom Element direkt mit der nativen Formular-Infrastruktur des Browsers.
Wert uebermitteln
setFormValue() legt fest, was beim Absenden unter dem name-Attribut in FormData landet.
Validierung
setValidity() nutzt dieselben Flags wie native Elemente und aktiviert :invalid automatisch.
Wichtige Grenze
Tastatur-Interaktionen wie Leertaste fuer Checkboxen muessen weiterhin selbst implementiert werden.