x-data
Alpine
Alpine.js · Plugin-Entwicklung · Direktiven · Magic Properties
Alpine.js Custom Plugin erstellen
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.

18 Min. Lesezeit Alpine.plugin() · addDirective · addMagic · addStore · npm-Paket Alpine.js 3.x · Hyvä Themes · Magento 2

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.

11. FAQ: Alpine.js Custom Plugin erstellen

1Unterschied Alpine.data() vs. Plugin?
Alpine.data() registriert Komponentenlogik. Ein Plugin erweitert Alpine selbst mit Direktiven, Magic Properties und Stores – nicht an eine Komponenteninstanz gebunden.
2Wann muss Alpine.plugin() aufgerufen werden?
Vor Alpine.start(). Am sichersten im alpine:init-Event. Bei ESM direkt vor Alpine.start() im Initialisierungscode.
3evaluateLater vs. evaluate?
evaluate wertet einmalig aus. evaluateLater gibt eine Funktion zurück, die reaktiv auswertet. Für effect() immer evaluateLater verwenden.
4Konfiguration an Plugin übergeben?
Plugin als Factory-Funktion: function MeinPlugin(config) { return function(Alpine) { ... } }. Dann Alpine.plugin(MeinPlugin({ option: 'wert' })).
5Was passiert ohne cleanup()?
Memory Leaks: externe Bibliotheken, Event-Listener und Observer laufen weiter, auch wenn das Element aus dem DOM entfernt wurde.
6Plugin-Abhängigkeiten?
Abhängiges Plugin zuerst registrieren. Alpine.plugin(BasisPlugin); dann Alpine.plugin(ErweiterungsPlugin). Reihenfolge ist entscheidend.
7Wie funktionieren Modifier in Direktiven?
Das Binding-Objekt enthält das Array modifiers. Bei x-tooltip.bottom ist modifiers gleich ['bottom']. Per modifiers.includes() auswerten.
8Plugin ohne npm einbinden?
Ja, als globale Variable oder via alpine:init-Event. Für Hyvä in web/js/ ablegen und über Layout-XML oder requirejs-config.js einbinden.
9Wie testet man ein Alpine.js Plugin?
Playwright für E2E-Tests. @alpinejs/testing-utils für Unit-Tests. Interne Plugin-Funktionen lassen sich direkt testen, ohne Alpine-Runtime zu laden.
10Bestehenden Store im Plugin erweitern?
Alpine.store('name') liest den Store. Besser: eigenen Store erstellen statt fremde zu modifizieren. Verhindert unerwartete Seiteneffekte bei Plugin-Updates.