Product Catalog
A complete product catalog featuring real-time search, category filtering, sorting, and responsive design. This recipe demonstrates how to build a performant, user-friendly product browsing experience.
Features Demonstrated
Two-way data binding - Search input with instant updates
Computed properties - Filtered product list based on search and filters
repeat.forwith keys - Efficient list renderingEvent handling - Sort buttons, filter checkboxes
Conditional rendering - Empty states, loading states
Value converters - Currency formatting
CSS class binding - Active filters, selected sort order
Debouncing - Optimize search performance
Code
View Model (product-catalog.ts)
interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
image: string;
inStock: boolean;
rating: number;
}
type SortOption = 'name' | 'price-low' | 'price-high' | 'rating';
export class ProductCatalog {
// Data
products: Product[] = [
{
id: 1,
name: 'Wireless Headphones',
description: 'Premium noise-canceling headphones with 30-hour battery',
price: 299.99,
category: 'Audio',
image: '/images/headphones.jpg',
inStock: true,
rating: 4.5
},
{
id: 2,
name: 'Smart Watch',
description: 'Fitness tracking with heart rate monitor and GPS',
price: 399.99,
category: 'Wearables',
image: '/images/smartwatch.jpg',
inStock: true,
rating: 4.2
},
{
id: 3,
name: 'Laptop Stand',
description: 'Ergonomic aluminum stand for better posture',
price: 49.99,
category: 'Accessories',
image: '/images/stand.jpg',
inStock: false,
rating: 4.8
},
{
id: 4,
name: 'Mechanical Keyboard',
description: 'RGB backlit with customizable switches',
price: 159.99,
category: 'Accessories',
image: '/images/keyboard.jpg',
inStock: true,
rating: 4.6
},
{
id: 5,
name: 'USB-C Hub',
description: '7-in-1 adapter with 4K HDMI and SD card reader',
price: 79.99,
category: 'Accessories',
image: '/images/hub.jpg',
inStock: true,
rating: 4.3
},
{
id: 6,
name: 'Wireless Earbuds',
description: 'True wireless with active noise cancellation',
price: 199.99,
category: 'Audio',
image: '/images/earbuds.jpg',
inStock: true,
rating: 4.4
}
];
// Filter state
searchQuery = '';
selectedCategories: string[] = [];
sortBy: SortOption = 'name';
showOutOfStock = true;
// Computed property for unique categories
get categories(): string[] {
return [...new Set(this.products.map(p => p.category))].sort();
}
// Computed property for filtered and sorted products
get filteredProducts(): Product[] {
let filtered = this.products;
// Filter by search query
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(p =>
p.name.toLowerCase().includes(query) ||
p.description.toLowerCase().includes(query)
);
}
// Filter by selected categories
if (this.selectedCategories.length > 0) {
filtered = filtered.filter(p =>
this.selectedCategories.includes(p.category)
);
}
// Filter out of stock if needed
if (!this.showOutOfStock) {
filtered = filtered.filter(p => p.inStock);
}
// Sort products
return this.sortProducts(filtered);
}
get hasActiveFilters(): boolean {
return this.searchQuery.trim() !== '' ||
this.selectedCategories.length > 0 ||
!this.showOutOfStock;
}
private sortProducts(products: Product[]): Product[] {
const sorted = [...products];
switch (this.sortBy) {
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'price-low':
return sorted.sort((a, b) => a.price - b.price);
case 'price-high':
return sorted.sort((a, b) => b.price - a.price);
case 'rating':
return sorted.sort((a, b) => b.rating - a.rating);
default:
return sorted;
}
}
clearFilters() {
this.searchQuery = '';
this.selectedCategories = [];
this.showOutOfStock = true;
}
setSortOrder(sortOption: SortOption) {
this.sortBy = sortOption;
}
}Template (product-catalog.html)
Styles (product-catalog.css)
How It Works
1. Search with Debouncing
The search input uses debouncing to avoid excessive filtering operations:
This waits 300ms after the user stops typing before updating searchQuery, which triggers the filteredProducts computed property.
2. Reactive Filtering
The filteredProducts getter automatically recalculates when any filter changes:
3. Multiple Checkbox Selection
Category filters use array binding:
Aurelia automatically adds/removes items from the selectedCategories array.
4. Efficient List Rendering
Using key: id tells Aurelia to track products by ID, enabling efficient DOM updates when sorting or filtering:
5. Dynamic CSS Classes
The active sort button and out-of-stock cards use class binding:
Variations
Add Price Range Filter
Add to Cart Functionality
Persist Filters in URL
Use the router to save filter state:
Related
Last updated
Was this helpful?