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 Wertnullist, 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 MethodesetValidityhilft bei der Steuerung des Gültigkeitszustands unseres Elements innerhalb des Formulars. Wenn das Formular ungültig ist, muss eine Validierungsnachricht vorhanden sein.willValidate— Isttrue, wenn das Element bei der Übermittlung des Formulars ausgewertet wird.validity— Ein Gültigkeitsobjekt, das den APIs und Semantiken vonHTMLInputElement.prototype.validityentspricht.validationMessage— Wenn die Steuerung mitsetValidityals ungültig gesetzt wurde, ist dies die übergebene Nachricht, die den Fehler beschreibt.checkValidity— Gibttruezurück, wenn das Element gültig ist, andernfalls gibt esfalsezurück und löst eininvalid-Ereignis auf dem Element aus.reportValidity— Tut dasselbe wiecheckValidityund meldet Probleme an den Benutzer, wenn das Ereignis nicht abgebrochen wird.labels— Eine Liste von Elementen, die dieses Element mit dem Attributlabel[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.
Könnten Sie die Vorteile zusammenfassen? Warum sollten wir das Rad neu erfinden, wenn das viel zusätzliche Arbeit, Bugs und mangelnde Sicherheit bedeutet?
Ist das nicht wie so viele moderne JavaScript-Hypes, bei denen es im Grunde Spaß für den Entwickler, aber schlecht für Leistung, Stabilität, Sicherheit, progressive Verbesserung und den Benutzer ist?
Um Ihren ersten Satz umzuformulieren: „Seit Anbeginn der Zeit warnen Experten davor, bestehende Browser-Elemente neu zu erstellen.“
Dennoch, vielleicht übersehe ich etwas, und es gibt wirklich einen Wert darin, dies zu tun (abgesehen davon, dass es so keeewl ist)?
Hallo, Skythe. Dies ist eine native API, die Ihnen die Möglichkeit gibt, auf das zuzugreifen, was der Browser standardmäßig bietet. Sicher, es muss im Moment noch polyfilled werden, aber hoffentlich wird es bald mehr Akzeptanz geben.
Die drei größten Anwendungsfälle, die ich sehe, sind
Grundsätzlich haben wir oben das Minimum Viable Product mit
ElementInternals, wohin Sie es mitnehmen, liegt bei Ihnen.Danke! Wenn ich an Formularen arbeite, versuche ich immer, mich so weit wie möglich auf native Browser-Verhaltensweisen zu verlassen.
Aber manchmal ist es nicht möglich. Meistens landen wir bei einem input type="hidden", der von der benutzerdefinierten Formulareingabe gesteuert wird.
Aber jetzt können wir mit ElementInternals direkt wie eine native Formulareingabe mit Fehlern und Validierung agieren.
Das ist großartig. Der Hauptvorteil ist, dass es nun eine nicht-hacky Lösung für das Problem gibt, das auftritt, wenn man eine benutzerdefinierte Eingabe mit Stil-Kapselung (ShadowDOM) erstellt und sehr schnell feststellt, dass sie sich nicht im übergeordneten
<form>registriert, da eine Shadow-Grenze dazwischen liegt.Die aktuellen Lösungen, die ich kenne
- Erzeugung eines input type="hidden" zur Delegation. Es ist ziemlich offensichtlich, warum das nicht gut ist.
- Eine benutzerdefinierte Eingabe umschließt die native Eingabe über einen
<slot>und erzeugt die native Eingabe selbst in LightDOM. LightDOM ist jedoch Benutzer-Territorium, nicht Komponenten-Autor.Und jetzt haben wir einen ordnungsgemäßen Weg :). Danke für den Artikel!
Ein Hauptanliegen der Formularassoziation sind die automatisch ausgefüllten Geschwister im Formular, was noch nicht unterstützt wird oder vielleicht nur fehlerhaft ist?
In vielen Fällen löst die Verwendung des Namensattributs die Autovervollständigung in einem bestimmten Feld aus. Es gibt auch eine API in ElementInternals zum automatischen Ausfüllen des Formulars, aber sie ist noch nicht polyfilled (ich überlege noch, wie man das am besten erreicht) und ein Teil wird in Chrome immer noch nicht unterstützt.