# Product List Component

The Product List component provides a filterable, searchable product catalog with infinite scroll for building category pages and search results.

## Overview

The Product List component:
- Displays products in a responsive grid
- Supports infinite scroll pagination
- Provides search functionality
- Offers advanced filtering options
- Links to product detail pages
- Shows real-time availability
- Supports add-to-cart from list view

## Basic Usage

### Declarative Setup

Use data attributes to configure the product list:

```html
<script
  defer
  data-liquid-commerce-elements
  data-token="YOUR_API_KEY"
  data-env="production"
  src="https://elements.reservebar-worker.workers.dev/all/elements.js"
></script>

<div
  data-liquid-commerce-elements-products-list="my-collection-slug"
  data-rows="3"
  data-columns="4"
  data-filters="price,brands,categories"
  data-product-url="/product/{grouping}"
></div>
```

**Attributes:**
- `data-liquid-commerce-elements-products-list`: Product list container; value is the collection slug
- `data-rows`: Number of rows to display (default: 3)
- `data-columns`: Number of columns (default: 4)
- `data-filters`: Comma-separated filter types
- `data-product-url`: URL pattern for product detail pages (optional)

`data-product-url` accepts a string template with one of two placeholders:
- `{grouping}` — replaced with the product's salsifyGrouping ID
- `{upc}` — replaced with the selected size's UPC

If your PDP URLs aren't derivable from a single placeholder (e.g. each
product has a hand-curated marketing slug), use the
[Product URL Map](#product-url-map) instead.

### With Search and Filters

Separate containers for search and filters:

```html
<!-- Search container -->
<div data-liquid-commerce-elements-products-list-search="my-collection-slug"></div>

<!-- Filters container -->
<div data-liquid-commerce-elements-products-list-filters="my-collection-slug" data-filters="price,brands,fulfillment"></div>

<!-- Product list -->
<div
  data-liquid-commerce-elements-products-list="my-collection-slug"
  data-rows="4"
  data-columns="3"
  data-product-url="/products/{grouping}"
></div>
```

### Programmatic Setup

Use JavaScript for dynamic configuration:

```javascript
const client = await Elements('YOUR_API_KEY', { env: 'production' });

// Inject product list
await client.injectProductList({
  containerId: 'products',
  slug: 'my-collection-slug',
  rows: 3,
  columns: 4,
  filters: ['price', 'brands', 'categories', 'fulfillment'],
  productUrl: '/product/{grouping}'
});

// Inject search (optional)
await client.injectProductListSearch({
  containerId: 'search',
  slug: 'my-collection-slug'
});

// Inject filters (optional)
await client.injectProductListFilters({
  containerId: 'filters',
  slug: 'my-collection-slug',
  filters: ['price', 'brands']
});
```

## Available Filters

The following filter type values can be used in the `filters` array:

| Filter Value   | Description                              |
|----------------|------------------------------------------|
| `'price'`      | Price range slider with min/max values   |
| `'brands'`     | Checkboxes for available brands          |
| `'categories'` | Category selection checkboxes            |
| `'fulfillment'`| Shipping vs. on-demand delivery toggle   |
| `'engraving'`  | Filter by personalization support        |
| `'sizes'`      | Filter by product size/volume            |
| `'flavor'`     | Filter by flavor profile                 |
| `'region'`     | Filter by region of origin               |
| `'variety'`    | Filter by product variety                |
| `'vintage'`    | Filter by vintage year                   |
| `'country'`    | Filter by country of origin              |
| `'appellation'`| Filter by appellation                    |
| `'materials'`  | Filter by materials                      |

```javascript
// Example: use multiple filters
filters: ['price', 'brands', 'categories', 'fulfillment', 'sizes']
```

## URL Query Param Filters

The product list auto-applies filters from the page URL on first load. Useful for category landing pages, "shop the look" links, marketing emails, or any flow where you want to deep-link into a pre-filtered list.

### Whitelist

Only filter keys that are configured for the list are honored — anything else in the URL is silently ignored. The whitelist resolves in this priority order:

1. `data-filters` on `<div data-liquid-commerce-elements-products-list>` (use this when the page does **not** mount a filters UI but you still want URL filtering — e.g. a curated category page).
2. `data-filters` on the matching `<... -products-list-filters>` container (the common case when a filters panel is mounted).
3. `availableFilters` from the theme config for the list slug.
4. `filters` array passed to `injectProductList(...)` programmatically.

### Supported formats

| Filter | URL format | Example |
| --- | --- | --- |
| Multi-value (`brands`, `categories`, `flavor`, `region`, `variety`, `vintage`, `country`, `appellation`, `materials`, `sizes`) | Comma-separated **or** repeated keys | `?brands=Bacardi,Glenlivet` or `?brands=Bacardi&brands=Glenlivet` |
| `fulfillment` | Single value: `all`, `shipping`, or `onDemand` | `?fulfillment=shipping` |
| `engraving` | `true` or `false` | `?engraving=true` |
| `price` | `min-max` range; `min-` or `-max` are accepted | `?price=20-150`, `?price=20-`, `?price=-150` |

Invalid values are dropped (e.g. `?fulfillment=garbage`, `?price=abc` — no error, the filter just isn't applied). Combining params is supported:

```
https://yoursite.com/best-sellers?brands=Bacardi&categories=Wine&price=20-150&fulfillment=shipping
```

### Behavior

- URL params win over any state persisted from a previous session.
- Once the list mounts, the filters panel (if present) reflects the seeded values, and the initial product fetch is filtered.
- Subsequent in-page interactions (toggling filters, scrolling, etc.) do not write back to the URL — the URL is read-only at load time.

### Standalone use (no filters component)

URL filtering works without injecting a filters panel. Declare the whitelist on the products-list container itself:

```html
<div
  data-liquid-commerce-elements-products-list="curated-page"
  data-filters="price,brands,categories"
  data-rows="4"
  data-columns="4"
></div>
```

Now `https://yoursite.com/curated-page?brands=Bacardi&price=20-150` filters the list on load even though no filters UI is present.

## Search Functionality

### Search Box

The search component provides full-text search across:
- Product names
- Descriptions
- Brand names
- Categories
- SKUs/UPCs

### Search Behavior

- Real-time search as user types (debounced)
- Minimum 2 characters to trigger search
- Highlights matching terms in results
- Shows result count
- "Clear search" button appears when active

### Programmatic Search

Search is controlled through the injected search component. Use `injectProductListSearch()` to add a search box that automatically filters the associated product list.

## Grid Layout

### Responsive Grid

The grid automatically adjusts for screen sizes:

**Desktop** (> 1024px):
- Uses configured columns (e.g., 4 columns)

**Tablet** (768px - 1024px):
- Reduces to 3 or 2 columns

**Mobile** (< 768px):
- Single column or 2 columns depending on space

### Configuring Layout

```javascript
await client.injectProductList({
  containerId: 'products',
  slug: 'my-collection',
  rows: 5,        // Number of rows per page
  columns: 4      // Columns in grid (desktop)
});
```

Total products per page = rows × columns (e.g., 5 × 4 = 20 products)

## Infinite Scroll

### How It Works

1. Initial products load (rows × columns)
2. User scrolls to bottom
3. Next page loads automatically
4. Appends to existing products
5. Continues until all products shown

### Loading States

Shows loading indicator:
- On initial load
- When loading next page
- When applying filters
- When searching

### End of Results

When all products are shown:
- Infinite scroll stops
- Shows "No more products" message
- Scroll to top button may appear

## Product Cards

Each product card shows:

- Product image
- Product name
- Brand
- Price (or price range for multiple sizes)
- Rating (if available)
- "Quick View" or "View Details" button
- "Add to Cart" button (optional)
- Availability indicator

### Card Interaction

**Click on card:** Navigate to product detail page (if `productUrl` configured — see [Product URL Map](#product-url-map) for partner-owned PDP URLs that aren't derivable from a token).

**Quick Add:** Add product to cart directly from list view (if enabled)

**Click on image:** Open image in lightbox or navigate to product page

## Customization

### Theme Configuration

Product list theming is applied per-list using the collection slug as a key:

```javascript
const client = await Elements('YOUR_API_KEY', {
  env: 'production',
  customTheme: {
    productList: {
      theme: {
        backgroundColor: '#ffffff'
      },
      layout: {
        lists: {
          'my-collection-slug': {
            productCard: {
              style: 'card',           // 'card' or 'ghost'
              cornerRadius: '8px',
              showPrice: true,
              showSizes: true,
              showRetailerName: true,
              showFulfillmentOptions: true,
              enableShippingFulfillment: true,
              enableOnDemandFulfillment: true,
              enablePersonalization: true,
              showQuantityCounter: true,
              enablePreCart: true,
              showCollectionTags: false
            },
            presentationMode: 'drawer',  // 'drawer' or 'modal'
            rows: 4,
            columns: 3
          }
        }
      }
    }
  }
});
```

See [Configuration Reference](../api/configuration.md#product-list-theme) for the complete list of options.

## Use Cases

### Category Page

```html
<!DOCTYPE html>
<html>
<head>
  <title>Whiskey Collection</title>
  <script
    defer
    data-liquid-commerce-elements
    data-token="YOUR_API_KEY"
    data-env="production"
    src="https://elements.reservebar-worker.workers.dev/all/elements.js"
  ></script>
</head>
<body>
  <h1>Whiskey Collection</h1>
  
  <!-- Search -->
  <div data-liquid-commerce-elements-products-list-search="whiskey-collection"></div>

  <div class="catalog">
    <!-- Filters sidebar -->
    <aside>
      <div data-liquid-commerce-elements-products-list-filters="whiskey-collection" data-filters="price,brands,sizes"></div>
    </aside>

    <!-- Product grid -->
    <main>
      <div
        data-liquid-commerce-elements-products-list="whiskey-collection"
        data-rows="4"
        data-columns="3"
        data-product-url="/whiskey/{grouping}"
      ></div>
    </main>
  </div>
</body>
</html>
```

### Search Results Page

```javascript
import { Elements } from '@liquidcommerce/elements-sdk';

const client = await Elements('YOUR_API_KEY', { env: 'production' });

// Inject product list with search
await client.injectProductList({
  containerId: 'search-results',
  slug: 'all-products',
  rows: 5,
  columns: 4,
  filters: ['price', 'brands', 'categories']
});

await client.injectProductListSearch({
  containerId: 'search-box',
  slug: 'all-products'
});
```

### Custom Product URL Patterns

Different URL patterns for different product types:

```javascript
// Get product type from data
const productType = getProductTypeFromData();

let urlPattern;
switch (productType) {
  case 'whiskey':
    urlPattern = '/spirits/whiskey/{grouping}';
    break;
  case 'wine':
    urlPattern = '/wine/{grouping}';
    break;
  default:
    urlPattern = '/products/{grouping}';
}

await client.injectProductList({
  containerId: 'products',
  slug: 'all-products',
  rows: 3,
  columns: 4,
  productUrl: urlPattern
});
```

### Product URL Map

For partners whose PDPs have hand-curated URLs that aren't derivable from a
single placeholder (e.g. dedicated marketing pages, Shopify handles, WordPress
slugs), pass a **map** instead of a string template. Keys are product
identifiers — either a UPC or a salsifyGrouping ID, the same identifier types
accepted by `injectProductElement`. The card looks up UPC first, then grouping
ID; products not in the map render without a link.

#### Declarative — JSON script tag

Drop a single `<script type="application/json">` tag anywhere on the page,
keyed by list slug → identifier → URL. Generate it server-side from your CMS.

```html
<script data-liquid-commerce-elements-product-urls type="application/json">
{
  "best-sellers": {
    "GROUPING-33277": "/wines/macallan-12-special-edition",
    "00832889005513": "/spirits/cabernet-2018-club-only"
  },
  "limited-releases": {
    "GROUPING-78941": "/exclusive/pappy-23-allocation"
  }
}
</script>

<div data-liquid-commerce-elements-products-list="best-sellers"></div>
```

When both `data-product-url` and a slug entry in this script are present for
the same list, the **map wins** — it's the more specific intent.

##### Shopify Liquid

```liquid
<script data-liquid-commerce-elements-product-urls type="application/json">
{
  "best-sellers": {
    {% for p in collections.best-sellers.products %}
      "{{ p.metafields.lc.grouping_id }}": "{{ p.url }}"{% unless forloop.last %},{% endunless %}
    {% endfor %}
  }
}
</script>
```

##### WordPress / WooCommerce (PHP)

```php
<script data-liquid-commerce-elements-product-urls type="application/json">
<?= json_encode(['best-sellers' => $lc_identifier_to_pdp_url_map]) ?>
</script>
```

#### Programmatic

```javascript
await client.injectProductList({
  containerId: 'products',
  slug: 'best-sellers',
  rows: 3,
  columns: 4,
  productUrl: {
    'GROUPING-33277': '/wines/macallan-12-special-edition',
    '00832889005513': '/spirits/cabernet-2018-club-only',
  },
});
```

## Events

While product list events are primarily internal, you can listen for cart events when users add products:

```javascript
window.addEventListener('lce:actions.cart_item_added', (event) => {
  const { itemId, quantity } = event.detail.data;
  console.log(`Product added from list: ${itemId}`);
});
```

## Accessibility

The product list component includes:

- Keyboard navigation support
- Screen reader labels
- ARIA attributes for filters and search
- Focus management
- High contrast support

### Keyboard Shortcuts

- `Tab`: Navigate between products and filters
- `Enter/Space`: Select product or toggle filter
- `Escape`: Clear search or close filters
- `Arrow keys`: Navigate grid (when focused)

## Performance

### Optimization Features

- **Image lazy loading**: Images load as they enter viewport
- **Virtual scrolling**: Only renders visible products
- **Debounced search**: Reduces API calls during typing
- **Filter caching**: Caches filter results
- **Progressive loading**: Loads in batches

### Large Catalogs

For catalogs with thousands of products:

```javascript
await client.injectProductList({
  containerId: 'products',
  slug: 'all-products',
  rows: 3,
  columns: 4,
  filters: ['price', 'brands'],  // Limit filters to most useful
  // More rows = larger pages = fewer API calls
});
```

## Best Practices

### Provide Clear Navigation

```html
<nav class="breadcrumb">
  <a href="/">Home</a> &gt;
  <a href="/products">Products</a> &gt;
  <span>Whiskey</span>
</nav>
```

### Show Result Counts

The product list component automatically displays the number of results. You can also track cart additions from the list using standard cart events:

```javascript
window.addEventListener('lce:actions.cart_item_added', (event) => {
  console.log('Item added from product list:', event.detail.data);
});
```

### Mobile-First Design

Ensure filters work well on mobile:

```css
@media (max-width: 768px) {
  .filters-sidebar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    transform: translateY(100%);
    transition: transform 0.3s;
  }
  
  .filters-sidebar.open {
    transform: translateY(0);
  }
}
```

### Default to Relevant Filters

For category pages, pre-select relevant filters:

```javascript
// On whiskey category page
await client.injectProductList({
  containerId: 'products',
  slug: 'whiskey-collection',
  rows: 4,
  columns: 3,
  filters: ['price', 'brands', 'sizes']  // Most relevant for whiskey
});
```

## Troubleshooting

### Products Not Loading

1. Check browser console for errors
2. Verify API key is correct
3. Ensure container ID exists
4. Check network tab for API responses
5. Verify products exist in catalog

### Filters Not Working

1. Ensure filter types are spelled correctly
2. Check that products have filterable attributes
3. Verify theme config allows filters
4. Look for JavaScript errors

### Infinite Scroll Not Triggering

1. Check container has finite height
2. Verify scroll event listeners are attached
3. Ensure there are more products to load
4. Check console for errors

### Search Not Finding Products

1. Verify minimum character count is met (2 chars)
2. Check search is not case-sensitive (it shouldn't be)
3. Ensure products have searchable text fields
4. Look for API errors in network tab

## See Also

- [Product Component](./product-component.md) - Individual product display
- [Cart Component](./cart-component.md) - Add products to cart
- [Theming](./theming.md) - Customize appearance
- [Events](./events.md) - Available events
