Produktfilter sind im E-Commerce entscheidend für die Benutzererfahrung. In diesem Artikel zeigen wir Ihnen, wie Sie ein schnelles, dynamisches Filtersystem mit JavaScript und PHP umsetzen – inklusive AJAX für reibungslose Updates und erweiterte Funktionen für bessere Performance.
<form id="filter-form">
<fieldset>
<legend>Produktfilter</legend>
<label>
Kategorie:
<select name="category">
<option value="">Alle Kategorien</option>
<option value="shirts">Shirts</option>
<option value="hosen">Hosen</option>
<option value="schuhe">Schuhe</option>
<option value="accessoires">Accessoires</option>
</select>
</label>
<label>
Preis:
<select name="price">
<option value="">Alle Preise</option>
<option value="low">Unter 50€</option>
<option value="medium">50–100€</option>
<option value="high">Über 100€</option>
</select>
</label>
<label>
Sortierung:
<select name="sort">
<option value="">Standard</option>
<option value="price_asc">Preis aufsteigend</option>
<option value="price_desc">Preis absteigend</option>
<option value="name_asc">Name A-Z</option>
</select>
</label>
</fieldset>
</form>
<div id="loading-indicator" style="display: none;">
<p>Produkte werden geladen...</p>
</div>
<div id="product-list"></div>
Wir beginnen mit einer strukturierten HTML-Vorlage, die aus einem Formular zur Filterung und einem Container für die Produktliste besteht. Die Verwendung von Fieldset und Legend verbessert die Barrierefreiheit, während der Loading-Indikator dem Benutzer visuelles Feedback gibt.
document.getElementById("filter-form").addEventListener("change", function () {
const formData = new FormData(this);
const loadingIndicator = document.getElementById("loading-indicator");
const productList = document.getElementById("product-list");
// Ladeindikator anzeigen
loadingIndicator.style.display = "block";
productList.style.opacity = "0.5";
fetch("get_products.php", {
method: "POST",
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
productList.innerHTML = html;
productList.style.opacity = "1";
// Anzahl der Ergebnisse anzeigen
const productCount = productList.querySelectorAll('.product').length;
updateResultCount(productCount);
})
.catch(err => {
console.error("Fehler beim Laden der Produkte:", err);
productList.innerHTML = '<p class="error">Fehler beim Laden der Produkte. Bitte versuchen Sie es später erneut.</p>';
productList.style.opacity = "1";
})
.finally(() => {
loadingIndicator.style.display = "none";
});
});
function updateResultCount(count) {
let countElement = document.getElementById("result-count");
if (!countElement) {
countElement = document.createElement("p");
countElement.id = "result-count";
document.getElementById("product-list").insertAdjacentElement("beforebegin", countElement);
}
countElement.textContent = `${count} Produkt${count !== 1 ? 'e' : ''} gefunden`;
}
Diese erweiterte JavaScript-Funktion holt bei jeder Filteränderung neue Produktdaten vom Server. Sie bietet verbessertes Error-Handling, einen Ladeindikator und zeigt die Anzahl der gefundenen Produkte an.
<?php
header('Content-Type: text/html; charset=utf-8');
// Eingabedaten validieren und bereinigen
$category = filter_input(INPUT_POST, 'category', FILTER_SANITIZE_STRING) ?? '';
$price = filter_input(INPUT_POST, 'price', FILTER_SANITIZE_STRING) ?? '';
$sort = filter_input(INPUT_POST, 'sort', FILTER_SANITIZE_STRING) ?? '';
// Beispiel-Produktdaten (in der Praxis: Datenbankabfrage)
$products = [
["id" => 1, "name" => "Blaues Shirt", "category" => "shirts", "price" => 29.99, "image" => "shirt1.jpg"],
["id" => 2, "name" => "Schwarze Hose", "category" => "hosen", "price" => 89.50, "image" => "hose1.jpg"],
["id" => 3, "name" => "Designer-Shirt", "category" => "shirts", "price" => 120.00, "image" => "shirt2.jpg"],
["id" => 4, "name" => "Sportschuhe", "category" => "schuhe", "price" => 75.00, "image" => "schuh1.jpg"],
["id" => 5, "name" => "Elegante Hose", "category" => "hosen", "price" => 45.00, "image" => "hose2.jpg"],
];
// Produkte nach Kriterien filtern
$filtered = array_filter($products, function ($product) use ($category, $price) {
// Kategorie-Filter
if ($category && $product['category'] !== $category) {
return false;
}
// Preis-Filter
if ($price === 'low' && $product['price'] >= 50) return false;
if ($price === 'medium' && ($product['price'] < 50 || $product['price'] > 100)) return false;
if ($price === 'high' && $product['price'] <= 100) return false;
return true;
});
// Sortierung anwenden
switch ($sort) {
case 'price_asc':
usort($filtered, fn($a, $b) => $a['price'] <=> $b['price']);
break;
case 'price_desc':
usort($filtered, fn($a, $b) => $b['price'] <=> $a['price']);
break;
case 'name_asc':
usort($filtered, fn($a, $b) => strcasecmp($a['name'], $b['name']));
break;
}
// HTML-Ausgabe generieren
if (empty($filtered)) {
echo '<p class="no-results">Keine Produkte gefunden, die Ihren Kriterien entsprechen.</p>';
} else {
foreach ($filtered as $item) {
$formattedPrice = number_format($item['price'], 2, ',', '.');
echo "<div class='product' data-id='{$item['id']}'>
<img src='images/{$item['image']}' alt='{$item['name']}' loading='lazy'>
<h3>{$item['name']}</h3>
<p class='price'>{$formattedPrice}€</p>
<button class='add-to-cart' data-product-id='{$item['id']}'>In den Warenkorb</button>
</div>";
}
}
// Beispiel für Datenbankabfrage (PDO)
/*
try {
$pdo = new PDO("mysql:host=localhost;dbname=shop", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM products WHERE 1=1";
$params = [];
if ($category) {
$sql .= " AND category = :category";
$params[':category'] = $category;
}
if ($price) {
switch ($price) {
case 'low':
$sql .= " AND price < 50";
break;
case 'medium':
$sql .= " AND price BETWEEN 50 AND 100";
break;
case 'high':
$sql .= " AND price > 100";
break;
}
}
if ($sort) {
switch ($sort) {
case 'price_asc':
$sql .= " ORDER BY price ASC";
break;
case 'price_desc':
$sql .= " ORDER BY price DESC";
break;
case 'name_asc':
$sql .= " ORDER BY name ASC";
break;
}
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Datenbankfehler: " . $e->getMessage());
echo '<p class="error">Fehler beim Laden der Produkte.</p>';
}
*/
?>
Die Datei get_products.php wertet die POST-Daten aus, filtert und sortiert die Produktliste und gibt sie als strukturiertes HTML zurück. Die Kommentare zeigen, wie Sie eine echte Datenbankanbindung implementieren können.
let debounceTimer;
const cache = new Map();
document.getElementById("filter-form").addEventListener("change", function () {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const formData = new FormData(this);
const cacheKey = new URLSearchParams(formData).toString();
// Prüfen, ob Ergebnis bereits im Cache vorhanden ist
if (cache.has(cacheKey)) {
document.getElementById("product-list").innerHTML = cache.get(cacheKey);
return;
}
const loadingIndicator = document.getElementById("loading-indicator");
const productList = document.getElementById("product-list");
loadingIndicator.style.display = "block";
productList.style.opacity = "0.5";
fetch("get_products.php", {
method: "POST",
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
// Ergebnis im Cache speichern
cache.set(cacheKey, html);
productList.innerHTML = html;
productList.style.opacity = "1";
const productCount = productList.querySelectorAll('.product').length;
updateResultCount(productCount);
})
.catch(err => {
console.error("Fehler beim Laden der Produkte:", err);
productList.innerHTML = '<p class="error">Fehler beim Laden der Produkte. Bitte versuchen Sie es später erneut.</p>';
productList.style.opacity = "1";
})
.finally(() => {
loadingIndicator.style.display = "none";
});
}, 300);
});
// Cache nach 5 Minuten leeren
setTimeout(() => {
cache.clear();
}, 5 * 60 * 1000);
Mit Debouncing vermeiden wir zu viele schnelle Requests bei mehreren schnellen Änderungen. Das integrierte Caching reduziert Server-Anfragen für identische Filter-Kombinationen zusätzlich.
#filter-form {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#filter-form fieldset {
border: none;
padding: 0;
margin: 0;
}
#filter-form legend {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
#filter-form label {
display: inline-block;
margin-right: 20px;
margin-bottom: 10px;
font-weight: 500;
}
#filter-form select {
display: block;
width: 100%;
max-width: 200px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
margin-top: 5px;
}
#loading-indicator {
text-align: center;
padding: 20px;
font-style: italic;
color: #666;
}
#product-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
transition: opacity 0.3s ease;
}
.product {
border: 1px solid #eee;
border-radius: 8px;
padding: 15px;
background: white;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.product:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.product img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
}
.product h3 {
margin: 10px 0;
font-size: 1.1em;
color: #333;
}
.product .price {
font-size: 1.2em;
font-weight: bold;
color: #e74c3c;
margin: 10px 0;
}
.add-to-cart {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s ease;
}
.add-to-cart:hover {
background: #2980b9;
}
.no-results, .error {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
.error {
color: #e74c3c;
background: #fdf2f2;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
#result-count {
margin-bottom: 20px;
font-weight: 500;
color: #666;
}
Diese CSS-Styles sorgen für eine professionelle, responsive Darstellung des Filtersystems. Das Grid-Layout passt sich automatisch an verschiedene Bildschirmgrößen an.
Ein dynamischer Produktfilter sorgt für bessere Benutzerführung und höhere Conversion-Raten. Mit JavaScript und PHP lässt sich ein performantes und leicht anpassbares Filtersystem für jeden E-Commerce-Shop realisieren. Die vorgestellte Lösung bietet erweiterte Funktionen wie Caching, Debouncing und professionelle Fehlerbehandlung, die für produktive Umgebungen unerlässlich sind. Durch die modulare Struktur können Sie das System einfach an Ihre spezifischen Anforderungen anpassen und mit weiteren Features wie Suchfunktionen oder erweiterten Filtern ergänzen.
Nicht direkt, aber die Logik lässt sich mit eigenen Endpoints und Templates in WooCommerce adaptieren. Sie können WooCommerce-Hooks verwenden, um das System zu integrieren.
Ersetzen Sie das $products-Array durch eine SQL-Abfrage und nutzen Sie z. B. PDO, um die Daten dynamisch zu laden. Ein Beispiel finden Sie im kommentierten Code-Bereich.
Ja, fügen Sie einfach weitere Felder im HTML-Formular hinzu und erweitern Sie die entsprechenden Bedingungen im PHP-Code. Das System ist modular aufgebaut und leicht erweiterbar.
Debouncing sorgt dafür, dass Funktionen nicht zu oft hintereinander aufgerufen werden – ideal bei schnellen UI-Interaktionen. Es reduziert die Server-Last und verbessert die Performance.
Das Prinzip bleibt gleich – State Management und API-Aufrufe übernehmen dabei die JavaScript-Frameworks. Sie können die PHP-API unverändert verwenden.
Ergänzen Sie ein Suchfeld im HTML-Formular und erweitern Sie die PHP-Logik um eine LIKE-Abfrage oder nutzen Sie Elasticsearch für erweiterte Suchfunktionen.
Ja, durch die responsive CSS-Grid-Layout-Implementierung passt sich das System automatisch an verschiedene Bildschirmgrößen an.
Implementieren Sie Pagination, Lazy Loading und Datenbank-Indizierung. Zusätzlich können Sie Server-seitiges Caching mit Redis oder Memcached einsetzen.
Nutze den erweiterten WYSIWYG-Editor in Shopware 6. Dieser Editor ermöglicht die einfache Einbettung von Medien in die Beschreibung und viele weitere Features.
ab 7,99 €* / Monat
Plugin mietenOptimieren Sie Ihren Shop und schaffen Sie damit ein besseres Erlebnis für Ihre Kunden. Dieses Plugin minimiert die Ladezeit Ihres Shops und bietet zahlreiche Konfigurationen.
ab 27,49 €* / Monat
Plugin mietenErstellen und bearbeiten Sie Ihre eigenen Template-Erweiterungen schnell und einfach in der Administration. Anzeige vorhandener Storefront-Vorlagenpfade und -Inhalte.
ab 3,99 €* / Monat
Plugin mietenHinweis: * Alle Preise verstehen sich zzgl. Mehrwertsteuer
x