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
| Name | Type | Required | Description |
|---|---|---|---|
| currentFilters | FilterInput[] | Yes | Active filters array |
| availableAttributes | FilterableAttribute[] | No | Filterable product attributes |
| priceRange | PriceRange | Yes | Min/max price range |
| categories | CategoryFilter[] | No | Available categories |
| setting | object | No | Store settings (language, currency) |
| onFilterUpdate | (filters: FilterInput[]) => void | No | Custom filter update handler |
| children | RenderFunction | Yes | Render function receiving filter props |
Render Props
| Name | Type | Description |
|---|---|---|
| currentFilters | FilterInput[] | Current active filters |
| availableAttributes | FilterableAttribute[] | Filterable attributes |
| priceRange | PriceRange | Price min/max values |
| categories | CategoryFilter[] | Available categories |
| addFilter | (key, operation, value) => void | Add a filter |
| removeFilter | (key) => void | Remove filter by key |
| removeFilterValue | (key, value) => void | Remove specific value from filter |
| toggleFilter | (key, operation, value) => void | Toggle filter on/off |
| clearAllFilters | () => void | Clear all active filters |
| updateFilter | (filters) => void | Update all filters at once |
| hasFilter | (key) => boolean | Check if filter exists |
| getFilterValue | (key) => string | undefined | Get filter value by key |
| isOptionSelected | (attributeCode, optionId) => boolean | Check if option is selected |
| isCategorySelected | (categoryId) => boolean | Check if category is selected |
| getSelectedCount | (attributeCode) => number | Count selected options for attribute |
| getCategorySelectedCount | () => number | Count selected categories |
| isLoading | boolean | True when filters are updating |
| activeFilterCount | number | Number 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:
- URL parameter update
- GraphQL page data fetch
- 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
Related Components
- CategoryContext - Category page context
- ProductContext - Product page context