Wishlist-Funktionalität in Hyvä anpassen: Alpine.js & Ajax
AI generated
Hyvä
phtml
Hyvä Theme · Alpine.js · Magento_Wishlist
Wishlist-Funktionalität in Hyvä anpassen
Von Alpine.js State bis zur Server-Persistenz

Die Wishlist-Funktionalität im Hyvä Theme ersetzt Magentos Luma-Widgets durch schlankes phtml und Alpine.js: ein Alpine.store hält die Produkt-IDs der Merkliste im Client, während fetch()-Aufrufe gegen den Magento_Wishlist-Controller mit form_key die Serverseite synchron halten. Dieser Artikel zeigt, wie das Herz-Icon sofort auf Klicks reagiert, wie optimistische Updates mit Rollback funktionieren und wie Gäste sauber zum Login geführt werden, ohne dass eine einzige Seite komplett neu lädt.

12 Min. Lesezeit Alpine.js · fetch() · form_key Magento 2.4.8 · Hyvä Theme

1. Wie die Standard-Wishlist in Magento_Wishlist funktioniert

Bevor man die Wishlist-Funktionalität im Hyvä Theme anpasst, lohnt sich ein Blick auf das Standardverhalten von Magento_Wishlist. Das Kernmodul liefert eine vollständige Merklisten-Logik: das Model Magento\Wishlist\Model\Wishlist kapselt die Beziehung zwischen Kunde und gemerkten Produkten, die Tabelle wishlist_item speichert Produkt-ID, Menge und optionale Beschreibung, und der Controller Magento\Wishlist\Controller\Index\Add verarbeitet das Hinzufügen serverseitig. Diese Schicht bleibt bei einer Anpassung der Wishlist-Funktionalität in Hyvä unverändert: Modelle, Repositories und Controller sind Vendor-Code und werden nicht überschrieben, sondern lediglich aus dem Frontend heraus anders angesprochen.

Im Luma-Theme wird diese Serverlogik über Knockout.js-Templates und jQuery-Widgets an das Frontend angebunden. Der Add-to-Wishlist-Link ist ein Anchor-Tag, dessen Klick entweder eine komplette Seitennavigation auslöst oder über ein Knockout-Binding eine asynchrone Anfrage anstößt, deren Ergebnis per Observable in die Oberfläche zurückgespielt wird. Für die Wishlist-Funktionalität bedeutet das in der Praxis: mehrere Icons für dasselbe Produkt auf einer Seite, etwa in einem Cross-Sell-Slider und gleichzeitig in der Produktliste, kennen sich gegenseitig nicht und müssen einzeln synchronisiert werden. Genau dieses Problem löst ein zentraler Client-State, wie er in Hyvä zum Einsatz kommt.

2. Warum Hyvä die Luma-Widgets ersetzt

Hyvä ersetzt aus Prinzip jede Knockout.js- und jQuery-Widget-Abhängigkeit durch serverseitig gerendertes phtml in Kombination mit Alpine.js. Für die Wishlist-Funktionalität bedeutet das konkret: Die Templates unter Magento_Wishlist/templates werden im Theme durch schlanke phtml-Dateien ersetzt, die keine UI-Components und keine Knockout-Bindings mehr enthalten. Layout-XML im Theme, etwa catalog_product_view.xml und catalog_category_view.xml, tauscht die Blöcke aus, ohne dass ein einziges Core-Template von Magento_Wishlist überschrieben werden muss. Updates des Kernmoduls bleiben dadurch problemlos kompatibel.

Der zentrale Unterschied liegt nicht im Backend, sondern in der Art, wie das Frontend mit dem Server kommuniziert. Statt eines Seitenwechsels sendet die angepasste Wishlist-Funktionalität einen fetch()-Request an denselben Controller, den auch Luma verwendet, wertet die JSON-Antwort aus und aktualisiert ausschließlich den betroffenen Teil der Seite. Das reduziert nicht nur die wahrgenommene Ladezeit, sondern vermeidet auch einen kompletten Neuaufbau von Alpine-Komponenten, die an anderer Stelle der Seite bereits initialisiert wurden.

3. Das Herz-Icon: Add-to-Wishlist-Komponente

Das sichtbarste Element der Wishlist-Funktionalität ist das Herz-Icon auf der Produktliste und der Produktdetailseite. In Hyvä ist das keine Widget-Instanz, sondern ein einfaches SVG innerhalb eines x-data-Scopes, der beim Rendern die Produkt-ID sowie den initialen Wishlist-Status erhält. Der Zustand "ist auf der Wishlist" wird nicht lokal im Icon gehalten, sondern über $store.wishlist aus dem globalen Alpine-Store gelesen, damit alle Vorkommen desselben Produkts auf der Seite denselben Zustand zeigen.

Der Klick auf das Icon ruft eine Methode des Stores auf, die intern zwischen Hinzufügen und Entfernen unterscheidet, je nachdem ob die Produkt-ID bereits in der lokalen Menge enthalten ist. Wichtig für CSP-konforme Auslieferung: Jeder Inline-<script>-Block, der Alpine-Definitionen enthält, muss im phtml direkt danach mit $hyvaCsp->registerInlineScript() registriert werden, sonst blockiert die Content Security Policy von Hyvä die Ausführung.


<!-- app/design/frontend/Mironsoft/default/Magento_Wishlist/templates/button/add.phtml -->
<?php
/** @var \Magento\Catalog\Block\Product\AbstractProduct $block */
/** @var \Magento\Catalog\Model\Product $product */
$product = $block->getProduct();
?>
<div x-data="{ productId: <?= (int) $product->getId() ?> }" class="inline-flex">
    <button
        type="button"
        class="wishlist-heart p-2 rounded-full hover:bg-gray-100 transition-colors"
        x-on:click="$store.wishlist.toggle(productId)"
        x-bind:aria-pressed="$store.wishlist.has(productId).toString()"
        :aria-label="$store.wishlist.has(productId) ? 'Remove from wishlist' : 'Add to wishlist'"
    >
        <svg class="w-5 h-5" viewBox="0 0 24 24" stroke-width="2"
             :class="$store.wishlist.has(productId) ? 'fill-red-500 stroke-red-500' : 'fill-none stroke-gray-500'">
            <path stroke-linecap="round" stroke-linejoin="round"
                  d="M12 21s-7-4.35-9.5-8.5C.5 8.5 2 4 6 4c2 0 3.5 1.2 4 2.5.5-1.3 2-2.5 4-2.5 4 0 5.5 4.5 3.5 8.5C19 16.65 12 21 12 21z"/>
        </svg>
    </button>
</div>

4. Globaler State mit Alpine.store

Ein x-data-Objekt pro Icon würde für eine einfache Seite reichen, scheitert aber, sobald dasselbe Produkt mehrfach auf einer Seite erscheint, etwa in der Kategorie-Liste und gleichzeitig in einem "Zuletzt angesehen"-Slider. Die Wishlist-Funktionalität braucht deshalb einen einzigen, seitenweiten State: einen Alpine.store('wishlist', …), der beim alpine:init-Event registriert wird und von jeder Komponente über $store.wishlist referenziert werden kann.

Der Store hält die Menge der Produkt-IDs, den form_key für die Ajax-Requests sowie einen einfachen Lade-Status pro Produkt, damit ein Doppelklick während einer laufenden Anfrage keine zweite Anfrage auslöst. Methoden wie has(id), toggle(id) und remove(id) kapseln die komplette Logik der Wishlist-Funktionalität an einer einzigen Stelle im Code, statt sie über mehrere Templates zu verteilen.


// app/design/frontend/Mironsoft/default/Magento_Wishlist/web/js/wishlist-store.js
document.addEventListener('alpine:init', () => {
  Alpine.store('wishlist', {
    // Hydrated from a small JSON blob emitted by the ViewModel on page load
    ids: new Set(window.wishlistHydration?.productIds ?? []),
    formKey: window.wishlistHydration?.formKey ?? '',
    loading: {},

    has(productId) {
      return this.ids.has(productId);
    },

    toggle(productId) {
      // Guard against duplicate requests while one is already in flight
      if (this.loading[productId]) {
        return;
      }
      return this.has(productId) ? this.remove(productId) : this.add(productId);
    },

    add(productId) {
      // Implemented as a fetch() call, see the ajax example below
    },

    remove(productId) {
      // Mirrors add(), but targets the remove endpoint
    }
  });
});

5. Der Ajax-Request an den Wishlist-Controller

Der eigentliche Netzwerk-Request läuft über fetch() gegen denselben Controller, den auch die Luma-Wishlist verwendet: wishlist/index/add für das Hinzufügen und wishlist/index/remove für das Entfernen. Magento validiert bei beiden Endpunkten den form_key als CSRF-Schutz, ohne einen gültigen, aktuellen Wert lehnt der Controller die Anfrage ab. Die Wishlist-Funktionalität liest den form_key deshalb einmal beim Store-Init aus einem versteckten Formularfeld oder Meta-Tag aus und hängt ihn bei jedem Request als Formularfeld an.

Der Header Accept: application/json beziehungsweise X-Requested-With: XMLHttpRequest signalisiert dem Controller, dass eine JSON-Antwort statt eines Redirects erwartet wird. Schlägt die Anfrage fehl, etwa weil die Session zwischenzeitlich abgelaufen ist, liefert der Controller einen Fehlerstatus oder eine JSON-Struktur mit success: false, auf die die Wishlist-Funktionalität im Store mit einem Rollback reagieren muss, statt den fehlerhaften Zustand stehen zu lassen.


// Extends the store from the previous section with a concrete fetch() implementation
async add(productId) {
  const snapshot = new Set(this.ids); // Snapshot for rollback on failure
  this.ids.add(productId);            // Optimistic UI update, applied immediately
  this.loading[productId] = true;

  try {
    const response = await fetch('/wishlist/index/add/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: new URLSearchParams({
        product: productId,
        form_key: this.formKey
      })
    });

    if (!response.ok) {
      throw new Error(`Wishlist request failed with status ${response.status}`);
    }

    const data = await response.json();
    if (!data.success) {
      throw new Error(data.message ?? 'Unknown wishlist error');
    }
  } catch (error) {
    this.ids = snapshot; // Rollback: restore the state before the optimistic update
    console.error('Wishlist add failed, rolled back', error);
  } finally {
    this.loading[productId] = false;
  }
}

6. Optimistic vs. Pessimistic UI-Updates

Für die UI-Reaktion gibt es zwei grundsätzliche Strategien. Beim pessimistischen Update wartet die Oberfläche auf die Serverantwort, bevor sich das Herz-Icon füllt, sicher, aber spürbar träge bei langsamen Verbindungen. Beim optimistischen Update flippt die Wishlist-Funktionalität das Icon sofort um, noch bevor die Antwort des Servers eingetroffen ist, und nimmt die Änderung nur im Fehlerfall wieder zurück. Für ein Feature, das primär der Bequemlichkeit dient, ist die optimistische Variante fast immer die richtige Wahl.

Der Rollback-Mechanismus braucht einen Snapshot des Zustands vor der Mutation: Bevor die ID zur Menge hinzugefügt oder daraus entfernt wird, sichert die Store-Methode den vorherigen Wert. Schlägt der fetch()-Call fehl oder liefert der Server success: false, stellt der catch-Block genau diesen Snapshot wieder her, und ein kurzer Alpine-Toast mit x-transition informiert den Nutzer über den Fehler. So bleibt die Wishlist-Funktionalität auch bei Netzwerkproblemen konsistent, ohne dass der Kunde einen falschen Zustand sieht, der erst nach einem Reload korrigiert wird.

7. Persistenz und Hydration der Wishlist-Funktionalität

Beim ersten Laden der Seite kennt der Alpine-Store noch keine Produkt-IDs, er muss mit dem tatsächlichen Wishlist-Inhalt des eingeloggten Kunden befüllt werden. Dafür liefert ein ViewModel, das ArgumentInterface implementiert, die aktuelle Menge der Produkt-IDs als kompaktes JSON in einem eigenen <script type="application/json">-Block, direkt vor dem Store-Init-Skript. Diese Hydration ist der Punkt, an dem serverseitige Wahrheit und clientseitiger State der Wishlist-Funktionalität zusammengeführt werden.

Wichtig dabei: Der Server bleibt die Quelle der Wahrheit. Der Client-State ist ein Cache für gefühlte Performance, kein eigenständiger Datenspeicher. Ändert sich die Wishlist an anderer Stelle, etwa weil der Kunde in einem zweiten Browser-Tab ein Produkt entfernt hat, zeigt erst der nächste volle Seitenaufruf den korrekten Stand, denn die Wishlist-Funktionalität in Hyvä synchronisiert bewusst nicht per Websocket oder Polling zwischen Tabs, um die Komplexität gering zu halten.


<?php

declare(strict_types=1);

namespace Mironsoft\Wishlist\ViewModel;

use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Wishlist\Model\WishlistFactory;

/**
 * Provides the current wishlist product IDs as a JSON blob for Alpine.js hydration.
 */
class WishlistHydration implements ArgumentInterface
{
    /**
     * @param CustomerSession $customerSession Current customer session.
     * @param WishlistFactory $wishlistFactory Factory for the wishlist model.
     * @param Json $json Json serializer for the frontend payload.
     */
    public function __construct(
        private readonly CustomerSession $customerSession,
        private readonly WishlistFactory $wishlistFactory,
        private readonly Json $json
    ) {
    }

    /**
     * Builds the JSON payload with product IDs and form key for the Alpine store.
     *
     * @return string
     */
    public function getHydrationJson(): string
    {
        if (!$this->customerSession->isLoggedIn()) {
            return $this->json->serialize([
                'productIds' => [],
                'formKey' => $this->customerSession->getFormKey(),
            ]);
        }

        $wishlist = $this->wishlistFactory->create()->loadByCustomerId(
            (int) $this->customerSession->getCustomerId(),
            true
        );

        $productIds = [];
        foreach ($wishlist->getItemCollection() as $item) {
            $productIds[] = (int) $item->getProductId();
        }

        return $this->json->serialize([
            'productIds' => $productIds,
            'formKey' => $this->customerSession->getFormKey(),
        ]);
    }
}

8. Gast-Limitierung und Add-after-Login-Flow

Magentos Wishlist-Modul ist an eine Kundensession gebunden, ein Gast kann grundsätzlich keine Produkte merken, weil wishlist_item zwingend eine customer_id voraussetzt. Klickt ein nicht eingeloggter Besucher auf das Herz-Icon, darf die Wishlist-Funktionalität also nicht einfach einen Ajax-Fehler werfen, sondern muss den Besucher kontrolliert zum Login führen, ohne die eigentliche Absicht zu verlieren.

Der saubere Weg ist ein Add-after-Login-Flow: Bevor der Redirect zur Login-Seite stattfindet, schreibt die Wishlist-Funktionalität die gewünschte Produkt-ID zusammen mit der ursprünglichen URL in den sessionStorage des Browsers. Nach erfolgreichem Login liest ein kleines Alpine-Snippet auf der Zielseite diesen Eintrag aus, löst automatisch den ursprünglichen Add-Request aus und entfernt den Eintrag anschließend aus dem Storage, damit ein erneuter Login-Vorgang nicht denselben Add-Request wiederholt.


{
  "pendingWishlistAdd": {
    "productId": 1245,
    "requestedAt": "2026-07-23T14:32:00Z",
    "redirectAfterLogin": "/catalog/product/view/id/1245/"
  }
}

9. Von der Wishlist in den Warenkorb: Ansätze im Vergleich

Der Umstieg von serverseitig gerenderten Luma-Widgets auf Alpine-gesteuerte Wishlist-Funktionalität betrifft mehrere Aspekte gleichzeitig: Reload-Verhalten, Zustandssynchronisation, Umzug in den Warenkorb und CSRF-Handling. Die folgende Tabelle stellt den alten und den empfohlenen Ansatz nebeneinander.

Aufgabe Luma / alter Ansatz Empfohlenes Hyvä-Pattern Vorteil
Zur Wishlist hinzufügen Full-Page-Reload über Anchor-Link fetch() + Alpine.store Update Kein Seitenwechsel, sofortiges Feedback
Zustandssynchronisation Knockout-Observable pro Widget-Instanz Ein geteilter Alpine.store Alle Icons zeigen konsistent denselben Zustand
In den Warenkorb verschieben Formular-Submit auf der Wishlist-Seite fetch() gegen updatePost Optimistische Entfernung ohne Reload
Gast-Interaktion Referer-Redirect, Absicht geht oft verloren sessionStorage Add-after-Login-Flow Absicht bleibt über den Login-Vorgang erhalten
CSRF-Schutz Hidden Input, pro Serverform gerendert form_key einmal geladen, im fetch-Body Weniger Server-Renders, gleiche Sicherheit

Besonders beim Verschieben eines Artikels von der Wishlist in den Warenkorb zeigt sich der Unterschied deutlich: Luma leitet dafür klassischerweise auf die volle Wishlist-Seite um und verarbeitet dort ein Formular mit mehreren Checkboxen. Die Hyvä-Variante der Wishlist-Funktionalität sendet stattdessen einen einzelnen fetch()-Call an wishlist/index/updatePost, entfernt das Produkt optimistisch aus dem Store und aktualisiert den Warenkorb-Zähler in derselben Aktion, ganz ohne Seitenwechsel.

10. Zusammenfassung

Die Wishlist-Funktionalität im Hyvä Theme ist im Kern ein Frontend-Umbau: Dieselben Magento_Wishlist-Controller und -Modelle bleiben bestehen, aber phtml mit Alpine.js ersetzt Knockout.js und jQuery-Widgets vollständig. Ein zentraler Alpine.store hält die Menge der gemerkten Produkt-IDs, fetch()-Requests mit form_key übernehmen die Kommunikation mit dem Server, und optimistische Updates mit Rollback sorgen für ein Gefühl von Geschwindigkeit, ohne die Konsistenz zu gefährden.

Für Gäste bleibt die serverseitige Wishlist weiterhin an einen Login gebunden, ein Add-after-Login-Flow über sessionStorage fängt diese Einschränkung UX-seitig ab. Wer die Wishlist-Funktionalität in einem bestehenden Hyvä-Shop anpasst, sollte mit der Layout-XML-Ebene beginnen, den Store danach isoliert testen und erst zum Schluss die Ajax-Endpunkte gegen echte Session-Zustände prüfen.

Wishlist-Funktionalität in Hyvä, das Wichtigste auf einen Blick

Alpine.store statt Knockout

Ein zentraler, seitenweiter Store hält die Wishlist-Produkt-IDs und hält alle Icon-Instanzen synchron.

Optimistic UI mit Rollback

Sofortiges visuelles Feedback, mit Snapshot-basiertem Rollback bei fehlgeschlagenem fetch()-Call.

form_key & CSP

CSRF-Schutz über form_key im fetch-Body, jeder Inline-Script-Block via registerInlineScript() freigegeben.

Gast-Handling

Add-after-Login-Flow über sessionStorage bewahrt die Kaufabsicht über den Login-Redirect hinweg.

11. FAQ: Wishlist-Funktionalität in Hyvä

1Was ist die Wishlist-Funktionalität im Hyvä Theme genau?
Sie ist die Kombination aus dem unveränderten Magento_Wishlist-Backend und einem in Hyvä neu gebauten Frontend aus phtml und Alpine.js, das Knockout.js und jQuery-Widgets vollständig ersetzt.
2Warum verwendet Hyvä kein Knockout.js mehr für die Wishlist?
Hyvä verzichtet grundsätzlich auf Knockout.js, jQuery-Widgets und UI-Components. Die Wishlist-Logik läuft stattdessen über einen zentralen Alpine.store und einfache fetch()-Aufrufe.
3Wie aktualisiert sich das Herz-Icon ohne Page-Reload?
Das Icon liest seinen Zustand aus einem globalen Alpine.store. Ein Klick löst einen fetch()-Request aus und aktualisiert den Store, wodurch alle Vorkommen des Produkts sofort mit aktualisieren.
4Was ist ein Alpine.store und warum nicht x-data pro Icon?
Ein Alpine.store ist ein seitenweiter, reaktiver State-Container. Er sorgt dafür, dass mehrere Vorkommen desselben Produkts denselben Wishlist-Status zeigen.
5Warum braucht der fetch()-Call den form_key?
Magento schützt seine Controller-Endpunkte per CSRF-Token, dem form_key. Ohne gültigen form_key lehnt der Wishlist-Controller die Anfrage ab.
6Optimistisches vs. pessimistisches UI-Update?
Pessimistisch wartet auf die Serverantwort. Optimistisch ändert sofort und macht bei Fehlern via Rollback wieder rückgängig.
7Können Gäste ohne Login eine Wishlist nutzen?
Nein. Magentos Wishlist-Modul erfordert zwingend eine Kundensession, da wishlist_item an eine customer_id gebunden ist.
8Wie funktioniert der Add-after-Login-Flow?
Vor dem Login-Redirect wird die Produkt-ID im sessionStorage gespeichert. Nach dem Login löst ein Alpine-Snippet den ursprünglichen Add-Request automatisch aus.
9Wishlist-Artikel ohne Reload in den Warenkorb verschieben?
Ein fetch()-Call an wishlist/index/updatePost übernimmt den Umzug. Die UI entfernt das Produkt optimistisch und aktualisiert den Warenkorb-Zähler.
10Muss ich Magento_Wishlist-Controller anpassen?
In der Regel nicht. Controller und Modelle bleiben unverändert, angepasst werden nur Templates, Layout-XML und die Alpine.js-Schicht.

Mironsoft

Hyvä-Frontends, Alpine.js-Komponenten und Magento-2-Individualisierung

Wishlist-Funktionalität, die sich wie ein Teil des Shops anfühlt?

Wir bauen die Wishlist-Funktionalität in eurem Hyvä-Theme individuell aus: Alpine.store-Architektur, optimistische Updates mit Rollback und ein sauberer Add-after-Login-Flow für Gäste, vollständig CSP-konform.

Alpine-Komponenten

Herz-Icons, Listen und Merklisten-Seiten als reaktive Alpine.js-Bausteine

Ajax-Integration

Fetch-Requests, form_key-Handling und Fehler-Rollback nach Magento-Standard

CSP & Performance

registerInlineScript-konforme Umsetzung ohne zusätzliches JavaScript