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.for with keys - Efficient list rendering

  • Event 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:

Last updated

Was this helpful?