Filters Module
The Filters module provides URL-based collection filtering with automatic server-side section rerendering. Every filter change updates the URL and rerenders registered widgets by fetching fresh HTML from the server.
URL Format
# Search
?q=shoes
# Multi-select
?filters[gender][]=men&filters[gender][]=women
# Search-text
?filters[material][]=cotton
# Range
?filters[price][min]=100&filters[price][max]=500
# Sort
?sort=price-asc
# Combined
?q=shoes&filters[gender][]=men&filters[price][min]=50&filters[price][max]=200&sort=price-desc
Quick Start
1. Register a section
Tell the filters module which widget to rerender and where to put the HTML:
// Programmatic
qumra.filters.section('products-grid', '#products-container');
Or use the HTML attribute (auto-detected):
<div data-qumra-section="products-grid">
{% widget "products-grid" %}
</div>
2. Use filters
// Toggle a filter — auto-rerenders the section
await qumra.filters.toggle('color', 'red');
// Set a price range
await qumra.filters.range('price', { min: 100, max: 500 });
// Search
await qumra.filters.search('shoes');
// Sort
await qumra.filters.sort('price-asc');
// Clear everything
await qumra.filters.clear();
How It Works
1. Developer registers section: qumra.filters.section('products-grid', '#container')
— or — SDK auto-detects <div data-qumra-section="products-grid">
2. User clicks a filter
3. FiltersModule syncs the URL (history.replaceState)
4. FiltersModule sends: GET /collection/shoes?widgetKey=products-grid&filters[color][]=red
5. Server renders only the widget — returns HTML fragment
6. FiltersModule replaces innerHTML of the target element
7. FiltersModule emits collection:filtered
API Reference
qumra.filters.section(widgetKey, target)
Register a widget for auto-rerendering when filters change.
| Param | Type | Description |
|---|---|---|
widgetKey | string | The widget key (folder name in widgets/) |
target | string | CSS selector for the container element |
qumra.filters.section('products-grid', '#products-container');
qumra.filters.section('sidebar-filters', '#filters-sidebar');
Multiple sections can be registered — they all rerender in parallel.
qumra.filters.add(name, value) → Promise<void>
Add a value to a multi-select filter.
await qumra.filters.add('gender', 'men');
await qumra.filters.add('gender', 'women');
// URL: ?filters[gender][]=men&filters[gender][]=women
qumra.filters.toggle(name, value) → Promise<void>
Toggle a value on/off. If the value exists, it's removed; otherwise, it's added.
// First call — adds
await qumra.filters.toggle('gender', 'men');
// Second call — removes
await qumra.filters.toggle('gender', 'men');
qumra.filters.range(name, { min?, max? }) → Promise<void>
Set a range filter with min and/or max bounds.
await qumra.filters.range('price', { min: 100, max: 500 });
// URL: ?filters[price][min]=100&filters[price][max]=500
// Min only
await qumra.filters.range('price', { min: 50 });
qumra.filters.searchText(name, value) → Promise<void>
Set a search-text filter. The value is free text matched against keywords defined in the filter configuration. Replaces any previous value for this filter.
await qumra.filters.searchText('material', 'cotton');
// URL: ?filters[material][]=cotton
// Empty string removes the filter
await qumra.filters.searchText('material', '');
qumra.filters.remove(name, value?) → Promise<void>
Remove a specific value from a filter, or remove the entire filter.
// Remove specific value
await qumra.filters.remove('gender', 'men');
// Remove entire filter
await qumra.filters.remove('gender');
qumra.filters.clear() → Promise<void>
Clear all active filters and search query. Emits collection:reset.
await qumra.filters.clear();
qumra.filters.search(query) → Promise<void>
Set search query and rerender.
await qumra.filters.search('shoes');
// URL: ?q=shoes
// Combine search + filters
await qumra.filters.search('shoes');
await qumra.filters.add('gender', 'men');
// URL: ?q=shoes&filters[gender][]=men
qumra.filters.sort(value) → Promise<void>
Set sort order and rerender. Emits collection:sorted.
await qumra.filters.sort('price-asc');
// URL: ?sort=price-asc
Supported values:
| Value | Description |
|---|---|
createdAt-asc | Oldest first |
createdAt-desc | Newest first (default) |
price-asc | Cheapest first |
price-desc | Most expensive first |
rating-asc | Lowest rated first |
rating-desc | Highest rated first |
qumra.filters.apply(state, options?) → Promise<void>
Apply multiple filters at once — sends a single rerender. Optionally set sort, search query, and merge behavior in the same call.
| Param | Type | Description |
|---|---|---|
state | FiltersState | Filters to apply |
options.merge | boolean | Merge with existing filters instead of replacing (default: false) |
options.sort | string | Set sort order in the same commit |
options.q | string | Set search query in the same commit |
// Replace all filters (default)
await qumra.filters.apply({
gender: ['men', 'women'],
brand: ['nike'],
price: { min: 100, max: 500 }
});
// Merge — adds/updates gender and brand, keeps other existing filters
await qumra.filters.apply({
gender: ['men'],
brand: ['nike'],
}, { merge: true });
// Filters + sort + search in a single commit (one rerender)
await qumra.filters.apply({
gender: ['men', 'women'],
price: { min: 100, max: 500 },
}, {
sort: 'price-asc',
q: 'shoes',
});
// Sort + query only, keep existing filters
await qumra.filters.apply({}, {
sort: 'price-desc',
q: 'sneakers',
merge: true,
});
Why use apply over individual methods?
Individual calls trigger a rerender each:
// 3 rerenders
await qumra.filters.toggle('gender', 'men');
await qumra.filters.range('price', { min: 100, max: 500 });
await qumra.filters.sort('price-asc');
apply does the same in one rerender:
// 1 rerender
await qumra.filters.apply({
gender: ['men'],
price: { min: 100, max: 500 },
}, { sort: 'price-asc' });
qumra.filters.rerender() → Promise<void>
Re-render sections with current filters (no state change).
await qumra.filters.rerender();
qumra.filters.get(name?)
Get the current filter state (no network request).
// Get all filters
const all = qumra.filters.get();
// { gender: ['men', 'women'], price: { min: 100, max: 500 } }
// Get a specific filter
const gender = qumra.filters.get('gender');
// ['men', 'women']
// Returns null if filter doesn't exist
qumra.filters.get('color'); // null
qumra.filters.getQuery()
Get the current search query.
const q = qumra.filters.getQuery(); // "shoes"
qumra.filters.getSort()
Get the current sort order.
const sort = qumra.filters.getSort(); // "price-asc"
qumra.filters.has(name, value?)
Check if a filter or specific value is active.
qumra.filters.has('gender'); // true
qumra.filters.has('gender', 'men'); // true
qumra.filters.has('color'); // false
qumra.filters.hasAny()
Check if any filters are currently active.
if (qumra.filters.hasAny()) {
showClearButton();
}
qumra.filters.count()
Count the number of active filter groups.
qumra.filters.count(); // 2
Loading State
While the section is being fetched, the target element gets a data-qumra-loading="true" attribute:
[data-qumra-loading="true"] {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s;
}
Events
| Event | When | Payload |
|---|---|---|
collection:filtered | After any filter change | { filters, query } |
collection:sorted | After sort() | { sort } |
collection:reset | After clear() | — |
section:rendering | Before section fetch starts | { widgetKey } |
section:rendered | After section DOM swap | { widgetKey, html } |
qumra.on(qumra.EVENTS.COLLECTION.FILTERED, (payload) => {
updateFilterUI(payload.data.filters);
});
qumra.on(qumra.EVENTS.SECTION.RENDERED, (payload) => {
// Re-initialize any JS inside the new HTML
console.log('Rerendered:', payload.data.widgetKey);
});
Full Example
<div class="collection-page">
<!-- Filters -->
<aside>
<h3>Color</h3>
<label>
<input type="checkbox" data-qumra-filter="color" data-value="red"> Red
</label>
<label>
<input type="checkbox" data-qumra-filter="color" data-value="blue"> Blue
</label>
<h3>Price</h3>
<input type="number" id="min-price" placeholder="Min">
<input type="number" id="max-price" placeholder="Max">
<button id="apply-price">Apply</button>
<button data-qumra-filter-clear>Clear All</button>
</aside>
<!-- Products — auto-rerenders on filter change -->
<main data-qumra-section="products-grid">
{% widget "products-grid" %}
</main>
</div>
<style>
[data-qumra-loading="true"] {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s;
}
</style>
<script>
// Price range (needs manual binding for two inputs)
document.getElementById('apply-price').addEventListener('click', async () => {
const min = +document.getElementById('min-price').value;
const max = +document.getElementById('max-price').value;
await qumra.filters.range('price', { min, max });
// Section auto-rerenders — no extra code needed
});
</script>
Zero JavaScript needed for the checkbox filters — data-qumra-filter and data-qumra-section handle everything.