Erstellung von benutzerdefinierten Formularsteuerelementen mit ElementInternals

Avatar of Caleb Williams
Caleb Williams am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Seit Anbeginn der Zeit träumt die Menschheit davon, mehr Kontrolle über Formularelemente zu haben. OK, ich übertreibe vielleicht ein kleines bisschen, aber das Erstellen oder Anpassen von Formularkomponenten ist seit Jahren ein Heiliger Gral der Front-End-Webentwicklung.

Eines der weniger gefeierten, aber mächtigsten Features von benutzerdefinierten Elementen (z. B. <my-custom-element>) hat sich stillschweigend in Google Chrome ab Version 77 seinen Weg gebahnt und arbeitet sich seinen Weg in andere Browser. Der Standard ElementInternals ist ein sehr spannender Satz von Funktionen mit einem sehr unscheinbaren Namen. Zu den Funktionen, die Internals hinzufügt, gehören die Möglichkeit, an Formularen teilzunehmen, und eine API für Barrierefreiheitssteuerungen.

In diesem Artikel werden wir uns ansehen, wie man ein benutzerdefiniertes Formularsteuerelement erstellt, die Validierung von Einschränkungen integriert, die Grundlagen der internen Barrierefreiheit einführt und einen Weg sieht, diese Funktionen zu kombinieren, um ein hochportable Makro-Formularsteuerelement zu erstellen.

Beginnen wir mit der Erstellung eines sehr einfachen benutzerdefinierten Elements, das unserem Designsystem entspricht. Unser Element wird alle seine Stile im Shadow DOM halten und eine grundlegende Barrierefreiheit sicherstellen. Wir verwenden die wunderbare LitElement Bibliothek des Polymer-Teams bei Google für unsere Codebeispiele und obwohl Sie sie definitiv nicht benötigen, bietet sie eine großartige Abstraktion zum Schreiben von benutzerdefinierten Elementen.

In diesem Pen haben wir ein <rad-input> erstellt, das ein grundlegendes Design aufweist. Wir haben auch ein zweites Eingabefeld zu unserem Formular hinzugefügt, das ein normales HTML-Eingabefeld ist, und einen Standardwert hinzugefügt (damit Sie einfach auf Senden klicken und sehen können, dass es funktioniert).

Wenn wir auf unsere Schaltfläche "Senden" klicken, passieren ein paar Dinge. Erstens wird die Methode preventDefault des Submit-Events aufgerufen, in diesem Fall, um sicherzustellen, dass unsere Seite nicht neu geladen wird. Danach erstellen wir ein FormData-Objekt, das uns Zugriff auf Informationen über unser Formular gibt, das wir verwenden, um einen JSON-String zu erstellen und ihn an ein <output>-Element anzuhängen. Beachten Sie jedoch, dass der einzige Mehrwert, der unserem Output hinzugefügt wird, von dem Element mit name="basic" stammt.

Das liegt daran, dass unser Element noch nicht weiß, wie es mit dem Formular interagieren soll. Lassen Sie uns also unser <rad-input> mit einer ElementInternals-Instanz einrichten, damit es seinem Namen gerecht wird. Zuerst müssen wir die Methode attachInternals unseres Elements im Konstruktor aufrufen. Wir werden auch ein ElementInternals Polyfill in unsere Seite importieren, um mit Browsern zu arbeiten, die die Spezifikation noch nicht unterstützen.

Die Methode attachInternals gibt eine neue Element-Internals-Instanz zurück, die einige neue APIs enthält, die wir in unserer Methode verwenden können. Um unserem Element zu ermöglichen, diese APIs zu nutzen, müssen wir einen statischen formAssociated Getter hinzufügen, der true zurückgibt.

class RadInput extends LitElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
  }
}

Werfen wir einen Blick auf einige der APIs in der internals-Eigenschaft unseres Elements

  • setFormValue(value: string|FormData|File, state?: any): void — Diese Methode setzt den Wert des Elements im übergeordneten Formular, falls vorhanden. Wenn der Wert null ist, nimmt das Element nicht am Formularübermittlungsprozess teil.
  • form — Ein Verweis auf das übergeordnete Formular unseres Elements, falls vorhanden.
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — Die Methode setValidity hilft bei der Steuerung des Gültigkeitszustands unseres Elements innerhalb des Formulars. Wenn das Formular ungültig ist, muss eine Validierungsnachricht vorhanden sein.
  • willValidate — Ist true, wenn das Element bei der Übermittlung des Formulars ausgewertet wird.
  • validity — Ein Gültigkeitsobjekt, das den APIs und Semantiken von HTMLInputElement.prototype.validity entspricht.
  • validationMessage — Wenn die Steuerung mit setValidity als ungültig gesetzt wurde, ist dies die übergebene Nachricht, die den Fehler beschreibt.
  • checkValidity — Gibt true zurück, wenn das Element gültig ist, andernfalls gibt es false zurück und löst ein invalid-Ereignis auf dem Element aus.
  • reportValidity — Tut dasselbe wie checkValidity und meldet Probleme an den Benutzer, wenn das Ereignis nicht abgebrochen wird.
  • labels — Eine Liste von Elementen, die dieses Element mit dem Attribut label[for] beschriften.
  • Eine Reihe weiterer Steuerungen, die zum Setzen von Aria-Informationen auf dem Element verwendet werden.

Setzen des Werts eines benutzerdefinierten Elements

Lassen Sie uns unser <rad-input> anpassen, um einige dieser APIs zu nutzen

Hier haben wir die _onInput-Methode des Elements geändert, um einen Aufruf von this.internals.setFormValue einzufügen. Dies teilt dem Formular mit, dass unser Element einen Wert im Formular unter seinem angegebenen Namen registrieren möchte (der als Attribut in unserem HTML festgelegt ist). Wir haben auch eine firstUpdated-Methode hinzugefügt (lose analog zu connectedCallback, wenn kein LitElement verwendet wird), die den Wert des Elements auf einen leeren String setzt, wann immer das Element mit dem Rendering fertig ist. Dies stellt sicher, dass unser Element immer einen Wert für das Formular hat (und obwohl es nicht notwendig ist, möchten Sie Ihr Element vielleicht vom Formular ausschließen, indem Sie einen null-Wert übergeben).

Wenn wir nun einen Wert in unser Eingabefeld eingeben und das Formular absenden, sehen wir, dass wir einen radInput-Wert in unserem <output>-Element haben. Wir können auch sehen, dass unser Element der radInput-Eigenschaft des HTMLFormElement hinzugefügt wurde. Eine Sache, die Ihnen vielleicht aufgefallen ist, ist jedoch, dass trotz der Tatsache, dass unser Element keinen Wert hat, die Formularübermittlung trotzdem stattfindet. Lassen Sie uns als Nächstes eine Validierung zu unserem Element hinzufügen.

Hinzufügen von Einschränkungsvalidierung

Um die Validierung unseres Feldes einzurichten, müssen wir unser Element ein wenig modifizieren, um die Methode setValidity auf unserem Element-Internals-Objekt zu nutzen. Diese Methode nimmt drei Argumente entgegen (das zweite ist nur erforderlich, wenn das Element ungültig ist, das dritte ist immer optional). Das erste Argument ist ein partielles ValidityState-Objekt. Wenn ein Flag auf true gesetzt ist, wird die Steuerung als ungültig markiert. Wenn eine der integrierten Gültigkeitsschlüssel Ihre Bedürfnisse nicht erfüllt, gibt es einen universellen Schlüssel customError, der funktionieren sollte. Schließlich übergeben wir, wenn die Steuerung gültig ist, ein Objekt-Literal ({}), um die Gültigkeit der Steuerung zurückzusetzen.

Das zweite Argument hier ist die Nachricht zur Gültigkeit der Steuerung. Dieses Argument ist erforderlich, wenn die Steuerung ungültig ist, und ist nicht erlaubt, wenn die Steuerung gültig ist. Das dritte Argument ist ein optionales Validierungsziel, das den Fokus des Benutzers steuert, wenn und wann das Formular als ungültig übermittelt wird oder reportValidity aufgerufen wird.

Wir werden unserer <rad-input>-Methode eine neue Methode hinzufügen, die diese Logik für uns übernimmt.

_manageRequired() {
  const { value } = this;
  const input = this.shadowRoot.querySelector('input');
  if (value === '' && this.required) {
    this.internals.setValidity({
      valueMissing: true
    }, 'This field is required', input);
  } else {
    this.internals.setValidity({});
  }
}

Diese Funktion holt den Wert und die Eingabe der Steuerung. Wenn der Wert einer leeren Zeichenfolge entspricht und das Element als erforderlich markiert ist, rufen wir internals.setValidity auf und schalten die Gültigkeit der Steuerung um. Jetzt müssen wir nur noch diese Methode in unseren firstUpdated und _onInput Methoden aufrufen, und wir haben eine grundlegende Validierung zu unserem Element hinzugefügt.

Wenn wir vor der Eingabe eines Wertes in unser <rad-input> auf die Schaltfläche "Senden" klicken, wird in Browsern, die die ElementInternals-Spezifikation unterstützen, nun eine Fehlermeldung angezeigt. **Leider werden Validierungsfehler vom Polyfill noch nicht unterstützt**, da es keine zuverlässige Möglichkeit gibt, den integrierten Validierungspopup in nicht unterstützenden Browsern auszulösen.

Wir haben auch einige grundlegende Barrierefreiheitsinformationen zu unserem Beispiel hinzugefügt, indem wir unser internals-Objekt verwendet haben. Wir haben unserer Element eine zusätzliche Eigenschaft hinzugefügt, _required, die als Proxy für this.required und als Getter/Setter für required dient.

get required() {
  return this._required;
}

set required(isRequired) {
  this._required = isRequired;
  this.internals.ariaRequired = isRequired;
}

Durch die Übergabe der Eigenschaft required an internals.ariaRequired informieren wir Screenreader darüber, dass unser Element derzeit einen Wert erwartet. Im Polyfill geschieht dies durch Hinzufügen eines aria-required-Attributs; in unterstützenden Browsern wird das Attribut jedoch nicht zum Element hinzugefügt, da diese Eigenschaft dem Element inhärent ist.

Erstellung eines Mikroformulars

Nachdem wir nun ein funktionierendes Eingabefeld haben, das unserem Designsystem entspricht, möchten wir vielleicht beginnen, unsere Elemente zu Mustern zu komponieren, die wir in verschiedenen Anwendungen wiederverwenden können. Eines der überzeugendsten Features von ElementInternals ist, dass die Methode setFormValue nicht nur String- und Dateidaten, sondern auch FormData-Objekte annehmen kann. Nehmen wir also an, wir möchten ein gemeinsames Adressformular erstellen, das in mehreren Organisationen verwendet werden könnte, das können wir mit unseren neu erstellten Elementen problemlos tun.

In diesem Beispiel haben wir ein Formular, das innerhalb des Shadow-Roots unseres Elements erstellt wurde, in dem wir vier <rad-input>-Elemente zu einem Adressformular komponiert haben. Anstatt setFormValue mit einem String aufzurufen, haben wir diesmal den gesamten Wert unseres Formulars übergeben. Infolgedessen gibt unser Element die Werte jedes einzelnen Elements innerhalb seines untergeordneten Formulars an das äußere Formular weiter.


Das Hinzufügen von Einschränkungsvalidierung zu diesem Formular wäre ein ziemlich unkomplizierter Prozess, ebenso wie die Bereitstellung zusätzlicher Stile, Verhaltensweisen und das Einbinden von Inhalten. Die Verwendung dieser neueren APIs ermöglicht es Entwicklern endlich, eine Menge Potenzial in benutzerdefinierten Elementen freizusetzen und gibt uns endlich freie Hand bei der Kontrolle unserer Benutzererlebnisse.