Fortgeschrittene Formulare mit Alpine.js

Visualisiere Validierungsfehler in deinem Backend für Eingabeelemente mit Alpine.js und verbessere die Benutzerfreundlichkeit deiner Formulare. Mehr dazu in diesem Artikel.

Jairus Joer

Jairus Joer

Die Texte in diesem Artikel wurden teilweise mit Hilfe künstlicher Intelligenz erarbeitet und von uns korrigiert und überarbeitet. Für die Generierung wurden folgende Dienste verwendet:

Wenn du noch nicht mit der Arbeit an Formularen mit Alpine.js vertraut bist, kannst du deine Kenntnisse in unserem ersten Artikel zu diesem Thema, Interaktive Formulare mit Alpine.js, auffrischen.

In unserem ersten Artikel zu interaktiven Formularen mit Alpine.js haben wir bereits angedeutet, dass Alpine.js neben der allgemeinen Darstellung von serverseitigen Informationen im Formular auch dazu verwendet werden kann, um u.a. Einfluss auf einzelne Elemente zu nehmen.

Aufgrund der großen Nachfrage haben wir uns entschlossen, genau dieses Thema in diesem Folgeartikel aufzugreifen und beispielhaft zu zeigen, wie man mit Alpine.js Informationen und Zustände zur Validierung eines Formulars nutzen kann.

Einrichtung

Für diese Demonstration verwenden wir unsere Astro Boilerplate, die wir bereits in einem früheren Artikel ausführlich vorgestellt haben.

Download Astro Boilerplate

Wenn unsere Boilerplate nicht das Richtige für dich ist, ist das kein Problem. Die Schritte zur Validierung von Formulareingaben funktionieren in jedem Projekt mit Alpine.js.

Methoden für Alpine.js integrieren

Um im weiteren Verlauf der Implementierung auf die benötigten Daten und Methoden aus Alpine.js zugreifen zu können, werden diese zunächst deklariert, um im weiteren Verlauf Fehler zu vermeiden.

form.ts

form() kontrolliert den Zustand loading und speichert die vom Server gesendete Response über die Methode submit(), die beim Absenden des Formulars ausgeführt wird.

Außerdem ist eine fiktive fakeResponse() enthalten, die beispielhafte und vereinfachte Validierungsfehler von unserem fiktiven Backend “empfängt”.

scripts/alpine/form.ts
import { sleep } from "../utilities"

export const form = () => ({
  loading: false,
  response: null as unknown,

  async submit(event: SubmitEvent) {
    this.loading = true
    this.response = null
    const formData = new FormData(event.target as HTMLFormElement)

    /**
     * Replace the following fake response with your `fetch` request and 
     * receive the validated results from the server side as JSON. 
     * 
     * Make sure you add the necessary attributes to the `<Input />' 
     * elements to perform client-side validation as well.
     */

    const fakeResponse = async () => {
      await sleep(1000) // Mock response time

      return {
        errors: {
          // [input.name]: "message string"
          username: "Username is alrady taken",
          password: "Password is too short"
        }
      }
    }

    this.response = await fakeResponse()
    this.loading = false
  }
})

Die Response muss ein error Objekt enthalten, in dem jedes Schlüssel-Wert-Paar aus dem Namen des Eingabeelements und dem zugehörigen Validierungsfehler besteht.

input.ts

input.ts übernimmt die Darstellung der Validierungsfehler für ein Eingabeelement über die Methode validate(), die über das Attribut x-effect eingebunden wird, um die Daten für die Darstellung beim Absenden des Formulars neu zu berechnen.

scripts/alpine/input.ts
export const input = () => ({
  error: null as unknown,

  validate() {
    if (!this.response?.errors?.[this.$el.name]) return (this.error = null)
    this.error = this.response.errors[this.$el.name]
  },
});

globals.ts

Abschließend werden für diesen Schritt die für Alpine.js deklarierten Methoden importiert und im EventListener alpine:init registriert, um auf die benötigten Scopes zugreifen zu können.

scripts/globals.ts
import Alpine from "alpinejs"
import { app } from "./alpine/app"
import { form } from "./alpine/form"
import { input } from "./alpine/input"

// Await Alpine.js initialization
document.addEventListener("alpine:init", () => {
  Alpine.data("app", app)
  Alpine.data("form", form)
  Alpine.data("input", input)
})

Alpine.start()

Optionale Utility-Methoden deklarieren

Damit wir Namen für Eingabeelemente auch gleichzeitig als Label benutzen können, legen wir die Methode capitalize an, welche Strings geschrieben in kebab-case (z. B.: "email-address") aufteilt und jedes Wort kapitalisiert.

Solltest du dich gegen die Kapitalisierung entscheiden, müssen die entsprechenden Referenzen in der Komponente input.astro entfernt werden

scripts/utilities.ts
export const capitalize = (string: string) => {
  return string.split("-").map(word => word[0].toUpperCase() + word.substring(1)).join(" ")
}

Seiten und Komponenten in Astro anlegen

Im nächsten Schritt legen wir die von uns für das Formular benötigten Seiten und Komponenten an. Hierbei definieren wir eine <Input /> Komponente und integrieren diese in den Formularblock.

input.astro

input.astro fasst die Elemente <input /> und <label> in einer Komponente zusammen und enthält zusätzlich die Darstellung der Validierungsfehler, die über den Alpine-Kontext input abgebildet werden.

components/input.astro
---
import { capitalize } from "@/scripts/utilities"

const { name, ...props } = Astro.props
---

<div 
  class="relative font-medium" 
  x-data="input"
>
  <div 
    class="absolute pointer-events-none overflow-hidden top-3 inset-x-4 flex gap-1 items-center leading-4 text-xs transition-colors" 
    x-bind:class="error && 'text-rose-500'"
  >
    <label 
      class="mr-auto" 
      for={name} 
      title={capitalize(name)}
    >
      {capitalize(name)}{props?.required && '*'}
    </label>
    <div 
      class="ml-3 overflow-hidden flex justify-end gap-1" 
      x-cloak 
      x-show="error" 
      x-transition
    >
      <span 
        class="truncate" 
        x-text="error"
      ></span>
    </div>
  </div>
  <input 
    class="w-full pt-7 pb-3 px-4 border rounded-xl leading-6 bg-transparent text-foreground invalid:border-rose-500 disabled:bg-muted transition-colors" 
    x-bind:class="error && 'border-rose-500'" 
    {name} 
    {...props} 
    x-effect="validate" 
  />
</div>

index.astro

index.astro repräsentiert unseren Formularblock und verwendet die vordefinierte Komponente <Input /> und ergänzt deren Logik um den Kontext form, so dass Fehler aus dem Objekt response dargestellt werden können.

Während unsere Komponente <Input /> die Darstelung der Validierungsfehler handhabt, binden wir das Attribut disabled der einzelnen Eingabeelemente and den Zustand loading, um Mehrfachsendungen des Formulars während der Verarbeitung zu unterbinden.

pages/index.astro
---
import Root from "@/layouts/root.astro"
import Input from "@/components/input.astro"

const meta = {
  title: "Advanced forms with Alpine.js"
}
---

<Root {meta}>
  <main>
    <form 
      class="grid gap-2 p-6" 
      x-data="form" 
      x-on:submit.prevent="submit"
    >
      <Input
        id="username"
        name="username"
        type="email"
        required
        placeholder="[email protected]"
        x-bind:disabled="loading"
      />
      <Input
        id="password"
        name="password"
        type="password"
        required
        placeholder="Your password"
        x-bind:disabled="loading"
      />
      <button
        class="bg-primary text-primary-foreground h-12 rounded-xl font-medium disabled:opacity-50 transition-opacity"
        type="submit"
        x-bind:disabled="loading"
      >
        Submit
      </button>
    </form>
  </main>
</Root>

TL;DR

Mit Alpine.js zeigen wir, wie Validierungsfehler aus dem Backend dynamisch in einem Formular dargestellt werden und wie Eingabeelemente auf entsprechende Ereignisse im Browser reagieren.