Eigene x-Direktiven registrieren: die Alpine Directive API im Detail
AI generated
x-data
Alpine
Alpine.js / Directive API
Eigene x-Direktiven registrieren
Die Alpine Directive API im Detail, mit effect() und cleanup()

Alpine.directive() gibt euch die Möglichkeit, eine komplett neue x-Direktive wie x-tooltip oder x-clickaway zu bauen, mit vollem Zugriff auf das DOM-Element, den Ausdruck aus dem HTML und einem Reaktivitäts-Werkzeugkasten, der sich sauber in Alpines eigenen Lebenszyklus einfügt.

10 Min. Lesezeit Alpine.directive() effect() und cleanup()

1. Direktive, Magic Property oder Plugin: drei verschiedene Erweiterungspunkte

Alpine bietet nicht nur einen, sondern drei unterschiedliche Wege, eigene Funktionalität einzubauen, und diese drei werden in der Praxis häufig durcheinandergeworfen. Eine Direktive wie x-tooltip oder x-clickaway ist ein neues HTML-Attribut, das Alpine beim Parsen eines Elements erkennt und mit einer eigenen Callback-Funktion verknüpft. Sie hat direkten Zugriff auf das DOM-Element, kann Event-Listener registrieren, eigene Kindknoten erzeugen und auf Änderungen im Ausdruck reagieren. Genau darum geht es in diesem Artikel: die Alpine.directive()-API im Detail, nicht die verwandten, aber anderen Konzepte Magic Property und Plugin.

Eine Magic Property wie $el oder eine selbst registrierte $clipboard ist dagegen kein Attribut, sondern ein Wert oder eine Funktion, die innerhalb eines beliebigen Alpine-Ausdrucks über das Dollarzeichen aufgerufen wird, etwa in x-on:click="$clipboard(text)". Sie hat keinen eigenen Platz im HTML-Markup und keinen eigenen Lebenszyklus, sondern wird bei jeder Auswertung des Ausdrucks neu bereitgestellt. Ein Plugin wiederum, erzeugt über Alpine.plugin(), ist die Verteilungseinheit: ein Plugin kann beliebig viele Direktiven, Magic Properties, globale Stores und Alpine.data()-Komponenten zu einem npm-Paket bündeln, so wie es @alpinejs/mask oder @alpinejs/focus tun. Wer nur eine einzelne Direktive für ein Projekt braucht, muss dafür kein Plugin bauen, ein direkter Aufruf von Alpine.directive() reicht völlig aus.

2. Grundsyntax: Alpine.directive() und der richtige Zeitpunkt der Registrierung

Die Registrierung folgt einem einfachen Muster: Alpine.directive(name, callback). Der Name wird dabei in camelCase angegeben, auch wenn er später im HTML in dash-case verwendet wird, aus myTooltip wird also x-my-tooltip. Der Callback erhält bei jedem Element, auf dem die Direktive im Markup steht, mehrere Argumente, über die im nächsten Abschnitt im Detail gesprochen wird.

Entscheidend ist der Zeitpunkt: Alpine.directive() muss aufgerufen werden, bevor Alpine.start() läuft, sonst hat Alpine das DOM bereits initialisiert und kennt die neue Direktive für bereits gerenderte Elemente nicht. Bei einer CDN-Einbindung heißt das konkret: die Registrierung passiert im alpine:init-Event-Listener, bei einem Build-Setup mit Bundler direkt vor dem manuellen Alpine.start()-Aufruf.


document.addEventListener('alpine:init', () => {
  // Registrierung MUSS vor Alpine.start() passieren
  Alpine.directive('tooltip', (el, { value, expression, modifiers }, { Alpine, effect, cleanup }) => {
    // Direktiven-Logik kommt in den nächsten Abschnitten
  })
})

3. Die Parameter im Detail: el, Ausdrucks-Kontext und Utilities

Der Callback von Alpine.directive() bekommt drei Argumente. Das erste, el, ist das rohe DOM-Element, auf dem die Direktive steht, hier lässt sich direkt mit classList, addEventListener oder appendChild arbeiten, exakt wie bei jedem anderen DOM-Element auch. Das zweite Argument ist ein Objekt mit dem geparsten Ausdruck: value enthält den Teil vor einem Doppelpunkt, etwa right bei x-tooltip:right, modifiers ist ein Array aller Punkt-Modifikatoren, etwa ['delay', '500'] bei x-tooltip.delay.500, und expression ist der rohe JavaScript-Ausdruck als String, also der Inhalt zwischen den Anführungszeichen im Attribut.

Das dritte Argument ist ein Utility-Objekt, das den eigentlichen Zugriff auf Alpines Maschinerie erlaubt: evaluate(expression) wertet den Ausdruck einmalig im Kontext der Komponente aus, evaluateLater(expression) gibt eine wiederverwendbare Auswertungsfunktion zurück, effect(callback) registriert eine reaktive Funktion, die bei jeder Änderung eines darin gelesenen reaktiven Werts erneut läuft, und cleanup(callback) registriert eine Funktion, die läuft, sobald das Element aus dem DOM entfernt wird. Diese vier Funktionen bilden zusammen den kompletten Werkzeugkasten für eine gut gebaute eigene Direktive.

4. Praxisbeispiel: eine vollständige x-tooltip-Direktive

Am konkretesten wird die API an einem vollständigen Beispiel. Die folgende Direktive erzeugt bei mouseenter ein Tooltip-Element, positioniert es relativ zum Zielelement und entfernt es bei mouseleave wieder. Der Tooltip-Text kommt aus dem Ausdruck und wird über evaluateLater plus effect reaktiv gehalten, ändert sich also der zugrunde liegende Wert in x-data, aktualisiert sich der bereits sichtbare Tooltip automatisch mit, ohne dass jemand ein manuelles Update anstoßen muss.


Alpine.directive('tooltip', (el, { expression }, { evaluateLater, effect, cleanup }) => {
  let getText = evaluateLater(expression)
  let tooltip = document.createElement('div')
  tooltip.className = 'alpine-tooltip'
  tooltip.style.cssText = 'position:absolute;display:none;padding:4px 8px;background:#111;color:#fff;border-radius:4px;font-size:12px;'
  document.body.appendChild(tooltip)

  let show = () => {
    let rect = el.getBoundingClientRect()
    tooltip.style.left = `${rect.left + window.scrollX}px`
    tooltip.style.top = `${rect.top + window.scrollY - 32}px`
    tooltip.style.display = 'block'
  }
  let hide = () => { tooltip.style.display = 'none' }

  // effect() hält den Tooltip-Text reaktiv, evaluate() allein würde das nicht tun
  effect(() => {
    getText(text => { tooltip.textContent = text })
  })

  el.addEventListener('mouseenter', show)
  el.addEventListener('mouseleave', hide)

  // Ohne cleanup() bliebe der Tooltip-Knoten und die Listener im DOM hängen
  cleanup(() => {
    el.removeEventListener('mouseenter', show)
    el.removeEventListener('mouseleave', hide)
    tooltip.remove()
  })
})

5. Reaktivität mit effect(): warum ein einmaliges evaluate() oft nicht reicht

Ein häufiger Anfängerfehler ist, den Ausdruck nur einmal beim Erzeugen der Direktive mit evaluate() auszulesen und das Ergebnis fest in einer Variable zu speichern. Das funktioniert für statische Werte, ignoriert aber jede spätere Änderung. Wird der Ausdruck stattdessen in effect(() => { ... }) eingebettet, merkt sich Alpine automatisch, welche reaktiven Properties innerhalb des Callbacks gelesen wurden, und führt den Callback bei jeder Änderung dieser Properties erneut aus, exakt wie bei x-text oder x-show intern auch.

Für die Tooltip-Direktive bedeutet das konkret: der Text im bereits offenen Tooltip aktualisiert sich live, wenn sich beispielsweise ein Zähler oder ein Server-Status ändert, auf den der Ausdruck verweist, ganz ohne dass die Direktive selbst neu initialisiert werden muss. Diese Granularität, ein effect() pro Direktiveninstanz statt eines globalen Re-Renders, ist einer der Kernvorteile gegenüber einer naiven Eigenimplementierung mit rohem MutationObserver oder manuellem Polling.

6. cleanup(): Event-Listener und DOM-Knoten sauber entfernen

Jede Direktive, die Ressourcen außerhalb des eigenen Elements anlegt, etwa einen zusätzlichen Tooltip-Knoten im body, einen globalen Event-Listener auf window oder ein setInterval, muss diese Ressourcen auch wieder freigeben, sobald das Element verschwindet. Genau dafür existiert cleanup(callback): die übergebene Funktion läuft automatisch, wenn Alpine das Element entfernt, sei es durch x-if, das seinen Wahrheitswert wechselt, durch x-for, dessen Liste kürzer wird, oder schlicht durch klassisches DOM-Entfernen per JavaScript.

Wird cleanup() vergessen, bleibt bei der Tooltip-Direktive aus dem vorherigen Abschnitt beispielsweise der erzeugte Tooltip-Knoten im DOM hängen, selbst wenn das ursprüngliche Element längst verschwunden ist, und der mouseenter-Listener bleibt aktiv und hält eine Referenz auf eine Closure, die eigentlich hätte entsorgt werden sollen. Bei einer Liste mit hunderten Elementen, die per x-for häufig neu gerendert wird, summiert sich das zu einem klassischen Memory Leak, der sich in den Chrome DevTools unter dem Detached-DOM-Nodes-Filter im Heap-Snapshot zuverlässig nachweisen lässt.

7. modifiers und value praktisch nutzen: Konfiguration direkt im Attributnamen

Modifikatoren erlauben es, Konfiguration direkt im HTML-Attribut unterzubringen, ohne den Ausdruck selbst zu verkomplizieren. Bei x-tooltip.right.delay.500="'Text'" landet ['right', 'delay', '500'] im modifiers-Array, und die Direktive kann daraus lesen, dass der Tooltip rechts statt oberhalb erscheinen und die Anzeige um 500 Millisekunden verzögert werden soll. Das entspricht exakt dem Muster, das Alpine für eingebaute Direktiven wie x-on:input.debounce.300ms selbst verwendet.

Der value wiederum kommt aus einem optionalen Doppelpunkt-Teil, etwa x-tooltip:top="'Text'", und eignet sich für genau eine Hauptoption, während modifiers für mehrere, oft boolesche Flags gedacht ist. Wer beides kombiniert, sollte in der eigenen Direktive früh im Callback eine kleine Konfigurationsauswertung vorschalten, die aus value und modifiers ein sauberes Optionsobjekt baut, statt diese Logik über die ganze Callback-Funktion zu verteilen.

8. Priorität gegenüber eingebauten Direktiven und Namenskonvention

Alpine wertet Direktiven auf einem Element in einer festen Reihenfolge aus, unter anderem damit x-data immer vor x-bind und x-bind immer vor x-on läuft. Eine eigene Direktive reiht sich standardmäßig an einer festen Stelle in diese Reihenfolge ein, lässt sich über .before('bind') oder .after('bind') direkt an der Registrierung aber gezielt vor oder nach einer eingebauten Direktive einsortieren, etwa wenn die eigene Direktive auf einen bereits von x-bind gesetzten Attributwert angewiesen ist.

Bei der Namensvergabe gilt eine feste Konvention: mehrteilige Namen werden bei der Registrierung in camelCase geschrieben, im HTML aber automatisch als dash-case erwartet. Alpine.directive('clickOutside', ...) wird im Markup also zu x-click-outside, nicht zu x-clickOutside. Wer diese Umwandlung übersieht und im HTML versehentlich camelCase schreibt, bekommt keinen Fehler, sondern schlicht eine Direktive, die niemals ausgelöst wird, weil Alpine sie im DOM nicht wiedererkennt.

9. Häufige Fehler und wie man sie in der Praxis findet

Der mit Abstand häufigste Fehler ist die Registrierung nach Alpine.start(), meist weil ein Modul erst nach einem asynchronen Import geladen wird. Das Symptom ist eindeutig: die Direktive bleibt als rohes, unverarbeitetes Attribut im gerenderten HTML sichtbar, taucht in den Alpine DevTools nicht als aktive Direktive auf, und es erscheint keine Fehlermeldung in der Konsole, weil Alpine ein unbekanntes x--Attribut einfach ignoriert statt es zu beanstanden.

Der zweithäufigste Fehler ist fehlendes cleanup() bei Direktiven, die eigene DOM-Knoten oder globale Listener anlegen. Drittens wird oft übersehen, dass evaluate() synchron und einmalig arbeitet, während echte, dauerhaft aktualisierte Reaktivität immer effect() braucht. Wer diese drei Punkte, korrekter Registrierungszeitpunkt, konsequentes Cleanup und der bewusste Griff zu effect() statt evaluate() für dynamische Werte, im Kopf behält, vermeidet die überwiegende Mehrheit aller Probleme mit eigenen Direktiven.

Merkmal x-Direktive (Alpine.directive) Magic Property ($x) Plugin (Alpine.plugin)
Registrierungs-API Alpine.directive(name, cb) Alpine.magic(name, cb) Alpine.plugin(cb)
Nutzung im Markup x-name="..." als HTML-Attribut $name innerhalb eines Ausdrucks beliebig, bündelt mehrere Erweiterungen
Direkter DOM-Zugriff Ja, über das el-Argument Nur indirekt, über das el-Argument im Callback Abhängig vom Inhalt des Plugins
Typischer Einsatzzweck eigenes Verhalten an einem Element, z.B. Tooltip wiederverwendbarer Wert oder Helfer, z.B. Zwischenablage npm-Paket mit mehreren Direktiven und Magics
Reaktivität über effect() explizit herstellbar wird bei jeder Auswertung neu bereitgestellt abhängig vom Inhalt des Plugins

Mironsoft

Alpine.js-Interaktivität für Hyvä-Frontends

Hyvä-Frontend, das mehr Interaktivität braucht, aber ohne React-Overhead?

Wir bauen interaktive Frontend-Komponenten für Hyvä-Themes mit Alpine.js, leichtgewichtig und ohne Build-Step-Komplexität, von einfachen Toggles bis zu komplexen Formular-Flows.

Custom-Komponenten

Interaktive Alpine.js-Komponenten für spezifische Shop-Anforderungen entwickeln.

Performance-Review

Bestehende Alpine.js-Implementierungen auf Reaktivitäts-Fallen und Performance prüfen.

Team-Schulung

Entwickler in Alpine.js-Patterns für Hyvä-Themes praxisnah einarbeiten.

10. Zusammenfassung

Alpine Directive API: das Wichtigste auf einen Blick

Direktive

Alpine.directive(name, callback) registriert ein neues x-Attribut mit vollem Zugriff auf das DOM-Element.

effect()

Macht eine Direktive reaktiv, der Callback läuft erneut, sobald sich ein gelesener Wert ändert.

cleanup()

Räumt Event-Listener und zusätzliche DOM-Knoten auf, sobald das Element aus dem DOM entfernt wird.

Timing

Registrierung muss vor Alpine.start() passieren, sonst bleibt die Direktive im HTML unverarbeitet.

11. FAQ: Alpine Directive API: das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen einer Alpine-Direktive und einer Magic Property?
Eine Direktive ist ein eigenes HTML-Attribut mit direktem Zugriff auf das DOM-Element und wird über Alpine.directive() registriert. Eine Magic Property ist dagegen ein Wert oder eine Funktion, die innerhalb eines Ausdrucks über ein Dollarzeichen aufgerufen wird, etwa $clipboard, und hat keinen eigenen Platz im Markup.
2Wo muss Alpine.directive() aufgerufen werden?
Immer vor Alpine.start(), am besten innerhalb eines alpine:init Event Listeners. Wird die Direktive erst danach registriert, bleibt sie für bereits gerenderte Elemente ein unverarbeitetes, wirkungsloses Attribut.
3Wofür wird cleanup() in einer eigenen Direktive gebraucht?
cleanup() registriert eine Funktion, die automatisch läuft, sobald das Element aus dem DOM entfernt wird, etwa durch x-if oder x-for. Sie sollte alle zusätzlich erzeugten DOM-Knoten und Event-Listener entfernen, damit keine Memory Leaks entstehen.
4Warum reicht evaluate() allein für reaktive Direktiven nicht aus?
evaluate() wertet den Ausdruck nur einmalig aus und merkt sich keine Abhängigkeiten. Für eine Direktive, die auf spätere Änderungen reagieren soll, muss der Ausdruck stattdessen innerhalb von effect() ausgewertet werden, damit Alpine die gelesenen reaktiven Properties nachverfolgt.
5Wie werden mehrteilige Direktivennamen benannt?
Bei der Registrierung wird camelCase verwendet, etwa clickOutside, im HTML erwartet Alpine automatisch die entsprechende dash-case Schreibweise, also x-click-outside. Eine camelCase-Schreibweise im HTML wird nicht erkannt.
6Kann eine eigene Direktive vor eingebauten Direktiven wie x-bind ausgeführt werden?
Ja, über die Methoden before() und after(), die direkt an Alpine.directive() angehängt werden, etwa Alpine.directive('name', callback).before('bind'). Das steuert die Reihenfolge relativ zu den eingebauten Direktiven auf demselben Element.
7Was steht in value und was in modifiers?
value enthält den optionalen Teil hinter einem Doppelpunkt im Attributnamen, etwa top bei x-tooltip:top. modifiers ist ein Array aller Punkt-Modifikatoren, etwa delay und 500 bei x-tooltip.delay.500, und eignet sich für mehrere, oft boolesche Zusatzoptionen.
8Warum zeigt die Konsole keinen Fehler, wenn eine Direktive nicht funktioniert?
Alpine ignoriert unbekannte x-Attribute stillschweigend, statt einen Fehler zu werfen, weil es nicht zwischen einer noch nicht registrierten eigenen Direktive und einem beliebigen anderen HTML-Attribut unterscheiden kann. Deshalb hilft nur ein Blick in die Alpine DevTools oder gezieltes Logging beim Debuggen.
9Braucht jede eigene Direktive ein eigenes Plugin?
Nein. Ein Plugin ist nur die Verteilungseinheit für mehrere Erweiterungen als npm-Paket. Für eine einzelne Direktive innerhalb eines Projekts reicht ein direkter Aufruf von Alpine.directive() vollkommen aus, ohne den zusätzlichen Aufwand eines Plugins.
10Wie greift eine eigene Direktive auf das zugehörige DOM-Element zu?
Über das erste Argument des Callbacks, üblicherweise el genannt. Es ist ein normales DOM-Element, auf dem sich alle Standard-APIs wie addEventListener, classList oder appendChild direkt verwenden lassen.