Direktiven, Magic Properties und Stores
Die Alpine.js Plugin-API ermöglicht es, eigene Direktiven wie x-tooltip, magische Properties wie $format und globale Stores zu registrieren – und diese Erweiterungen sauber gebündelt in jedem Alpine-Projekt wiederzuverwenden.
Inhaltsverzeichnis
- 1. Wozu ein Alpine.js Plugin statt Alpine.data()?
- 2. Die Alpine.js Plugin-API: Alpine.plugin() verstehen
- 3. Eigene Direktive mit addDirective erstellen
- 4. Magische Properties mit addMagic registrieren
- 5. Globale Stores via addStore einrichten
- 6. Plugin-Lifecycle: init, bind, cleanup
- 7. Praxisbeispiel: x-tooltip Plugin mit Popper.js
- 8. Plugin als npm-Paket veröffentlichen
- 9. Plugin-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Wozu ein Alpine.js Plugin statt Alpine.data()?
Alpine.js bietet mit Alpine.data() eine einfache Möglichkeit, wiederverwendbare Komponenten-Logik zu registrieren. Aber was, wenn du Verhalten auf DOM-Ebene hinzufügen willst, das nicht an eine Komponente gebunden ist? Was, wenn du eine neue Direktive wie x-tooltip oder x-clipboard brauchst, die an beliebige Elemente angehängt werden kann? Oder eine magische Property wie $format, die in jedem x-data-Kontext verfügbar ist? Genau dafür existiert die Alpine.js Plugin-API.
Ein Plugin ist im Kern eine Funktion, die Alpine als Argument erhält und dessen interne API nutzt, um Direktiven, magische Properties und Stores zu registrieren. Das Muster Alpine.plugin(MeinPlugin) führt diese Funktion aus und integriert alle Erweiterungen in die Alpine-Runtime. Der entscheidende Vorteil gegenüber losem JavaScript: Alles ist sauber gekapselt, tree-shakeable, testbar und npm-fähig. Offizielle Alpine.js Plugins wie @alpinejs/focus, @alpinejs/persist und @alpinejs/morph verwenden exakt dieselbe API, die auch für Custom Plugins verfügbar ist.
Für Hyvä-Themes und Magento 2 sind Custom Plugins besonders wertvoll: Statt projektspezifische Logik in einzelne .phtml-Dateien zu streuen, lässt sie sich in ein zentrales Plugin-Modul zusammenführen, das einmal geladen wird und überall verfügbar ist. Das verbessert die Wartbarkeit erheblich und reduziert die Wahrscheinlichkeit von inkonsistenten Implementierungen derselben Funktion an mehreren Stellen.
2. Die Alpine.js Plugin-API: Alpine.plugin() verstehen
Die Plugin-Funktion erhält das Alpine-Objekt und hat Zugriff auf alle internen Methoden: Alpine.addDirective(name, callback) registriert eine neue Direktive, Alpine.addMagic(name, callback) fügt eine magische Property hinzu, Alpine.addStore(name, object) erstellt einen reaktiven globalen Store. Zusätzlich gibt es Alpine.onBeforeComponentInitialized und Alpine.onComponentInitialized für Hooks in den Komponenten-Lifecycle.
Das Plugin muss vor Alpine.start() registriert werden. Bei der CDN-Variante bedeutet das, das Plugin-Script vor dem Alpine-Hauptscript zu laden oder in einem alpine:init-Event zu registrieren. Bei der npm-Variante wird Alpine.plugin(MeinPlugin) vor Alpine.start() aufgerufen. Bei mehreren Plugins ist die Reihenfolge wichtig, wenn Plugins voneinander abhängen – Abhängigkeiten müssen zuerst registriert werden.
// Plugin structure — the function receives Alpine as argument
function MironsoftPlugin(Alpine) {
// 1. Register a custom directive: x-autofocus
Alpine.addDirective('autofocus', (el, { expression, modifiers }, { effect, evaluate, cleanup }) => {
// Execute once on init — no expression needed
const delay = modifiers.includes('delay') ? 100 : 0;
const timer = setTimeout(() => {
el.focus();
}, delay);
// Cleanup function — called when element is removed from DOM
cleanup(() => clearTimeout(timer));
});
// 2. Register a magic property: $uid (unique ID generator)
Alpine.addMagic('uid', (el) => {
return (prefix = 'alpine') => {
if (!el._alpineUid) {
el._alpineUid = `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
}
return el._alpineUid;
};
});
// 3. Register a global reactive store: $store.theme
Alpine.addStore('theme', {
current: localStorage.getItem('theme') || 'light',
toggle() {
this.current = this.current === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', this.current);
},
get isDark() {
return this.current === 'dark';
}
});
}
// Register plugin before Alpine.start()
document.addEventListener('alpine:init', () => {
Alpine.plugin(MironsoftPlugin);
});
3. Eigene Direktive mit addDirective erstellen
Der Callback von addDirective erhält drei Argumente: das DOM-Element el, ein Objekt mit expression, value und modifiers, und ein Utilities-Objekt mit effect, evaluate, evaluateLater und cleanup. Das expression-Property enthält die rohe String-Entsprechung des Attributwerts – also was nach dem Doppelpunkt steht, bei x-tooltip="'Hallo'" wäre es 'Hallo'. Die Funktion evaluate(expression) wertet diesen Ausdruck im Kontext der umgebenden Alpine-Komponente aus.
Für reaktive Direktiven ist effect(() => { ... }) der richtige Baustein. Alles innerhalb von effect wird neu ausgeführt, wenn sich Daten ändern, von denen es abhängt – genau wie x-text oder x-bind intern funktionieren. Die cleanup-Funktion registriert Code, der ausgeführt wird, wenn das Element aus dem DOM entfernt wird – zum Beispiel Event-Listener entfernen, Timer löschen oder externe Bibliothekinstanzen zerstören. Ohne Cleanup entstehen Memory Leaks, besonders in Single-Page-Applications oder bei dynamisch gerenderten Hyvä-Komponenten.
4. Magische Properties mit addMagic registrieren
Magische Properties sind unter dem Prefix $ in jedem Alpine-Komponenten-Kontext verfügbar. Alpine selbst stellt $el, $refs, $store, $dispatch, $nextTick, $watch, $root und $data bereit. Mit addMagic kann man eigene Properties hinzufügen, die denselben Zugang erhalten. Der Callback erhält das aktuelle DOM-Element und kann einen Wert, eine Funktion oder ein Objekt zurückgeben.
Ein typisches Beispiel für eine nützliche magische Property: $format für Zahlen und Datumsformatierung. Statt in jeder Komponente eine Formatierungsfunktion zu definieren, wird sie einmal im Plugin registriert und ist dann überall als $format.currency(price) oder $format.date(timestamp) verfügbar. Das ist besonders in Magento 2 / Hyvä wertvoll, wo Preise, Mengen und Datumsangaben in vielen Komponenten formatiert werden müssen und die Locale-Einstellung zentral bekannt ist.
5. Globale Stores via addStore einrichten
Globale Stores sind der Alpine.js-Mechanismus für anwendungsweiten, reaktiven Zustand. Mit Alpine.addStore('cart', { items: [], total: 0, addItem(item) { ... } }) wird ein Store erstellt, auf den alle Komponenten über $store.cart zugreifen können. Änderungen an Store-Properties lösen reaktive Updates in allen Komponenten aus, die diese Properties lesen – ohne manuelle Event-Emitter oder Pub/Sub-Systeme. Stores können Getter, Setters und Methoden enthalten und sind vollständig reaktiv.
In Plugins kann addStore genutzt werden, um Plugin-interne Konfiguration zentral zu verwalten. Beispielsweise kann ein Toast-Benachrichtigungsplugin einen Store mit der Benachrichtigungswarteschlange verwalten, auf den sowohl die Trigger-Methode ($notify.success('...')) als auch die Render-Komponente (x-data="{ get notifications() { return $store.notifications.queue } }") zugreifen. Das entkoppelt das Auslösen von Benachrichtigungen von der Darstellungslogik vollständig.
// Advanced plugin: $format magic property + $notify magic method
function MironsoftFormatPlugin(Alpine) {
// $format — locale-aware formatting utilities
Alpine.addMagic('format', () => {
const locale = document.documentElement.lang || 'de-DE';
const currency = document.documentElement.dataset.currency || 'EUR';
return {
currency(value, options = {}) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2,
...options
}).format(value);
},
number(value, decimals = 0) {
return new Intl.NumberFormat(locale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(value);
},
date(value, style = 'medium') {
const date = value instanceof Date ? value : new Date(value);
return new Intl.DateTimeFormat(locale, { dateStyle: style }).format(date);
},
relative(value) {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diff = (new Date(value) - Date.now()) / 1000;
const units = [
[60, 'second'], [3600, 'minute'], [86400, 'hour'], [Infinity, 'day']
];
for (const [limit, unit] of units) {
if (Math.abs(diff) < limit) {
return rtf.format(Math.round(diff / (limit / 60)), unit);
}
}
}
};
});
}
// Usage in template: x-text="$format.currency(product.price)"
// Usage: x-text="$format.date(order.createdAt)"
6. Plugin-Lifecycle: init, bind, cleanup
Alpine.js Direktiven haben einen klar definierten Lifecycle. Der Callback von addDirective wird ausgeführt, wenn das Element initialisiert wird. Reaktive Effekte über effect() werden danach bei jeder Datenänderung erneut ausgeführt. Der cleanup-Callback wird ausgeführt, wenn das Element aus dem DOM entfernt wird oder wenn Alpine das Element neu initialisiert (z.B. nach einem x-if-Toggle). Dieser dreistufige Lifecycle spiegelt das wider, was in React als mount, update und unmount bekannt ist.
Ein häufiger Fehler in Custom Plugins: externe Bibliotheken (Popper.js, Chart.js, Flatpickr) werden in der Direktive initialisiert, aber nicht in der cleanup-Funktion zerstört. Das führt zu Memory Leaks und unerwartetem Verhalten bei dynamisch gerenderten Elementen. Jede externe Bibliothek, die eine destroy-Methode anbietet, muss in cleanup aufgerufen werden. Gleiches gilt für Event-Listener, ResizeObserver, MutationObserver und Intersection Observer – alle müssen in cleanup sauber abgemeldet werden.
7. Praxisbeispiel: x-tooltip Plugin mit Popper.js
Ein Tooltip-Plugin zeigt alle Aspekte der Plugin-API in Kombination. Die Direktive x-tooltip akzeptiert einen reaktiven Ausdruck für den Tooltip-Text, erstellt dynamisch ein Tooltip-Element, positioniert es mit Popper.js und zeigt es beim Hover oder Fokus des Elements an. Der Tooltip-Text wird reaktiv via effect aktualisiert, wenn sich der Ausdruck ändert. Beim Cleanup wird die Popper-Instanz zerstört und das Tooltip-Element aus dem DOM entfernt.
Die Verwendung im Template ist dann denkbar einfach: <button x-tooltip="tooltipText">Hover mich</button>. Modifiers wie x-tooltip.bottom oder x-tooltip.click steuern Placement und Trigger-Verhalten. Diese API ist wesentlich sauberer als Inline-JavaScript im Template und kapselt die Popper.js-Abhängigkeit vollständig im Plugin – Templates sind frei von bibliotheksspezifischen Details. Das Plugin kann mit einer Konfigurationsoption aus dem Plugin-Aufruf parametrisiert werden: Alpine.plugin(TooltipPlugin({ defaultPlacement: 'top' })).
// x-tooltip plugin — wraps Popper.js with clean Alpine directive
function TooltipPlugin(config = {}) {
return function (Alpine) {
Alpine.addDirective('tooltip', (el, { expression, modifiers }, { effect, evaluateLater, cleanup }) => {
// Evaluate expression reactively
const getText = evaluateLater(expression);
// Create tooltip DOM element
const tooltip = document.createElement('div');
tooltip.setAttribute('role', 'tooltip');
tooltip.className = 'tooltip-popup bg-slate-900 text-white text-xs rounded px-2 py-1 pointer-events-none';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
// Determine placement from modifiers or config
const placement = modifiers.find(m =>
['top', 'bottom', 'left', 'right'].includes(m)
) || config.defaultPlacement || 'top';
// Init Popper (assume Popper.js is loaded separately)
let popperInstance = null;
const initPopper = () => {
if (window.Popper) {
popperInstance = window.Popper.createPopper(el, tooltip, {
placement,
modifiers: [{ name: 'offset', options: { offset: [0, 8] } }]
});
}
};
// Update tooltip text reactively
effect(() => {
getText(value => { tooltip.textContent = value; });
});
// Show/hide handlers
const show = () => { tooltip.style.display = 'block'; initPopper(); };
const hide = () => { tooltip.style.display = 'none'; };
el.addEventListener('mouseenter', show);
el.addEventListener('mouseleave', hide);
el.addEventListener('focusin', show);
el.addEventListener('focusout', hide);
// Cleanup: destroy Popper and remove tooltip element
cleanup(() => {
popperInstance?.destroy();
tooltip.remove();
el.removeEventListener('mouseenter', show);
el.removeEventListener('mouseleave', hide);
el.removeEventListener('focusin', show);
el.removeEventListener('focusout', hide);
});
});
};
}
Alpine.plugin(TooltipPlugin({ defaultPlacement: 'bottom' }));
8. Plugin als npm-Paket veröffentlichen
Ein gut strukturiertes Alpine.js Plugin lässt sich problemlos als npm-Paket veröffentlichen, damit es projektübergreifend genutzt werden kann. Die Paketstruktur ist einfach: Eine Hauptdatei exportiert die Plugin-Funktion als Default-Export. Eine package.json definiert main für CommonJS, module für ES Modules und exports für moderne Node.js-Versionen. Peer Dependencies sollten alpinejs als Peer Dependency deklarieren, um sicherzustellen, dass das Plugin mit der im Projekt verwendeten Alpine-Version kompatibel ist.
Für Hyvä Themes und Magento 2 kann das Plugin auch über Composer als PHP-Paket verteilt werden, wenn es statische Web-Assets enthält, die über das Magento Asset-System deployed werden müssen. Alternativ wird das npm-Paket über die package.json des Hyvä-Themes als Abhängigkeit eingebunden und beim Build-Prozess in das Tailwind-Bundle integriert. Wichtig ist dabei, dass das Plugin korrekt tree-shakeablen ESM-Code produziert, damit ungenutzte Features nicht ins Bundle wandern.
9. Plugin-Strategien im Vergleich
Alpine.js bietet mehrere Wege, Logik zu kapseln und wiederzuverwenden. Die Wahl der richtigen Strategie hängt vom Anwendungsfall ab.
| Strategie | Anwendungsfall | API | Scope |
|---|---|---|---|
| Alpine.data() | Wiederverwendbare Komponentenlogik | x-data="meinKomponent()" |
Komponenteninstanz |
| addDirective | DOM-Verhalten an beliebige Elemente | x-meine-direktive="..." |
Element + Reaktivität |
| addMagic | Utility-Funktionen überall verfügbar | $meinHelper.methode() |
Alle Komponenten |
| addStore | Geteilter reaktiver Zustand | $store.meinStore.prop |
Global, alle Komponenten |
| Alpine.plugin() | Bundle aus Direktiven + Magic + Stores | Alpine.plugin(MeinPlugin) |
Gesamte Alpine-Instanz |
Die Entscheidungsregel ist pragmatisch: Wenn die Logik an eine Komponente gebunden ist, kommt Alpine.data(). Wenn sie DOM-Elemente direkt manipuliert (externe Bibliotheken, Events, Lifecycle), kommt eine Direktive. Wenn sie eine Utility-Funktion ist, die in Expressions genutzt wird, kommt addMagic. Wenn Zustand zwischen unverbundenen Komponenten geteilt werden soll, kommt ein Store. Ein Plugin bündelt all das und macht die Erweiterung als eine Einheit installierbar.
Mironsoft
Alpine.js Plugin-Entwicklung, Hyvä Themes und Magento 2 Frontend
Custom Alpine.js Plugin für dein Projekt?
Wir entwickeln maßgeschneiderte Alpine.js Plugins für Hyvä Themes und Magento 2 – von der Direktive über Magic Properties bis zum vollständig getesteten npm-Paket.
Plugin-Entwicklung
Direktiven, Magic Properties und Stores nach Maß für Hyvä und Magento 2
Code-Review
Bestehende Alpine.js Plugins analysieren: Lifecycle, Memory Leaks, Reaktivität
npm-Paketierung
Plugin als wiederverwendbares ESM-Paket vorbereiten und veröffentlichen
10. Zusammenfassung
Die Alpine.js Plugin-API ermöglicht es, Alpine mit eigenen Direktiven, magischen Properties und globalen Stores zu erweitern, ohne die Alpine-Internals zu patchen oder externe Abhängigkeiten zu umgehen. Mit Alpine.addDirective() entstehen DOM-native Verhaltenserweiterungen wie x-tooltip oder x-autofocus. Mit Alpine.addMagic() werden Utility-Funktionen wie $format in alle Komponenten-Kontexte integriert. Mit Alpine.addStore() entsteht geteilter, reaktiver Zustand ohne Boilerplate.
Der entscheidende Unterschied zu Alpine.data(): Plugins sind kompositionsfreundlich, tree-shakeable und npm-fähig. Sie können alle drei API-Bausteine in einem Bundle kombinieren und als einheitliche Erweiterung installiert werden. Der Lifecycle über effect und cleanup in Direktiven stellt sicher, dass externe Bibliotheken korrekt initialisiert und zerstört werden – ohne Memory Leaks. Für Hyvä Themes und Magento 2 ist das Plugin-Pattern die sauberste Methode, projektweites Alpine.js-Verhalten zu standardisieren.
Alpine.js Custom Plugin — Das Wichtigste auf einen Blick
Plugin registrieren
Alpine.plugin(MeinPlugin) vor Alpine.start() aufrufen. Die Plugin-Funktion erhält Alpine und registriert Direktiven, Magic und Stores.
Direktiven
Alpine.addDirective('name', callback). Callback erhält el, Binding-Objekt und Utilities. effect für Reaktivität, cleanup für Memory-Management.
Magic Properties
Alpine.addMagic('name', callback) — überall als $name verfügbar. Ideal für Formatierungsfunktionen, IDs und Utilities, die in Expressions genutzt werden.
Cleanup ist Pflicht
Jede externe Bibliothek, Observer und Event-Listener muss in cleanup() zerstört werden. Ohne Cleanup entstehen Memory Leaks bei dynamischen Komponenten.