Alpine.js in Hyvä Theme: Interaktive Magento 2 Komponenten ohne jQuery erklärt
· Lesezeit: ca. 20 Minuten · Kategorien: Magento 2, Hyvä Theme, JavaScript
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.
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?
- 2. Die Grunddirektiven
- 3. Weitere Direktiven und Magic Properties
- 4. Alpine.js in Hyvä phtml-Templates
- 5. Das Komponenten-Pattern
- 6. Globaler State mit $store
- 7. Alpine.js mit der Magento 2 REST-API
- 8. Hyvä Events und $dispatch
- 9. Häufige Fehler und Lösungen
- 10. Alpine.js vs. Knockout.js
- 11. Zusammenfassung
- 12. FAQ
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.
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?
x-data, @click und x-show direkt im Markup definiert.2 Wie unterscheidet sich Alpine.js von Knockout.js?
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?
json_encode() + $block->escapeHtmlAttr(): <div x-data="= $block->escapeHtmlAttr(json_encode($data)) ?>">. Für Komponenten-Funktionen: x-data="produktView(= $block->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?
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?
$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'?
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?
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?
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.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.