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.
Inhaltsverzeichnis
- 1. Direktive, Magic Property oder Plugin: drei verschiedene Erweiterungspunkte
- 2. Grundsyntax: Alpine.directive() und der richtige Zeitpunkt der Registrierung
- 3. Die Parameter im Detail: el, Ausdrucks-Kontext und Utilities
- 4. Praxisbeispiel: eine vollständige x-tooltip-Direktive
- 5. Reaktivität mit effect(): warum ein einmaliges evaluate() oft nicht reicht
- 6. cleanup(): Event-Listener und DOM-Knoten sauber entfernen
- 7. modifiers und value praktisch nutzen: Konfiguration direkt im Attributnamen
- 8. Priorität gegenüber eingebauten Direktiven und Namenskonvention
- 9. Häufige Fehler und wie man sie in der Praxis findet
- 10. Zusammenfassung
- 11. FAQ
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.