Skip to main content

ProductFilter

Description

A render props component for building product filtering interfaces. Manages filter state, URL updates, and provides helper functions for attribute, category, and price filtering.

Import

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

Usage

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

function ProductFilters({ currentFilters, availableAttributes, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
availableAttributes={availableAttributes}
priceRange={priceRange}
>
{({ addFilter, removeFilter, isOptionSelected }) => (
<div>
{availableAttributes.map(attr => (
<div key={attr.attributeCode}>
<h3>{attr.attributeName}</h3>
{attr.options.map(option => (
<label key={option.optionId}>
<input
type="checkbox"
checked={isOptionSelected(attr.attributeCode, option.optionId.toString())}
onChange={() => addFilter(attr.attributeCode, 'in', option.optionId.toString())}
/>
{option.optionText}
</label>
))}
</div>
))}
</div>
)}
</ProductFilter>
);
}

Props

NameTypeRequiredDescription
currentFiltersFilterInput[]YesActive filters array
availableAttributesFilterableAttribute[]NoFilterable product attributes
priceRangePriceRangeYesMin/max price range
categoriesCategoryFilter[]NoAvailable categories
settingobjectNoStore settings (language, currency)
onFilterUpdate(filters: FilterInput[]) => voidNoCustom filter update handler
childrenRenderFunctionYesRender function receiving filter props

Render Props

NameTypeDescription
currentFiltersFilterInput[]Current active filters
availableAttributesFilterableAttribute[]Filterable attributes
priceRangePriceRangePrice min/max values
categoriesCategoryFilter[]Available categories
addFilter(key, operation, value) => voidAdd a filter
removeFilter(key) => voidRemove filter by key
removeFilterValue(key, value) => voidRemove specific value from filter
toggleFilter(key, operation, value) => voidToggle filter on/off
clearAllFilters() => voidClear all active filters
updateFilter(filters) => voidUpdate all filters at once
hasFilter(key) => booleanCheck if filter exists
getFilterValue(key) => string | undefinedGet filter value by key
isOptionSelected(attributeCode, optionId) => booleanCheck if option is selected
isCategorySelected(categoryId) => booleanCheck if category is selected
getSelectedCount(attributeCode) => numberCount selected options for attribute
getCategorySelectedCount() => numberCount selected categories
isLoadingbooleanTrue when filters are updating
activeFilterCountnumberNumber of active filters

Type Definitions

FilterInput

interface FilterInput {
key: string; // Filter key (attribute code)
operation: 'eq' | 'in' | 'range' | 'gt' | 'lt'; // Filter operation
value: string; // Filter value(s)
}

FilterableAttribute

interface FilterableAttribute {
attributeCode: string;
attributeName: string;
attributeId: number;
options: Array<{
optionId: number;
optionText: string;
}>;
}

PriceRange

interface PriceRange {
min: number;
minText: string;
max: number;
maxText: string;
}

Examples

Attribute Checkboxes

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

function AttributeFilters({ currentFilters, availableAttributes, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
availableAttributes={availableAttributes}
priceRange={priceRange}
>
{({ addFilter, removeFilterValue, isOptionSelected, getSelectedCount }) => (
<div className="filters">
{availableAttributes.map(attr => (
<div key={attr.attributeCode} className="filter-group">
<h3>
{attr.attributeName}
{getSelectedCount(attr.attributeCode) > 0 && (
<span> ({getSelectedCount(attr.attributeCode)})</span>
)}
</h3>
{attr.options.map(option => (
<label key={option.optionId}>
<input
type="checkbox"
checked={isOptionSelected(attr.attributeCode, option.optionId.toString())}
onChange={() => {
if (isOptionSelected(attr.attributeCode, option.optionId.toString())) {
removeFilterValue(attr.attributeCode, option.optionId.toString());
} else {
addFilter(attr.attributeCode, 'in', option.optionId.toString());
}
}}
/>
{option.optionText}
</label>
))}
</div>
))}
</div>
)}
</ProductFilter>
);
}

Price Range Filter

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';
import { useState } from 'react';

function PriceFilter({ currentFilters, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
priceRange={priceRange}
>
{({ addFilter, removeFilter, hasFilter, getFilterValue }) => {
const [min, setMin] = useState(priceRange.min);
const [max, setMax] = useState(priceRange.max);

const applyPriceFilter = () => {
addFilter('price', 'range', `${min}-${max}`);
};

return (
<div className="price-filter">
<h3>Price Range</h3>
<div>
<input
type="number"
value={min}
onChange={(e) => setMin(Number(e.target.value))}
min={priceRange.min}
max={priceRange.max}
/>
<span>to</span>
<input
type="number"
value={max}
onChange={(e) => setMax(Number(e.target.value))}
min={priceRange.min}
max={priceRange.max}
/>
</div>
<button onClick={applyPriceFilter}>Apply</button>
{hasFilter('price') && (
<button onClick={() => removeFilter('price')}>Clear</button>
)}
</div>
);
}}
</ProductFilter>
);
}

Category Filter

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

function CategoryFilter({ currentFilters, categories, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
categories={categories}
priceRange={priceRange}
>
{({ toggleFilter, isCategorySelected }) => (
<div className="category-filter">
<h3>Categories</h3>
{categories.map(category => (
<label key={category.categoryId}>
<input
type="checkbox"
checked={isCategorySelected(category.categoryId.toString())}
onChange={() => toggleFilter('cat', 'in', category.categoryId.toString())}
/>
{category.name}
</label>
))}
</div>
)}
</ProductFilter>
);
}

Active Filters Display

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

function ActiveFilters({ currentFilters, availableAttributes, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
availableAttributes={availableAttributes}
priceRange={priceRange}
>
{({ currentFilters, removeFilter, clearAllFilters, activeFilterCount }) => {
if (activeFilterCount === 0) {
return null;
}

return (
<div className="active-filters">
<h4>Active Filters ({activeFilterCount})</h4>
<div className="filter-tags">
{currentFilters
.filter(f => !['page', 'limit', 'ob', 'od'].includes(f.key))
.map((filter, index) => (
<span key={index} className="filter-tag">
{filter.key}: {filter.value}
<button onClick={() => removeFilter(filter.key)}>×</button>
</span>
))}
</div>
<button onClick={clearAllFilters}>Clear All</button>
</div>
);
}}
</ProductFilter>
);
}

Complete Filter Panel

import { ProductFilter } from '@components/frontStore/catalog/ProductFilter';

function FilterPanel({ currentFilters, availableAttributes, categories, priceRange }) {
return (
<ProductFilter
currentFilters={currentFilters}
availableAttributes={availableAttributes}
categories={categories}
priceRange={priceRange}
>
{({
addFilter,
removeFilterValue,
toggleFilter,
clearAllFilters,
isOptionSelected,
isCategorySelected,
getSelectedCount,
activeFilterCount,
isLoading
}) => (
<div className="filter-panel">
{/* Header */}
<div className="filter-header">
<h2>Filters</h2>
{activeFilterCount > 0 && (
<button onClick={clearAllFilters} disabled={isLoading}>
Clear All ({activeFilterCount})
</button>
)}
</div>

{/* Categories */}
{categories.length > 0 && (
<div className="filter-section">
<h3>Categories</h3>
{categories.map(cat => (
<label key={cat.categoryId}>
<input
type="checkbox"
checked={isCategorySelected(cat.categoryId.toString())}
onChange={() => toggleFilter('cat', 'in', cat.categoryId.toString())}
disabled={isLoading}
/>
{cat.name}
</label>
))}
</div>
)}

{/* Attributes */}
{availableAttributes.map(attr => (
<div key={attr.attributeCode} className="filter-section">
<h3>
{attr.attributeName}
{getSelectedCount(attr.attributeCode) > 0 && (
<span className="count">
({getSelectedCount(attr.attributeCode)})
</span>
)}
</h3>
<div className="filter-options">
{attr.options.map(option => {
const selected = isOptionSelected(
attr.attributeCode,
option.optionId.toString()
);
return (
<label key={option.optionId} className={selected ? 'selected' : ''}>
<input
type="checkbox"
checked={selected}
onChange={() => {
if (selected) {
removeFilterValue(attr.attributeCode, option.optionId.toString());
} else {
addFilter(attr.attributeCode, 'in', option.optionId.toString());
}
}}
disabled={isLoading}
/>
{option.optionText}
</label>
);
})}
</div>
</div>
))}

{/* Loading Overlay */}
{isLoading && (
<div className="loading-overlay">
Updating filters...
</div>
)}
</div>
)}
</ProductFilter>
);
}

Filter Operations

  • eq: Equals (single value)
  • in: In list (comma-separated values)
  • range: Between values (min-max)
  • gt: Greater than
  • lt: Less than

Behavior

URL Management

Filters are synced with URL query parameters. Updates trigger:

  1. URL parameter update
  2. GraphQL page data fetch
  3. History state push

Multi-Value Filters

The in operation supports multiple values (comma-separated). Use addFilter to append values or removeFilterValue to remove specific values.

Reserved Keys

The following filter keys are reserved and excluded from active count: page, limit, ob, od.

Features

  • Render Props Pattern: Flexible UI implementation
  • URL Sync: Filters synced with URL parameters
  • Multi-Value Support: Comma-separated values for 'in' operation
  • Loading State: Shows loading during updates
  • Active Filter Count: Tracks number of active filters
  • Helper Functions: isOptionSelected, hasFilter, etc.
  • Auto Page Reset: Clears page parameter when filtering
  • Type Safe: Full TypeScript support