Leichte Formularvalidierung mit Alpine.js und Iodine.js

Avatar of Hugh Haworth
Hugh Haworth am

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

Viele Benutzer erwarten heutzutage sofortiges Feedback bei der Formularvalidierung. Wie erzielen Sie dieses Maß an Interaktivität, wenn Sie eine kleine statische Website oder eine serverseitig gerenderte Rails- oder Laravel-App erstellen? Alpine.js und Iodine.js sind zwei minimale JavaScript-Bibliotheken, mit denen wir hochgradig interaktive Formulare mit geringem technischen Schuldenaufwand und minimaler Auswirkung auf unsere Ladezeit erstellen können. Bibliotheken wie diese ersparen Ihnen die Notwendigkeit, schwergewichtige JavaScript-Tools mit Build-Schritten einzubinden, die Ihre Architektur verkomplizieren können.

Ich werde verschiedene Versionen der Formularvalidierung durchgehen, um die APIs dieser beiden Bibliotheken zu erklären. Wenn Sie das fertige Produkt kopieren und einfügen möchten, hier ist, was wir bauen werden. Versuchen Sie, mit fehlenden oder ungültigen Eingaben zu spielen und sehen Sie, wie das Formular reagiert.

Ein kurzer Blick auf die Bibliotheken

Bevor wir richtig eintauchen, ist es eine gute Idee, sich mit den Werkzeugen vertraut zu machen, die wir verwenden.

Alpine ist darauf ausgelegt, von einem CDN in Ihr Projekt gezogen zu werden. Kein Build-Schritt, keine Bundler-Konfiguration und keine Abhängigkeiten. Es benötigt nur ein kurzes GitHub README für seine Dokumentation. Mit nur 8,36 Kilobytes minifiziert und komprimiert ist es etwa ein Fünftel der Größe eines "create-react-app hello world". Hugo Di Fracesco bietet einen vollständigen und gründlichen Überblick darüber, was es ist und wie es funktioniert. Seine anfängliche Beschreibung ist ziemlich gut.

Alpine.js ist ein Ersatz im Vue-Template-Stil für jQuery und Vanilla JavaScript, anstatt ein Konkurrent für React/Vue/Svelte/WhateverFramework zu sein.

Iodine hingegen ist eine Micro-Formularvalidierungsbibliothek, die von Matt Kingshott erstellt wurde, der in der Laravel/Vue/Tailwind-Welt arbeitet. Iodine kann mit jedem Frontend-Framework als Helfer für die Formularvalidierung verwendet werden. Es ermöglicht uns, ein einzelnes Datenelement mit mehreren Regeln zu validieren. Iodine gibt auch sinnvolle Fehlermeldungen zurück, wenn die Validierung fehlschlägt. Sie können mehr in Matts Blogbeitrag lesen, der die Gründe hinter Iodine erklärt.

Ein kurzer Blick darauf, wie Iodine funktioniert

Hier ist eine sehr grundlegende clientseitige Formularvalidierung mit Iodine. Wir schreiben etwas Vanilla JavaScript, um auf die Formularübermittlung zu lauschen, und verwenden dann DOM-Methoden, um durch die Eingaben zu iterieren und jede Eingabe zu überprüfen. Wenn sie falsch ist, fügen wir der ungültigen Eingabe die Klasse "invalid" hinzu und verhindern die Formularübermittlung.

Wir ziehen Iodine über diesen CDN-Link für dieses Beispiel

<script src="https://cdn.jsdelivr.net/gh/mattkingshott/iodine@3/dist/iodine.min.js" defer></script>

Oder wir können es mit Skypack in ein Projekt importieren

import kingshottIodine from "https://cdn.skypack.dev/@kingshott/iodine";

Wir müssen kingshottIodine importieren, wenn wir Iodine von Skypack importieren. Dies fügt immer noch Iodine zu unserem globalen/window-Scope hinzu. In Ihrem Benutzercode können Sie sich weiterhin auf die Bibliothek als Iodine beziehen, aber stellen Sie sicher, dass Sie kingshottIodine importieren, wenn Sie sie von Skypack beziehen.

Um jede Eingabe zu überprüfen, rufen wir die Methode is auf Iodine auf. Wir übergeben den Wert der Eingabe als ersten Parameter und ein Array von Zeichenfolgen als zweiten Parameter. Diese Zeichenfolgen sind die Regeln, denen die Eingabe folgen muss, um gültig zu sein. Eine Liste der integrierten Regeln finden Sie in der Iodine-Dokumentation.

Die is-Methode von Iodine gibt entweder true zurück, wenn der Wert gültig ist, oder eine Zeichenfolge, die die fehlgeschlagene Regel angibt, wenn die Prüfung fehlschlägt. Das bedeutet, wir müssen einen strikten Vergleich verwenden, wenn wir auf die Ausgabe der Funktion reagieren; andernfalls bewertet JavaScript die Zeichenfolge als true. Was wir tun können, ist, ein Array von Zeichenfolgen für die Regeln für jede Eingabe als JSON in HTML-Datenattributen zu speichern. Dies ist nicht in Alpine oder Iodine integriert, aber ich finde es eine schöne Möglichkeit, Eingaben mit ihren Einschränkungen zusammen zu platzieren. Beachten Sie, dass Sie, wenn Sie dies tun, das JSON mit einfachen Anführungszeichen umschließen und doppelte Anführungszeichen innerhalb des Attributs verwenden müssen, um die JSON-Spezifikation einzuhalten.

So sieht das in unserem HTML aus

<input name="email" type="email" id="email" data-rules='["required","email"]'>

Wenn wir durch das DOM iterieren, um die Gültigkeit jeder Eingabe zu überprüfen, rufen wir die Funktion Iodine mit dem Eingabewert des Elements auf, gefolgt vom JSON.encode()-Ergebnis von dataset.rules des Elements. So sieht das mit Vanilla JavaScript DOM-Methoden aus.

let form = document.getElementById("form");

// This is a nice way of getting a list of checkable input elements
// And converting them into an array so we can use map/filter/reduce functions:
let inputs = [...form.querySelectorAll("input[data-rules]")];

function onSubmit(event) {
  inputs.map((input) => {
    if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {
      event.preventDefault();
      input.classList.add("invalid");
    }
  });
}
form.addEventListener("submit", onSubmit);

So sieht diese sehr grundlegende Implementierung aus

Wie Sie sehen können, ist das keine gute Benutzererfahrung. Am wichtigsten ist, dass wir dem Benutzer nicht mitteilen, was mit der Übermittlung falsch ist. Der Benutzer muss auch warten, bis das Formular abgeschickt ist, bevor er etwas Falsches erfährt. Und frustrierenderweise behalten alle Eingaben die Klasse "invalid" auch dann, wenn der Benutzer sie korrigiert hat, um unsere Validierungsregeln zu befolgen.

Hier kommt Alpine ins Spiel

Lassen Sie es uns einbinden und verwenden, um eine schöne Benutzerunterstützung während der Interaktion mit dem Formular zu bieten.

Eine gute Option für die Formularvalidierung ist die Validierung einer Eingabe, wenn sie aus dem Fokus genommen wird oder bei Änderungen nach dem Ausfokussieren. Dies stellt sicher, dass wir den Benutzer nicht anschreien, bevor er mit dem Schreiben fertig ist, und geben ihm dennoch sofortiges Feedback, wenn er eine ungültige Eingabe hinterlässt oder zurückgeht und einen Eingabewert korrigiert.

Wir ziehen Alpine vom CDN herein

<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.min.js" defer></script>

Oder wir können es mit Skypack in ein Projekt importieren

import alpinejs from "https://cdn.skypack.dev/alpinejs";

Nun gibt es nur noch zwei Zustände, die wir für jede Eingabe halten müssen

  • Ob die Eingabe aus dem Fokus genommen wurde
  • Die Fehlermeldung (das Fehlen dieser wird bedeuten, dass wir eine gültige Eingabe haben)

Die Validierung, die wir im Formular anzeigen, wird eine Funktion dieser beiden Zustände sein.

Alpine ermöglicht es uns, diesen Zustand in einer Komponente zu halten, indem wir ein einfaches JavaScript-Objekt in einem x-data-Attribut auf einem übergeordneten Element deklarieren. Dieser Zustand kann von seinen untergeordneten Elementen aufgerufen und geändert werden, um Interaktivität zu erzeugen. Um unser HTML sauber zu halten, können wir eine JavaScript-Funktion deklarieren, die alle Daten und/oder Funktionen zurückgibt, die das Formular benötigt. Dann müssen wir nur noch unsere Funktion mit dem Alpine.data-Attribut bei Alpine registrieren und Alpine.start() aufrufen, nachdem wir sie registriert haben. Die Verwendung von Alpine auf diese Weise bietet auch eine wiederverwendbare Möglichkeit, Logik zu teilen, da wir dieselbe Funktion in mehreren Komponenten oder sogar mehreren Projekten verwenden können.

Lassen Sie uns die Formulardaten initialisieren, um Objekte für jedes Eingabefeld mit zwei Eigenschaften zu speichern: eine leere Zeichenfolge für errorMessage und einen booleschen Wert namens blurred. Wir werden den Namensattribut jedes Elements als Schlüssel verwenden.


<form id="form" x-data="form" action="">
  <h1>Log In</h1>

  <label for="username">Username</label>
  <input name="username" id="username" type="text" data-rules='["required"]'>

  <label for="email">Email</label>
  <input name="email" type="email" id="email" data-rules='["required","email"]'>

  <label for="password">Password</label>
  <input name="password" type="password" id="password" data-rules='["required","minimum:8"]'>

  <label for="passwordConf">Confirm Password</label>
  <input name="passwordConf" type="password" id="passwordConf" data-rules='["required","minimum:8"]'>

  <input type="submit">
</form>

Und hier ist unsere Funktion zur Einrichtung der Daten. Beachten Sie, dass die Schlüssel mit dem name-Attribut unserer Eingaben übereinstimmen.


Alpine.data("form", form);
Alpine.start();
function form(){ 
  return {
    username: {errorMessage:'', blurred:false},
    email: {errorMessage:'', blurred:false},
    password: {errorMessage:'', blurred:false},
    passwordConf: {errorMessage:'', blurred:false},
  }
}

Jetzt können wir das x-bind:class-Attribut von Alpine auf unseren Eingaben verwenden, um die Klasse "invalid" hinzuzufügen, wenn die Eingabe aus dem Fokus genommen wurde und eine Nachricht für das Element in unseren Komponentendaten vorhanden ist. So sieht das bei unserer Benutzernameneingabe aus.

<input name="username" id="username" type="text" 
x-bind:class="{'invalid':username.errorMessage && username.blurred}" data-rules='["required"]'>

Auf Eingabeänderungen reagieren

Nun müssen wir unser Formular so gestalten, dass es auf Eingabeänderungen und das Ausfokussieren von Eingabeständen reagiert. Das können wir tun, indem wir Ereignis-Listener hinzufügen. Alpine bietet eine prägnante API dafür, entweder mit x-on oder, ähnlich wie bei Vue, können wir ein @-Symbol verwenden. Beide Deklarationsarten verhalten sich gleich.

Beim input-Ereignis müssen wir die Eigenschaft errorMessage in den Komponentendaten in eine Fehlermeldung ändern, wenn der Wert ungültig ist; andernfalls machen wir daraus eine leere Zeichenfolge.

Beim blur-Ereignis müssen wir die Eigenschaft blurred auf true setzen für das Objekt mit einem Schlüssel, der dem Namen des aus dem Fokus genommenen Elements entspricht. Wir müssen auch die Fehlermeldung neu berechnen, um sicherzustellen, dass sie nicht die leere Zeichenfolge verwendet, die wir als Fehlermeldung initialisiert haben.

Wir fügen also zwei weitere Funktionen zu unserem Formular hinzu, um auf das Ausfokussieren und Eingabeänderungen zu reagieren, und verwenden den name-Wert des Event-Ziels, um herauszufinden, welchen Teil unserer Komponentendaten wir ändern sollen. Wir können diese Funktionen als Eigenschaften in dem von der Funktion form() zurückgegebenen Objekt deklarieren.

Hier ist unser HTML für die Benutzernameneingabe mit den angehängten Ereignis-Listenern

<input 
  name="username" id="username" type="text"
  x-bind:class="{'invalid':username.errorMessage && username.blurred}" 
  @blur="blur" @input="input"
  data-rules='["required"]'
>

Und unser JavaScript mit den Funktionen, die auf die Ereignis-Listener reagieren

function form(){
  return {
    username: {errorMessage:'', blurred:false},
    email: {errorMessage:'', blurred:false},
    password:{ errorMessage:'', blurred:false},
    passwordConf: {errorMessage:'', blurred:false},
    blur: function(event) {
      let ele = event.target;
      this[ele.name].blurred = true;
      let rules = JSON.parse(ele.dataset.rules)
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    input: function(event) {
      let ele = event.target;
      let rules = JSON.parse(ele.dataset.rules)
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    getErrorMessage: function() {
    // to be completed
    }
  }
}

Fehler abrufen und anzeigen

Als nächstes müssen wir unsere Funktion getErrorMessage schreiben.

Wenn die Iodine-Prüfung true zurückgibt, setzen wir die Eigenschaft errorMessage auf eine leere Zeichenfolge. Andernfalls übergeben wir die gebrochene Regel an eine weitere Iodine-Methode: getErrorMessage. Diese gibt eine menschenlesbare Nachricht zurück. So sieht das aus.

getErrorMessage:function(value, rules){
  let isValid = Iodine.is(value, rules);
  if (isValid !== true) {
    return Iodine.getErrorMessage(isValid);
  }
  return '';
}

Nun müssen wir auch unsere Fehlermeldungen dem Benutzer anzeigen.

Fügen wir <p>-Tags mit der Klasse error-message unter jeder Eingabe hinzu. Wir können ein weiteres Alpine-Attribut namens x-show auf diesen Elementen verwenden, um sie nur anzuzeigen, wenn ihre Fehlermeldung vorhanden ist. Das x-show-Attribut veranlasst Alpine, display: none; auf das Element zu schalten, je nachdem, ob ein JavaScript-Ausdruck zu true ausgewertet wird. Wir können denselben Ausdruck verwenden, den wir in der Klasse show-invalid auf der Eingabe verwendet haben.

Um den Text anzuzeigen, können wir unsere Fehlermeldung mit x-text verbinden. Dies bindet automatisch den innertext an einen JavaScript-Ausdruck, in dem wir unseren Komponentenzustand verwenden können. So sieht das aus.

<p x-show="username.errorMessage && username.blurred" x-text="username.errorMessage" class="error-message"></p>

Eine letzte Sache, die wir tun können, ist, den onsubmit-Code von vor dem Einbinden von Alpine wiederzuverwenden, aber diesmal können wir den Ereignis-Listener zum Formularelement mit @submit hinzufügen und eine submit-Funktion in unseren Komponentendaten verwenden. Alpine ermöglicht es uns, $el zu verwenden, um auf das übergeordnete Element zu verweisen, das unseren Komponentenzustand hält. Das bedeutet, wir müssen keine längeren DOM-Methoden schreiben.

<form id="form" x-data="form" @submit="submit" action="">
  <!-- inputs...  -->
</form>
submit: function (event) {
  let inputs = [...this.$el.querySelectorAll("input[data-rules]")];
  inputs.map((input) => {
    if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {
      event.preventDefault();
    }
  });
}

Das nähert sich dem Ziel.

  • Wir haben Echtzeit-Feedback, wenn die Eingabe korrigiert wird.
  • Unser Formular informiert den Benutzer über Probleme, bevor er das Formular abschickt, und erst nachdem er die Eingaben aus dem Fokus genommen hat.
  • Unser Formular wird nicht abgeschickt, wenn es ungültige Eigenschaften gibt.

Validierung auf der Client-Seite einer serverseitig gerenderten App

Es gibt zwar immer noch einige Probleme mit dieser Version, einige werden jedoch in der Pen nicht sofort offensichtlich, da sie mit dem Server zusammenhängen. Zum Beispiel ist es schwierig, alle Fehler auf der Client-Seite in einer serverseitig gerenderten App zu validieren. Was ist, wenn die E-Mail-Adresse bereits verwendet wird? Oder ein komplizierter Datenbankeintrag überprüft werden muss? Unser Formular muss eine Möglichkeit haben, Fehler anzuzeigen, die auf dem Server gefunden wurden. Es gibt Möglichkeiten, dies mit AJAX zu tun, aber wir werden uns eine leichtere Lösung ansehen.

Wir können die serverseitigen Fehler in einem weiteren JSON-Array-Datenattribut für jedes Eingabeelement speichern. Die meisten Backend-Frameworks bieten hierfür eine reasonably einfache Möglichkeit. Wir können ein weiteres Alpine-Attribut namens x-init verwenden, um eine Funktion auszuführen, wenn die Komponente initialisiert wird. In dieser Funktion können wir die serverseitigen Fehler aus dem DOM in die Komponentendaten jedes Eingabeelements ziehen. Dann können wir die Funktion getErrorMessage aktualisieren, um zu prüfen, ob serverseitige Fehler vorliegen und diese zuerst zurückzugeben. Wenn keine vorhanden sind, können wir nach clientseitigen Fehlern suchen.

<input name="username" id="username" type="text" 
x-bind:class="{'invalid':username.errorMessage && username.blurred}" 
@blur="blur" @input="input" data-rules='["required"]' 
data-server-errors='["Username already in use"]'>

Und um sicherzustellen, dass die serverseitigen Fehler nicht die ganze Zeit angezeigt werden, auch nachdem der Benutzer mit der Korrektur begonnen hat, ersetzen wir sie durch ein leeres Array, wann immer die Eingabe geändert wird.

So sieht unsere Init-Funktion jetzt aus.

init: function () {
  this.inputElements = [...this.$el.querySelectorAll("input[data-rules]")];
  this.initDomData();
},
initDomData: function () {
  this.inputElements.map((ele) => {
  this[ele.name] = {
    serverErrors: JSON.parse(ele.dataset.serverErrors),
    blurred: false
    };
  });
}

Umgang mit voneinander abhängigen Eingaben

Einige der Formularfelder können für ihre Gültigkeit von anderen abhängen. Zum Beispiel würde ein Bestätigungsfeld für das Passwort von dem Passwort abhängen, das es bestätigt. Oder ein Feld für das Einstellungsdatum müsste einen Wert *später* als Ihr Geburtsdatum enthalten. Das bedeutet, es ist eine gute Idee, alle Eingaben des Formulars bei jeder Änderung zu überprüfen.

Wir können alle Eingabeelemente durchlaufen und ihren Zustand bei *jedem* Eingabe- und Fokussierungsereignis festlegen. Auf diese Weise wissen wir, dass Eingaben, die voneinander abhängen, keine veralteten Daten verwenden.

Um dies zu testen, fügen wir eine Regel matchingPassword für unsere Passwortbestätigung hinzu. Iodine ermöglicht es uns, mit der Methode addRule neue benutzerdefinierte Regeln hinzuzufügen.

Iodine.addRule(
  "matchingPassword",
  value => value === document.getElementById("password").value
);

Jetzt können wir eine benutzerdefinierte Fehlermeldung festlegen, indem wir der messages-Eigenschaft in Iodine einen Schlüssel hinzufügen.

Iodine.messages.matchingPassword="Password confirmation needs to match password";

Wir können beide Aufrufe in unserer init-Funktion einfügen, um diese Regel einzurichten.

In unserer vorherigen Implementierung hätten wir das "Passwort"-Feld ändern können und es hätte das Feld "Passwortbestätigung" nicht ungültig gemacht. Aber jetzt, da wir bei jeder Änderung durch alle Eingaben iterieren, stellt unser Formular immer sicher, dass das Passwort und die Passwortbestätigung übereinstimmen.

Einige letzte Handgriffe

Ein kleiner Refactor, den wir durchführen können, ist, die Funktion getErrorMessage so zu gestalten, dass sie nur eine Nachricht zurückgibt, wenn die Eingabe aus dem Fokus genommen wurde. Dies kann unser HTML etwas kürzer machen, da wir nur einen Wert prüfen müssen, bevor wir entscheiden, ob eine Eingabe ungültig ist. Das bedeutet, unser x-bind-Attribut kann so kurz sein:

x-bind:class="{'invalid':username.errorMessage}"

So sehen unsere Funktionen aus, um durch die Eingaben zu iterieren und die errorMessage-Daten jetzt zu setzen.

updateErrorMessages: function () {
  // Map through the input elements and set the 'errorMessage'
  this.inputElements.map((ele) => {
    this[ele.name].errorMessage = this.getErrorMessage(ele);
  });
},
getErrorMessage: function (ele) {
  // Return any server errors if they're present
  if (this[ele.name].serverErrors.length > 0) {
    return this[ele.name].serverErrors[0];
  }
  // Check using Iodine and return the error message only if the element has not been blurred
  const error = Iodine.is(ele.value, JSON.parse(ele.dataset.rules));
  if (error !== true && this[ele.name].blurred) {
    return Iodine.getErrorMessage(error);
  }
  // Return empty string if there are no errors
  return "";
},

Wir können auch die Ereignisse @blur und @input von allen unseren Eingaben entfernen, indem wir diese Ereignisse im übergeordneten Formularelement abhören. Es gibt jedoch ein Problem: Das blur-Ereignis wird nicht weitergeleitet (übergeordnete Elemente, die auf dieses Ereignis lauschen, erhalten es nicht, wenn es auf ihren untergeordneten Elementen ausgelöst wird). Glücklicherweise können wir blur durch das focusout-Ereignis ersetzen, das im Grunde dasselbe Ereignis ist, aber es wird weitergeleitet, sodass wir es in unserem übergeordneten Formularelement abhören können.

Schließlich wächst unser Code um viel Boilerplate. Wenn wir Namen von Eingaben ändern würden, müssten wir jedes Mal die Daten in unserer Funktion neu schreiben und neue Ereignis-Listener hinzufügen. Um das erneute Schreiben der Komponentendaten jedes Mal zu vermeiden, können wir durch die Eingaben des Formulars mit einem data-rules-Attribut iterieren, um unsere anfänglichen Komponentendaten in der init-Funktion zu generieren. Dies macht den Code wiederverwendbarer für zusätzliche Formulare. Alles, was wir tun müssten, ist, das JavaScript einzufügen und die Regeln als Datenattribut hinzuzufügen, und wir sind einsatzbereit.

Oh, und hey, nur weil es mit Alpine *so* einfach ist, fügen wir eine Einblend-Transition hinzu, die die Aufmerksamkeit auf die Fehlermeldungen lenkt.

<p class="error-message" x-show="username.errorMessage" x-text="username.errorMessage" x-transition:enter></p>

Und hier ist das Endergebnis: Reaktive, wiederverwendbare Formularvalidierung bei minimalen Seitenladekosten.

Wenn Sie dies in Ihrer eigenen Anwendung verwenden möchten, können Sie die Funktion form kopieren, um die gesamte geschriebene Logik wiederzuverwenden. Sie müssten nur Ihre HTML-Attribute konfigurieren, und Sie wären bereit.