Start · Sprachen · JavaScript · Referenz · HTMLElement.attachInternals()

HTMLElement.attachInternals()

Methode

Gibt ein <code>ElementInternals</code>-Objekt zurück, mit dem ein autonomes Custom Element an Formularen teilnehmen, eigene Validierung und ARIA-Semantik festlegen kann.

seit JavaScript Baseline 2023: Chrome 77, Edge 79, Firefox 93, Saf Kategorie: webapi

Signatur

HTMLElement.prototype.attachInternals()

Beschreibung

attachInternals() ist der Einstiegspunkt, über den ein autonomes Custom Element (also eine Klasse, die direkt von HTMLElement erbt) Zugriff auf Browser-Funktionen erhält, die sonst nur eingebauten Elementen vorbehalten sind. Das zurückgegebene ElementInternals-Objekt bietet unter anderem setFormValue(), setValidity(), checkValidity(), reportValidity(), form, labels, states (benutzerdefinierte Pseudoklassen wie :state(checked)) sowie ARIA-Eigenschaften wie role und ariaLabel.

Damit ein Element als formularzugehöriges Custom Element gilt (Wert wird mit dem Formular übermittelt, <label> funktioniert, disabled/reset-Callbacks werden aufgerufen), muss die Klasse zusätzlich die statische Eigenschaft static formAssociated = true besitzen. Ohne dieses Flag funktionieren zwar die ARIA-Eigenschaften und states, die formularbezogenen Methoden werfen aber einen NotSupportedError.

Die Methode wird typischerweise einmalig im Konstruktor aufgerufen; das Ergebnis speichert man in einem privaten Feld (z. B. #internals), damit Außenstehende die Formular- oder Barrierefreiheits-Semantik nicht manipulieren können.

Rückgabewert

Typ
ElementInternals
Beschreibung
Ein neues ElementInternals-Objekt, das fest an das aufrufende Element gebunden ist.

Beispiele

Formularzugehöriges Eingabeelement mit Validierung

class RatingInput extends HTMLElement {
  static formAssociated = true;
  #internals;
  #value = 0;

  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.attachShadow({ mode: 'open' }).innerHTML =
      '<button type="button">★ vergeben</button>';
    this.shadowRoot.querySelector('button')
      .addEventListener('click', () => { this.value = this.#value % 5 + 1; });
    this.value = 0;
  }

  get value() { return this.#value; }
  set value(v) {
    this.#value = Number(v);
    this.#internals.setFormValue(String(this.#value));
    if (this.#value === 0) {
      this.#internals.setValidity({ valueMissing: true }, 'Bitte eine Bewertung wählen.');
    } else {
      this.#internals.setValidity({});
    }
  }

  get form() { return this.#internals.form; }
  checkValidity() { return this.#internals.checkValidity(); }

  // Wird beim Zurücksetzen des Formulars aufgerufen
  formResetCallback() { this.value = 0; }
}

customElements.define('rating-input', RatingInput);

// HTML: <form><rating-input name="sterne"></rating-input></form>
const form = document.querySelector('form');
const rating = form.querySelector('rating-input');
console.log(rating.checkValidity());
rating.value = 4;
console.log(new FormData(form).get('sterne'));
false 4

ARIA-Rolle und eigener Zustand ohne Formularbezug

class ToggleSwitch extends HTMLElement {
  #internals = this.attachInternals();

  connectedCallback() {
    this.#internals.role = 'switch';
    this.#internals.ariaChecked = 'false';
    this.tabIndex = 0;
    this.addEventListener('click', () => this.toggle());
  }

  toggle() {
    const on = this.#internals.states.has('on');
    on ? this.#internals.states.delete('on') : this.#internals.states.add('on');
    this.#internals.ariaChecked = String(!on);
  }
}

customElements.define('toggle-switch', ToggleSwitch);
// CSS: toggle-switch:state(on) { background: green; }

const sw = document.createElement('toggle-switch');
document.body.append(sw);
sw.click();
console.log(sw.matches(':state(on)'));
true

Fehlerfälle erkennen

class MyEl extends HTMLElement {}
customElements.define('my-el', MyEl);

const el = new MyEl();
const internals = el.attachInternals();

try {
  el.attachInternals(); // zweiter Aufruf
} catch (e) {
  console.log(e.name);
}

try {
  internals.setFormValue('x'); // formAssociated fehlt
} catch (e) {
  console.log(e.name);
}

try {
  document.createElement('div').attachInternals(); // kein Custom Element
} catch (e) {
  console.log(e.name);
}
NotSupportedError NotSupportedError NotSupportedError

// Wichtig · Fallstricke

  • Nur einmal pro Element: Ein zweiter Aufruf wirft einen NotSupportedError (DOMException). Das Objekt daher sofort speichern.
  • Nur für autonome Custom Elements: Bei eingebauten Elementen (div), bei angepassten eingebauten Elementen (is="…"), bei noch nicht definierten Elementen oder wenn die Klasse static disabledFeatures = ['internals'] setzt, wird ebenfalls NotSupportedError geworfen.
  • formAssociated nicht vergessen: Ohne static formAssociated = true werfen setFormValue(), setValidity(), form, labels usw. Fehler.
  • Kapselung: Das Objekt nicht öffentlich exponieren (privates Feld #internals nutzen), sonst kann fremder Code Validität und Formularwert manipulieren.
  • Kompatibilität: Seit Safari 16.4 in allen modernen Browsern verfügbar. states und die CSS-Syntax :state() sind neuer (Chrome 125, Firefox 126, Safari 17.4); ältere Chrome-Versionen nutzten :--name. Für ältere Umgebungen gibt es das Polyfill element-internals-polyfill. Per Feature-Erkennung prüfen: 'attachInternals' in HTMLElement.prototype.