Alpine-Filterlogik integrieren: Kategorie-Buttons, Live-Filterung ohne Page-Reload
Alpine-Filterlogik integrieren: Kategorie-Buttons, Live-Filterung ohne Page-Reload
~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Jetzt kommt der interaktive Teil der Team-Seite dazu: Kategorie-Buttons, die das Grid live filtern, ganz ohne Page-Reload. Das Muster ist bewusst sehr ähnlich zum Akkordeon-Beispiel aus Kapitel 16 - nur dass hier nicht Sichtbarkeit einzelner Einträge, sondern die Zugehörigkeit zu einer aktiven Kategorie entscheidet.
Schritt 1: Team-Daten als JSON in x-data einbetten
Wie in Kapitel 15 am Mini-Cart-Beispiel gezeigt, betten wir die vom ViewModel gelieferten Team-Daten als JSON direkt in x-data ein - danach läuft die gesamte Filterlogik rein clientseitig, ohne weiteren Server-Request.
<?php
declare(strict_types=1);
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Serialize\Serializer\Json $json */
/** @var \Mironsoft\TeamPage\ViewModel\TeamMembers $teamViewModel */
$teamViewModel = $block->getData('team_view_model');
$json = $block->getData('json_serializer');
?>
<div
class="mx-auto max-w-6xl px-4 py-12 md:px-8"
x-data="teamFilter(<?= /* @noEscape */ $json->serialize($teamViewModel->getTeamMembers()) ?>)"
>json_serializer wird als zweites Argument im Layout-XML gebunden - genau wie das ViewModel selbst, nur diesmal auf eine Magento-Kernklasse statt auf eigenen Code verweisend:
<argument name="json_serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument>Schritt 2: die Alpine.data()-Komponente
Wie in Kapitel 16 gelernt, lohnt sich für mehrere zusammengehörige Methoden eine benannte Alpine.data()-Komponente statt eines reinen Inline-Objekts:
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('teamFilter', (members) => ({
members: members,
activeCategory: 'alle',
setCategory(category) {
this.activeCategory = category;
},
isActive(category) {
return this.activeCategory === category;
},
get visibleMembers() {
if (this.activeCategory === 'alle') {
return this.members;
}
return this.members.filter(
(member) => member.category === this.activeCategory
);
},
}));
});
</script>visibleMembers ist ein Getter - er berechnet die aktuell sichtbare Liste bei jedem Zugriff neu, basierend auf activeCategory. Das ist der Kern der Live-Filterung: Ändert sich activeCategory durch einen Button-Klick, berechnet Alpine visibleMembers automatisch neu und aktualisiert das DOM - ganz ohne manuellen Neuaufbau der Liste.
Schritt 3: die Kategorie-Buttons
<div class="mb-8 flex flex-wrap gap-2">
<?php foreach ($teamViewModel->getCategories() as $category): ?>
<button
type="button"
@click="setCategory('<?= $escaper->escapeJs($category['key']) ?>')"
:class="isActive('<?= $escaper->escapeJs($category['key']) ?>')
? 'bg-brand-accent text-white'
: 'bg-slate-100 text-brand-slate hover:bg-slate-200'"
class="rounded-full px-4 py-2 text-sm font-semibold transition"
>
<?= $escaper->escapeHtml($category['label']) ?>
</button>
<?php endforeach; ?>
</div>Zwei Escaper-Methoden kommen hier gezielt zum Einsatz: escapeJs() für den Kategorie-Schlüssel, weil er innerhalb eines JavaScript-Ausdrucks (@click="...") landet, und escapeHtml() für den sichtbaren Button-Text - genau die Escaper-Regeln aus Kapitel 8, jetzt konkret angewendet.
Schritt 4: das Grid auf visibleMembers umstellen
Die letzte Änderung ersetzt die PHP-foreach-Schleife aus Kapitel 20 durch eine clientseitige Alpine-Iteration mit x-for (offiziell x-for in Kombination mit einem <template>-Tag, dem Standardmuster für Listen in Alpine):
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<template x-for="member in visibleMembers" :key="member.name">
<article class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<div class="mb-4 h-40 w-full rounded-lg bg-slate-100"></div>
<h3 class="text-lg font-bold text-brand-dark" x-text="member.name"></h3>
<p class="text-sm text-brand-slate" x-text="member.role"></p>
</article>
</template>
</div>Wichtiger Unterschied zu Kapitel 20: Weil das Grid jetzt vollständig clientseitig aus den eingebetteten JSON-Daten aufgebaut wird, kommt hier kein $escaper mehr zum Einsatz - x-text setzt Text-Inhalt und escaped dabei automatisch (Alpine setzt intern textContent, nicht innerHTML), genau wie es $escaper->escapeHtml() auf der PHP-Seite tut.
Achtung: Wichtig: x-text ist sicher, weil es Text-Inhalt setzt. x-html dagegen fügt rohes HTML ein und wäre hier die falsche Wahl - genau wie eine ungeschützte PHP-Ausgabe ohne $escaper (Kapitel 8).
Zwischenstand testen
Nach einem bin/cache-clean zeigt /team jetzt die vier Kategorie-Buttons - ein Klick auf "Entwicklung" filtert das Grid sofort auf Anna und David, ein Klick auf "Alle" zeigt wieder alle vier Karten. Alles ohne Page-Reload.
Tipp: In den Browser-Entwicklertools lässt sich unter dem Netzwerk-Tab direkt beobachten, dass beim Klick auf einen Kategorie-Button kein neuer Request an den Server geht - der beste Beweis dafür, dass die Filterung tatsächlich rein clientseitig passiert.