Alpine.js in Hyvä Theme: Interaktive Magento 2 Komponenten ohne jQuery erklärt

· Lesezeit: ca. 20 Minuten · Kategorien: Magento 2, Hyvä Theme, JavaScript

 
x-data
Alpine
JavaScript & Frontend-Architektur

Alpine.js in Hyvä Theme
vollständig erklärt

x-data, x-show, x-for, $store und das Komponenten-Pattern – alle Alpine.js-Konzepte praxisnah erklärt für die Magento 2 Hyvä Theme Entwicklung ohne jQuery und Knockout.js.

⏱ 20 Min. Lesezeit Alpine.js ???? Hyvä Theme ???? Tutorial

Warum Alpine.js das Frontend von Hyvä Theme antreibt

Wer von einem klassischen Magento 2 Luma-Theme auf Hyvä umsteigt, begegnet unweigerlich Alpine.js. Kein Knockout.js mehr, kein RequireJS, keine UI-Komponenten aus dem Magento-Core. Stattdessen ein schlankes, deklaratives JavaScript-Framework, das direkt im HTML-Markup lebt – ähnlich wie Tailwind CSS für das Styling.

Alpine.js wurde 2019 von Caleb Porzio veröffentlicht und verfolgt einen klaren Ansatz: Interaktivität soll dort definiert werden, wo sie sichtbar ist – im HTML, nicht in einer separaten JavaScript-Datei. Das Framework ist 15 KB groß, hat keine Abhängigkeiten und lässt sich in fünf Minuten erlernen. Für das Hyvä Theme ist es die perfekte Ergänzung zu Tailwind CSS: Tailwind kümmert sich um das visuelle Erscheinungsbild, Alpine.js um das interaktive Verhalten.

Dieses Tutorial erklärt alle relevanten Alpine.js-Konzepte für die Hyvä Theme Entwicklung: von den Grunddirektiven über das Komponenten-Pattern bis hin zur Integration mit der Magento 2 REST-API und dem Hyvä-eigenen Event-System. Die Beispiele sind direkt aus der Magento 2 Praxis entnommen und funktionieren so in realen Hyvä-Projekten.

1. Was ist Alpine.js?

Alpine.js ist ein leichtgewichtiges JavaScript-Framework, das reaktive Datenbindung und deklarative DOM-Manipulation direkt in HTML-Attributen ermöglicht. Es benötigt keine Build-Toolchain, kein Bundling und keine zusätzlichen Abhängigkeiten. Das gesamte Framework ist eine einzige JavaScript-Datei, die entweder als <script>-Tag eingebunden oder über NPM installiert wird.

Die philosophische Verwandtschaft mit Vue.js ist kein Zufall – Caleb Porzio hat Alpine.js explizit als „Vue für Menschen, die kein Build-System wollen" bezeichnet. Wer Vue.js kennt, wird die Direktiven sofort wiedererkennen: x-data entspricht data(), x-bind entspricht v-bind, x-on entspricht v-on. Der entscheidende Unterschied: Alpine.js lebt komplett im HTML.

Alpine.js im Hyvä Theme einbinden

Im Hyvä Theme ist Alpine.js bereits vollständig integriert. Es wird über das Hyvä-eigene Template-System geladen und steht auf jeder Seite zur Verfügung. Du musst Alpine.js nicht manuell einbinden – es läuft im Kontext jedes .phtml-Templates automatisch.


<!-- In einem Hyvä-Layout: Alpine.js ist bereits global verfügbar -->
<!-- Kein require(['alpineJs'], ...) nötig — kein RequireJS! -->

<!-- Einfaches Beispiel direkt in einem phtml: -->
<div x-data="{ open: false }">
    <button @click="open = !open">Toggle</button>
    <div x-show="open">Hallo Welt</div>
</div>
  

Das ist der fundamentale Unterschied zum alten Magento-Luma-Theme: Keine RequireJS-Konfiguration, keine data-mage-init-Attribute, keine UI-Komponenten mit tief verschachtelten Knockout-Bindings. Alpine.js-Code ist lesbar, wartbar und liegt direkt dort, wo er wirkt.

2. Die Grunddirektiven

Alpine.js kennt 15 Direktiven, die als HTML-Attribute verwendet werden. Die wichtigsten sieben sind das Fundament jeder Alpine.js-Entwicklung im Hyvä Theme.

x-data – Der reaktive Zustand

x-data ist die Wurzel jeder Alpine.js-Komponente. Es definiert den reaktiven Zustand als JavaScript-Objekt. Alle Direktivn innerhalb des Elements haben Zugriff auf diese Daten. Ändert sich ein Datenwert, aktualisiert Alpine.js den DOM automatisch.


<!-- Inline-Definition: für einfache Fälle -->
<div x-data="{ count: 0, name: 'Hyvä' }">
    <p x-text="name"></p>
    <button @click="count++">Klicks: <span x-text="count"></span></button>
</div>

<!-- Funktions-Referenz: für komplexe Komponenten (empfohlen) -->
<div x-data="productGallery()">
    <!-- ... -->
</div>
<script>
function productGallery() {
    return {
        activeImage: 0,
        images: [],
        init() {
            // wird beim Initialisieren automatisch aufgerufen
        }
    }
}
</script>
  

x-show und x-if – Sichtbarkeit steuern

x-show und x-if steuern beide, ob ein Element sichtbar ist – aber auf grundlegend unterschiedliche Weise. x-show schaltet display: none per CSS um, das Element bleibt im DOM. x-if entfernt das Element vollständig aus dem DOM und fügt es wieder ein.


<!-- x-show: Element bleibt im DOM, nur CSS-Visibility ändert sich -->
<!-- Gut für: häufige Toggles, Elemente mit Animationen -->
<div x-data="{ menuOpen: false }">
    <button @click="menuOpen = !menuOpen">Menü</button>
    <nav x-show="menuOpen" x-transition>
        <!-- Menü-Items -->
    </nav>
</div>

<!-- x-if: Element wird aus dem DOM entfernt/hinzugefügt -->
<!-- Gut für: bedingte Inhalte die selten angezeigt werden -->
<template x-if="isLoggedIn">
    <div class="customer-dashboard">
        <!-- Wird nur gerendert wenn isLoggedIn === true -->
    </div>
</template>

<!-- x-transition: sanfte Ein-/Ausblendanimationen mit x-show -->
<div x-show="open"
     x-transition:enter="transition ease-out duration-200"
     x-transition:enter-start="opacity-0 translate-y-1"
     x-transition:enter-end="opacity-100 translate-y-0"
     x-transition:leave="transition ease-in duration-150"
     x-transition:leave-start="opacity-100 translate-y-0"
     x-transition:leave-end="opacity-0 translate-y-1">
    Dropdown-Inhalt
</div>
  

x-bind – Attribute dynamisch setzen

x-bind bindet HTML-Attribute an Alpine.js-Datenwerte. Die Kurzschreibweise ist der Doppelpunkt :. Damit lassen sich CSS-Klassen, ARIA-Attribute, Quellen für Bilder und alle anderen HTML-Attribute reaktiv steuern.


<div x-data="{ isActive: false, imageSrc: '/media/product.jpg', rating: 4 }">

    <!-- Einfaches Attribut-Binding -->
    <img :src="imageSrc" :alt="'Produktbild ' + rating">

    <!-- Klassen-Binding: Objekt-Notation -->
    <button :class="{ 'bg-brand-red text-white': isActive, 'bg-slate-100': !isActive }"
            @click="isActive = !isActive">
        Toggle
    </button>

    <!-- Klassen-Binding: Array-Notation -->
    <div :class="['rounded-xl p-4', isActive ? 'shadow-lg' : 'shadow-sm']">
        Inhalt
    </div>

    <!-- ARIA-Attribute für Barrierefreiheit -->
    <button :aria-expanded="isActive"
            :aria-label="isActive ? 'Schließen' : 'Öffnen'"
            @click="isActive = !isActive">
        Toggle
    </button>

</div>
  

x-on – Events abfangen

x-on registriert Event-Listener direkt im HTML. Die Kurzschreibweise ist das @-Zeichen. Alpine.js unterstützt alle nativen Browser-Events sowie benutzerdefinierte Events über $dispatch.


<div x-data="{ message: '' }">

    <!-- Klick-Event (Kurzschreibweise @click) -->
    <button @click="message = 'Geklickt!'">Klick mich</button>

    <!-- Tastatur-Events mit Modifier -->
    <input @keydown.enter="submitSearch()"
           @keydown.escape="clearSearch()"
           x-model="searchQuery">

    <!-- Window-Events (z.B. für Scroll-Handler) -->
    <div @scroll.window="handleScroll()"
         @resize.window.debounce.300="updateLayout()">
    </div>

    <!-- Benutzerdefiniertes Event (von Hyvä dispatched) -->
    <div @product-added-to-cart.window="showCartNotification($event.detail)">
    </div>

    <!-- Modifier: .prevent, .stop, .once, .self -->
    <form @submit.prevent="handleSubmit()">
        <button type="submit">Absenden</button>
    </form>

</div>
  

3. Weitere Direktiven und Magic Properties

x-model – Zwei-Wege-Datenbindung

x-model synchronisiert den Wert eines Formularfeldes mit einer Alpine.js-Variable in beide Richtungen. Ändert der Nutzer das Feld, aktualisiert sich die Variable – und umgekehrt.


<div x-data="{ qty: 1, selectedSize: '', search: '' }">

    <!-- Zahl-Input mit Mindest-/Maximalwert -->
    <input type="number" x-model.number="qty" min="1" max="99">
    <p>Menge: <span x-text="qty"></span></p>

    <!-- Select-Dropdown -->
    <select x-model="selectedSize">
        <option value="">Größe wählen</option>
        <option value="S">S</option>
        <option value="M">M</option>
        <option value="L">L</option>
    </select>

    <!-- Debounced Search (wartet 300ms nach letzter Eingabe) -->
    <input type="search" x-model.debounce.300ms="search"
           @input="fetchSuggestions(search)">

</div>
  

x-for – Listen rendern

x-for iteriert über Arrays und rendert für jeden Eintrag einen DOM-Knoten. Es funktioniert ausschließlich mit dem <template>-Tag als Container.


<div x-data="{ products: [] }" x-init="products = await fetchProducts()">

    <!-- Einfache Liste -->
    <template x-for="product in products" :key="product.id">
        <div class="product-card">
            <img :src="product.thumbnail" :alt="product.name">
            <p x-text="product.name"></p>
            <p x-text="'€ ' + product.price.toFixed(2)"></p>
        </div>
    </template>

    <!-- Mit Index -->
    <template x-for="(item, index) in cartItems" :key="item.sku">
        <div class="flex items-center gap-4">
            <span x-text="index + 1" class="text-slate-400 text-sm"></span>
            <span x-text="item.name"></span>
            <span x-text="item.qty + ' x'" class="text-slate-500"></span>
        </div>
    </template>

</div>
  

x-text, x-html und x-ref

x-text setzt den Textinhalt eines Elements (automatisch HTML-escaped), x-html setzt rohen HTML-Inhalt (Vorsicht bei user-generated content!). x-ref erstellt eine Referenz auf ein DOM-Element, das dann über $refs zugänglich ist.


<div x-data="{ title: '<b>Hyvä</b> Theme', price: 49.99 }">

    <!-- x-text: HTML-Sonderzeichen werden escaped -->
    <p x-text="title"></p>
    <!-- Ausgabe: "<b>Hyvä</b> Theme" (sicher) -->

    <!-- x-html: HTML wird gerendert — nur für vertrauenswürdige Quellen! -->
    <div x-html="title"></div>
    <!-- Ausgabe: "Hyvä Theme" (fett) -->

    <!-- x-ref: DOM-Referenz für direkten Zugriff -->
    <input x-ref="priceInput" type="number" :value="price">
    <button @click="$refs.priceInput.focus()">
        Feld fokussieren
    </button>

</div>
  

Magic Properties: $el, $refs, $event, $dispatch, $nextTick, $watch

Alpine.js stellt innerhalb von Direktiven sogenannte Magic Properties bereit – spezielle Variablen, die automatisch verfügbar sind.


// $el: Das aktuelle DOM-Element
@click="$el.classList.toggle('active')"

// $refs: Zugriff auf mit x-ref markierte Elemente
@click="$refs.modal.showModal()"

// $event: Das native Browser-Event-Objekt
@click="handleClick($event.target.dataset.id)"

// $dispatch: Benutzerdefiniertes Event auslösen
@click="$dispatch('cart-updated', { itemCount: 3 })"

// $nextTick: Wartet auf den nächsten DOM-Update-Zyklus
@click="isOpen = true; $nextTick(() => $refs.input.focus())"

// $watch: Reagiert auf Änderungen einer Variable
x-init="$watch('qty', value => calculateTotal(value))"

// $store: Zugriff auf den globalen Alpine.js Store
x-text="$store.cart.itemCount"
  

4. Alpine.js in Hyvä phtml-Templates

Im Hyvä Theme werden Alpine.js-Komponenten direkt in .phtml-Templates geschrieben. PHP liefert die Daten, Alpine.js übernimmt die Interaktivität. Diese Kombination ist das Herzstück des Hyvä-Entwicklungsmodells.

PHP-Daten an Alpine.js übergeben

Der häufigste Anwendungsfall: Ein PHP-ViewModel liefert Produktdaten, Alpine.js rendert sie interaktiv. Die Übergabe erfolgt über x-data mit einem PHP-generierten JSON-Objekt.


<?php
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Mironsoft\Catalog\ViewModel\ProductOptions $viewModel */
$viewModel = $block->getData('view_model');
$product = $block->getProduct();

// Produktdaten als JSON für Alpine.js vorbereiten
$productData = [
    'id'    => (int) $product->getId(),
    'name'  => $block->escapeHtml($product->getName()),
    'price' => (float) $product->getFinalPrice(),
    'images' => $viewModel->getGalleryImages($product),
    'stock'  => $viewModel->isInStock($product),
];
?>

<div x-data="productView(<?= $block->escapeHtmlAttr(json_encode($productData)) ?>)"
     class="product-view-wrapper">

    <!-- Produktname (reaktiv) -->
    <h1 x-text="product.name" class="text-2xl font-bold text-slate-900"></h1>

    <!-- Preis -->
    <p class="text-3xl font-bold text-brand-red">
        € <span x-text="product.price.toFixed(2)"></span>
    </p>

    <!-- Bildergalerie -->
    <div class="gallery-wrapper">
        <img :src="product.images[activeImage].url"
             :alt="product.images[activeImage].label"
             class="w-full rounded-xl">
        <div class="flex gap-2 mt-4">
            <template x-for="(img, idx) in product.images" :key="idx">
                <button @click="activeImage = idx"
                        :class="{ 'ring-2 ring-brand-red': activeImage === idx }"
                        class="w-16 h-16 rounded-lg overflow-hidden">
                    <img :src="img.thumbnail" :alt="img.label" class="w-full h-full object-cover">
                </button>
            </template>
        </div>
    </div>

    <!-- In-den-Warenkorb Button -->
    <button @click="addToCart()"
            :disabled="!product.stock || loading"
            :class="{ 'opacity-50 cursor-not-allowed': !product.stock || loading }"
            class="btn-primary w-full mt-6">
        <span x-show="!loading">In den Warenkorb</span>
        <span x-show="loading">Wird hinzugefügt...</span>
    </button>

</div>
  

Wichtig beim Übergeben von PHP-Daten an Alpine.js: Immer $block->escapeHtmlAttr() für den x-data-Wert verwenden, da er im HTML-Attribut-Kontext steht. Der JSON-encode übernimmt das korrekte Escaping der Inhalte.

5. Das Komponenten-Pattern für Hyvä Theme

Für komplexe Alpine.js-Komponenten empfiehlt sich das Auslagern der Logik in eine JavaScript-Funktion. Dieses Muster hält das HTML sauber und ermöglicht die Wiederverwendung derselben Komponente an mehreren Stellen.

Komponente als JavaScript-Funktion


// Pattern: Alpine.js Komponente als benannte Funktion
// Platzierung: am Ende des .phtml-Templates in einem <script>-Tag

function productView(initialData) {
    return {
        product: initialData,
        activeImage: 0,
        qty: 1,
        loading: false,

        // x-init wird automatisch beim Mount aufgerufen
        init() {
            // Warenkorb-Updates abhören
            this.$watch('qty', qty => {
                if (qty < 1) this.qty = 1;
                if (qty > 99) this.qty = 99;
            });
        },

        // Berechnete Werte (wie Vue computed)
        get formattedPrice() {
            return '€ ' + this.product.price.toFixed(2);
        },

        get canAddToCart() {
            return this.product.stock && !this.loading && this.qty > 0;
        },

        // Methoden
        setActiveImage(index) {
            this.activeImage = Math.max(0, Math.min(index, this.product.images.length - 1));
        },

        async addToCart() {
            if (!this.canAddToCart) return;
            this.loading = true;
            try {
                const response = await fetch('/rest/V1/carts/mine/items', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': 'Bearer ' + this.$store.customer.token,
                    },
                    body: JSON.stringify({
                        cartItem: {
                            sku: this.product.sku,
                            qty: this.qty,
                            quote_id: this.$store.cart.quoteId,
                        }
                    })
                });
                if (!response.ok) throw new Error('Cart error');
                this.$dispatch('product-added-to-cart', { product: this.product, qty: this.qty });
            } catch (e) {
                this.$dispatch('cart-error', { message: e.message });
            } finally {
                this.loading = false;
            }
        }
    };
}
  

Komponenten mit Alpine.data() global registrieren

Wenn eine Komponente auf mehreren Seiten benötigt wird, lässt sie sich mit Alpine.data() global registrieren. Im Hyvä Theme erfolgt dies typischerweise in einer eigenen JavaScript-Datei, die über das Layout geladen wird.


// Datei: web/js/components/mini-cart.js
// Eingebunden via: layout/default.xml <script src="Vendor_Theme::js/components/mini-cart.js"/>

document.addEventListener('alpine:init', () => {
    Alpine.data('miniCart', () => ({
        open: false,
        items: [],
        total: 0,
        itemCount: 0,

        init() {
            // Beim ersten Laden: Warenkorb aus dem Store lesen
            this.syncWithStore();
            // Event-Listener für Warenkorb-Updates
            window.addEventListener('product-added-to-cart', () => this.syncWithStore());
        },

        syncWithStore() {
            this.items     = this.$store.cart.items;
            this.total     = this.$store.cart.total;
            this.itemCount = this.$store.cart.itemCount;
        },

        async removeItem(itemId) {
            await fetch(`/rest/V1/carts/mine/items/${itemId}`, { method: 'DELETE', ... });
            this.$store.cart.removeItem(itemId);
            this.syncWithStore();
        }
    }));
});

// Verwendung im phtml-Template:
// <div x-data="miniCart()">
  

6. Globaler State mit $store

Alpine.js bietet mit Alpine.store() einen globalen, reaktiven Datenspeicher. Alle Komponenten auf der Seite können lesend und schreibend auf denselben Store zugreifen. Für Magento 2 ist das ideal, um Warenkorb-Status, Kundendaten und andere seitenweite Zustände konsistent zu halten.


// Store-Initialisierung — einmal beim Seitenlade
document.addEventListener('alpine:init', () => {

    // Warenkorb-Store
    Alpine.store('cart', {
        quoteId: null,
        items: [],
        itemCount: 0,
        total: 0,
        isLoading: false,

        async init() {
            this.isLoading = true;
            try {
                const res = await fetch('/rest/V1/carts/mine', {
                    headers: { 'Authorization': 'Bearer ' + Alpine.store('customer').token }
                });
                const data = await res.json();
                this.quoteId   = data.id;
                this.items     = data.items || [];
                this.itemCount = this.items.reduce((sum, i) => sum + i.qty, 0);
                this.total     = parseFloat(data.base_grand_total || 0);
            } catch (e) {
                console.error('Cart load failed', e);
            } finally {
                this.isLoading = false;
            }
        },

        addItem(item) {
            const existing = this.items.find(i => i.sku === item.sku);
            if (existing) {
                existing.qty += item.qty;
            } else {
                this.items.push(item);
            }
            this.itemCount = this.items.reduce((sum, i) => sum + i.qty, 0);
        },

        removeItem(itemId) {
            this.items = this.items.filter(i => i.item_id !== itemId);
            this.itemCount = this.items.reduce((sum, i) => sum + i.qty, 0);
        }
    });

    // Kunden-Store
    Alpine.store('customer', {
        isLoggedIn: false,
        token: null,
        name: '',
        init() {
            // Token aus Cookie oder PHP-Block lesen
            this.token = document.cookie.match(/token=([^;]+)/)?.[1] || null;
            this.isLoggedIn = !!this.token;
        }
    });
});

// Verwendung in beliebigen Komponenten:
// <span x-text="$store.cart.itemCount"></span>
// <div x-show="$store.customer.isLoggedIn">Willkommen!</div>
  

7. Alpine.js mit der Magento 2 REST-API

Alpine.js kann direkt mit der Magento 2 REST-API kommunizieren – kein jQuery, kein RequireJS nötig. Der native fetch-API ersetzt vollständig das alte $.ajax()-Pattern aus Luma.

Produktvorschau beim Hover laden


function productQuickView(sku) {
    return {
        product: null,
        loading: false,
        error: null,

        async loadProduct() {
            if (this.product) return; // bereits geladen
            this.loading = true;
            this.error   = null;
            try {
                // Magento REST API: Produkt nach SKU
                const res = await fetch(`/rest/V1/products/${encodeURIComponent(sku)}`);
                if (!res.ok) throw new Error(`HTTP ${res.status}`);
                this.product = await res.json();
            } catch (e) {
                this.error = 'Produkt konnte nicht geladen werden.';
            } finally {
                this.loading = false;
            }
        },

        get thumbnailUrl() {
            const img = this.product?.media_gallery_entries?.[0];
            return img ? `/media/catalog/product${img.file}` : '/static/placeholder.jpg';
        }
    };
}
  

<!-- Verwendung im phtml: -->
<div x-data="productQuickView('<?= $block->escapeJs($product->getSku()) ?>')"
     @mouseenter="loadProduct()"
     class="product-card cursor-pointer">

    <!-- Skeleton-Loader während des Ladens -->
    <div x-show="loading" class="animate-pulse bg-slate-200 rounded-lg h-48"></div>

    <!-- Produktbild nach dem Laden -->
    <template x-if="product && !loading">
        <div>
            <img :src="thumbnailUrl" :alt="product.name" class="w-full rounded-lg">
            <p x-text="product.name" class="font-semibold mt-2"></p>
        </div>
    </template>

    <!-- Fehlermeldung -->
    <p x-show="error" x-text="error" class="text-red-500 text-sm"></p>

</div>
  

Suchvorschläge mit Debounce


function liveSearch() {
    return {
        query: '',
        results: [],
        loading: false,
        open: false,

        async search() {
            if (this.query.length < 2) {
                this.results = [];
                this.open = false;
                return;
            }
            this.loading = true;
            try {
                // Magento Catalog Search API
                const params = new URLSearchParams({
                    'searchCriteria[filter_groups][0][filters][0][field]': 'name',
                    'searchCriteria[filter_groups][0][filters][0][value]': `%${this.query}%`,
                    'searchCriteria[filter_groups][0][filters][0][condition_type]': 'like',
                    'searchCriteria[pageSize]': '5',
                    'fields': 'items[id,sku,name,price],total_count',
                });
                const res = await fetch(`/rest/V1/products?${params}`);
                const data = await res.json();
                this.results = data.items || [];
                this.open = this.results.length > 0;
            } finally {
                this.loading = false;
            }
        }
    };
}
  

8. Hyvä Events und $dispatch

Das Hyvä Theme setzt auf ein event-basiertes Kommunikationsmodell zwischen Alpine.js-Komponenten. Anstatt direkte Referenzen zwischen Komponenten herzustellen, werden benutzerdefinierte Browser-Events über $dispatch ausgelöst und auf window gelistened.

Hyvä Core Events

Das Hyvä Theme definiert eigene Events, die im gesamten Theme verfügbar sind. Die wichtigsten:


// Hyvä Standard-Events — immer auf window hören!

// Warenkorb
'product-added-to-cart'           // { product, qty }
'cart-item-removed'               // { itemId }
'update-cart-item'                // { itemId, qty }

// Mini-Cart
'toggle-minicart'                 // {}
'reload-cart-section'             // {}

// Wunschliste
'product-added-to-wishlist'       // { productId }
'product-removed-from-wishlist'   // { productId }

// Nachrichten / Notifications
'customer-data-reload'            // { sectionNames: [...] }
'private-content-loaded'          // { data }

// Galerie
'update-gallery'                  // { images: [...] }
'gallery-loaded'                  // {}

// Beispiel: Event auslösen
this.$dispatch('product-added-to-cart', {
    product: this.product,
    qty: this.qty
});

// Beispiel: Event empfangen (auf window!)
// <div @product-added-to-cart.window="showNotification($event.detail)">
  

Komponenten-Kommunikation via Events


<!-- Szenario: Produktkonfiguration kommuniziert mit Warenkorb-Button -->

<!-- Komponente 1: Größenauswahl -->
<div x-data="{ selectedSize: null }"
     class="size-selector">
    <template x-for="size in ['XS','S','M','L','XL']">
        <button @click="selectedSize = size;
                        $dispatch('size-selected', { size: size })"
                :class="{ 'ring-2 ring-brand-red': selectedSize === size }"
                class="size-btn"
                x-text="size">
        </button>
    </template>
</div>

<!-- Komponente 2: Warenkorb-Button — empfängt size-selected -->
<div x-data="{ selectedSize: null, canAdd: false }"
     @size-selected.window="selectedSize = $event.detail.size; canAdd = true"
     class="add-to-cart-wrapper">
    <button @click="addToCart(selectedSize)"
            :disabled="!canAdd"
            :class="{ 'opacity-50': !canAdd }"
            class="btn-primary w-full">
        <span x-show="!canAdd">Größe auswählen</span>
        <span x-show="canAdd">In den Warenkorb</span>
    </button>
</div>
  

9. Häufige Fehler bei Alpine.js in Hyvä Theme

Fehler 1: Alpine ist nicht definiert

Der häufigste Fehler beim Umstieg: ReferenceError: Alpine is not defined. Die Ursache ist ein falsches Timing. Alpine.js registriert sich beim Laden asynchron. Code, der Alpine.store() oder Alpine.data() aufruft, muss auf das alpine:init-Event warten.


// FALSCH: Direkter Aufruf — Alpine ist ggf. noch nicht bereit
Alpine.store('cart', { ... });  // ReferenceError!

// RICHTIG: Im alpine:init Event-Listener
document.addEventListener('alpine:init', () => {
    Alpine.store('cart', { ... });  // Korrekt
    Alpine.data('myComponent', () => ({ ... }));  // Korrekt
});
  

Fehler 2: window-Modifier vergessen

Hyvä-Events werden auf window ausgelöst. Ein Event-Listener ohne .window-Modifier empfängt nur Events, die auf dem Element selbst oder seinen Kindknoten ausgelöst werden.


<!-- FALSCH: Empfängt keine Events von anderen Komponenten -->
<div @product-added-to-cart="showNotification()">

<!-- RICHTIG: .window Modifier — empfängt alle window-Events -->
<div @product-added-to-cart.window="showNotification($event.detail)">
  

Fehler 3: PHP-Daten nicht korrekt escapen

Beim Übergeben von PHP-Daten an x-data fehlt häufig das korrekte Escaping. JSON-Sonderzeichen oder Anführungszeichen können den Alpine.js-Parser brechen.


<?php
// FALSCH: Kein Escaping — XSS-Risiko und Parse-Fehler möglich
<div x-data="{ name: '<?= $product->getName() ?>' }">

// RICHTIG: json_encode + escapeHtmlAttr für den Attribut-Kontext
$data = json_encode(['name' => $product->getName(), 'price' => $product->getPrice()]);
?>
<div x-data="<?= $block->escapeHtmlAttr($data) ?>">

// Oder für komplexere Übergaben: Komponenten-Funktion mit JSON-Parameter
<div x-data="productView(<?= $block->escapeHtmlAttr(json_encode($productData)) ?>)">
  

Fehler 4: x-if ohne template-Tag


<!-- FALSCH: x-if direkt auf einem normalen Element -->
<div x-if="isLoggedIn">Willkommen!</div>

<!-- RICHTIG: x-if MUSS auf einem <template>-Tag stehen -->
<template x-if="isLoggedIn">
    <div>Willkommen!</div>
</template>
  

Fehler 5: Reaktivität durch direktes Array-Mutieren brechen


// FALSCH: Direktes Index-Setzen — Alpine.js erkennt die Änderung nicht
this.items[0] = newItem;        // Nicht reaktiv!
this.items.length = 0;          // Nicht reaktiv!

// RICHTIG: Array-Methoden verwenden die reaktiv sind
this.items.splice(0, 1, newItem);   // Reaktiv
this.items = [...this.items];        // Reaktiv (neues Array)
this.items.push(newItem);            // Reaktiv
this.items = this.items.filter(i => i.id !== deletedId);  // Reaktiv
  

10. Alpine.js vs. Knockout.js – Ein direkter Vergleich

Wer von Luma auf Hyvä migriert, kennt den Kulturschock: Knockout.js-Bindings, RequireJS-Module und data-mage-init-Attribute werden durch Alpine.js ersetzt. Die Konzepte sind ähnlich, die Implementierung grundlegend verschieden.

Alpine.js vs. Knockout.js in Magento 2 Merkmal Alpine.js (Hyvä) Knockout.js (Luma) Bundle-Größe ~15 KB (minified) ~65 KB + RequireJS Syntax HTML-Attribute (x-data, @click) data-bind Attribute + JS-ViewModel Build-Toolchain Keine erforderlich RequireJS + AMD-Module Lernkurve Flach — intuitiv für HTML-Kenner Steil — AMD + Observable-Pattern Performance Sehr hoch (kein Framework-Overhead) Mittel (Observable-Tracking) Magento Integration Hyvä Events + REST API UI Components + Section API

Das Ergebnis: Alpine.js ist nicht nur kleiner und schneller als Knockout.js – es ist auch erheblich leichter zu erlernen und zu warten. Der Code lebt im HTML, ist sofort lesbar und erfordert keine mentale Kontextverschiebung zwischen HTML-Markup und JavaScript-Dateien.

Mironsoft

Hyvä Theme & Alpine.js Entwicklung

Hyvä Theme professionell entwickeln?

Wir migrieren Ihr Magento 2 Shop von Luma zu Hyvä Theme, entwickeln performante Alpine.js-Komponenten und schulen Ihr Entwicklungsteam in modernem Magento-Frontend.

Luma → Hyvä Migration

Vollständige Theme-Migration, Knockout-Ablösung, Performance-Optimierung

Alpine.js Komponenten

Custom-Komponenten, Warenkorb-Integration, Produktkonfiguratoren

Core Web Vitals

LCP, CLS, INP Optimierung — Lighthouse Scores über 95

11. Zusammenfassung

Alpine.js ist das JavaScript-Fundament des Hyvä Theme und der modernste Weg, interaktive Komponenten in Magento 2 zu entwickeln. Mit deklarativem HTML-Markup, einem reaktiven Store-System und nahtloser Integration mit Hyvä-Events ersetzt es Knockout.js vollständig – bei einem Bruchteil der Komplexität und Größe.

Alpine.js in Hyvä Theme – Das Wichtigste auf einen Blick

x-data & Komponenten-Pattern

Einfache Zustände inline als Objekt, komplexe Logik in benannten Funktionen. Globale Wiederverwendung via Alpine.data() und alpine:init-Event.

PHP-Daten übergeben

Immer json_encode() + $block->escapeHtmlAttr() für den x-data-Attribut-Kontext. Keine manuellen String-Konkatenierungen.

Events & Kommunikation

Hyvä-Events via $dispatch auslösen, via @event.window empfangen. Immer den .window-Modifier verwenden für komponentenübergreifende Events.

$store für globalen State

Warenkorb, Kundenstatus und andere seitenweite Daten im Alpine.js Store speichern. Alle Komponenten greifen reaktiv auf denselben Store zu.

12. FAQ: Alpine.js in Hyvä Theme

1 Was ist Alpine.js und warum wird es in Hyvä Theme verwendet?
Alpine.js ist ein leichtgewichtiges (~15 KB) JavaScript-Framework für reaktive Datenbindung direkt im HTML. Im Hyvä Theme ersetzt es Knockout.js vollständig: kleiner, performanter, keine Build-Toolchain nötig. Interaktivität wird deklarativ mit Attributen wie x-data, @click und x-show direkt im Markup definiert.
2 Wie unterscheidet sich Alpine.js von Knockout.js?
Alpine.js (~15 KB, kein RequireJS, HTML-Attribute) vs. Knockout.js (~65 KB + RequireJS, AMD-Module, data-bind-Attribute, UI-Components). Alpine.js hat eine flache Lernkurve, bessere Performance und deutlich lesbareren Code. Im Hyvä Theme ist es die einzige JS-Option – Luma-Knockout-Code funktioniert dort nicht.
3 Wie übergebe ich PHP-Daten sicher an Alpine.js?
Mit json_encode() + $block->escapeHtmlAttr(): <div x-data="escapeHtmlAttr(json_encode($data)) ?>">. Für Komponenten-Funktionen: x-data="produktView(escapeHtmlAttr(json_encode($daten)) ?>)". Niemals Strings direkt konkatenieren – das öffnet XSS-Lücken und bricht den JSON-Parser.
4 Was ist der Unterschied zwischen x-show und x-if?
x-show toggelt display:none – das Element bleibt im DOM, gut für häufige Toggles mit CSS-Transitions. x-if entfernt das Element komplett aus dem DOM – muss auf <template> stehen, gut für selten angezeigte oder teure Elemente. x-show ist in den meisten Fällen die bessere Wahl für Toggles.
5 Wie registriere ich Alpine.js-Komponenten global?
Mit Alpine.data() im alpine:init-Event: document.addEventListener('alpine:init', () => Alpine.data('name', () => ({...}))). Die JS-Datei wird im Layout eingebunden. Im Template dann: <div x-data="name()">. So lässt sich dieselbe Komponente auf mehreren Seiten verwenden.
6 Wie kommunizieren zwei Alpine.js-Komponenten miteinander?
Über benutzerdefinierte Events: $dispatch('event-name', {data}) zum Senden, @event-name.window="handler($event.detail)" zum Empfangen. Der .window-Modifier ist zwingend für komponentenübergreifende Kommunikation. Alternativ: gemeinsamer $store für geteilten State.
7 Wie empfange ich Hyvä-eigene Events wie 'product-added-to-cart'?
Hyvä-Events werden auf window ausgelöst: @product-added-to-cart.window="showNotification($event.detail)". Wichtig: immer .window angeben. Ohne diesen Modifier werden Events von anderen Komponenten nicht empfangen. Eigene Events werden mit this.$dispatch('mein-event', payload) ausgelöst.
8 Was ist Alpine.js $store und wann verwende ich ihn?
Der globale reaktive Datenspeicher für seitenweite Zustände: Warenkorb-Inhalt, Kundenlogin-Status, Wunschliste. Definiert mit Alpine.store('name', {...}) im alpine:init-Event. Zugriff in beliebigen Komponenten via $store.name.property. Änderungen sind sofort in allen Komponenten reaktiv sichtbar – ideal als Alternative zu Events für häufig gelesenen State.
9 Wie rufe ich die Magento 2 REST-API in Alpine.js auf?
Mit dem nativen fetch(): const res = await fetch('/rest/V1/products/sku', { headers: { 'Authorization': 'Bearer ' + token } }). Für POST-Requests: method: 'POST', body: JSON.stringify(data). jQuery und $.ajax() werden im Hyvä Theme nicht benötigt. Mit async/await bleibt der Code übersichtlich.
10 Warum erscheint "ReferenceError: Alpine is not defined"?
Alpine.js wird asynchron geladen. Code, der Alpine.store() oder Alpine.data() direkt aufruft, läuft möglicherweise vor der Alpine.js-Initialisierung. Lösung: Alles in document.addEventListener('alpine:init', () => { ... }) wrappen. Dieser Event wird von Alpine.js ausgelöst kurz bevor es den DOM initialisiert – der zuverlässige Zeitpunkt für alle Registrierungen.