FilterList component is the Noir component that drives the UI of a product listing page (collection / search / category results). It typically covers:{
"filterData": [
{
"lines": [
{
"id": "Category Id",
"value": "Sample category name",
"slug": "sample-category-slug",
"name": "Sample category label",
"count": 2,
"selected": false,
"filterUrl": "/collection/sample?page=1&pageSize=18&sort=SortDate_desc"
},
"..."
],
"slug": "categories",
"type": "PathCategory"
},
"..."
],
"pageData": {
"title": "Sample listing title",
"alias": "sample-alias",
"page": 1,
"pageSize": 12,
"totalPages": 2,
"totalCount": 23,
"sort": "SortDate_desc",
"displayView": "Grid",
"activeFilters": [],
"clearAllFiltersUrl": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=Grid",
"paginationUrls": {
"nextPageUrl": "/collection/sample?page=2&pageSize=12&sort=SortDate_desc&view=Grid",
"lastPageUrl": "/collection/sample?page=2&pageSize=12&sort=SortDate_desc&view=Grid",
"pageUrls": {
"1": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=Grid",
"2": "/collection/sample?page=2&pageSize=12&sort=SortDate_desc&view=Grid"
}
},
"sortUrls": {
"price_asc": "/collection/sample?page=1&pageSize=12&sort=Price_asc&view=Grid",
"price_desc": "/collection/sample?page=1&pageSize=12&sort=Price_desc&view=Grid",
"title_asc": "/collection/sample?page=1&pageSize=12&sort=Title_asc&view=Grid",
"title_desc": "/collection/sample?page=1&pageSize=12&sort=Title_desc&view=Grid",
"sortDate_asc": "/collection/sample?page=1&pageSize=12&sort=SortDate_asc&view=Grid",
"sortDate_desc": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=Grid"
},
"pageSizeUrls": {
"12": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=Grid",
"24": "/collection/sample?page=1&pageSize=24&sort=SortDate_desc&view=Grid",
"100": "/collection/sample?page=1&pageSize=100&sort=SortDate_desc&view=Grid"
},
"displayViewUrls": {
"grid": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=Grid",
"list": "/collection/sample?page=1&pageSize=12&sort=SortDate_desc&view=List"
}
},
"productData": [
{
"vatIncluded": true,
"vatRate": 24,
"isMpnVisible": false,
"isSkuVisible": true,
"sku": "sample-sku",
"title": "Sample product title",
"subTitle": "",
"alias": "sample-product-alias",
"status": "Active",
"categoryId": "Category Id",
"categoryName": "Sample category name",
"categoryLink": "/category/sample-category",
"tags": [
"sample-tag",
"..."
],
"pathCategories": [
"Path Category Id",
"..."
],
"availability": "Available",
"isBundle": false,
"id": "Product Id",
"companyId": "Company Id",
"link": "product/sample-product",
"mediaItems": [
{
"id": "Media Item Id",
"link": "https://example.com/media/sample.jpg",
"position": 0,
"alt": "sample-image.jpg",
"mediaType": "Image"
},
"..."
],
"maxPrice": 130,
"minPrice": 120,
"maxRetailPrice": 130,
"minRetailPrice": 90.6,
"updateDate": "2025-01-01T00:00:00.0000000+00:00",
"insertDate": "2025-01-01T00:00:00.0000000+00:00",
"variantCount": 5,
"additionalFeatures": {
"icoTags": []
},
"productVariants": [
{
"quantityConstraints": {
"additive": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
},
"absolute": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
}
},
"id": "Variant Id",
"mediaItem": {},
"unitPrice": 120,
"price": 120,
"quantity": 0,
"retail": {
"price": 120,
"unitId": "(UNDEFINED)",
"unitPrice": 120
},
"dimension1ItemId": "Attribute Item Id",
"sellOutOfStock": true,
"requiresShipping": false,
"sku": "sample-sku",
"translation": {},
"canOrder": true,
"additionalFeatures": {},
"bundleItems": [],
"shoppingLists": [],
"startQuantity": 1,
"finalPriceText": "Sample price text",
"finalPrice": 120
},
"..."
],
"attributes": [
{
"attributeId": "Attribute Id",
"attributeItemId": "Attribute Item Id",
"attributeItemValue": "Sample attribute value",
"name": "Sample attribute name",
"slug": "sample-attribute-slug",
"usedAsFilter": true,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true
},
"..."
],
"brand": {
"name": "Sample brand name",
"link": "/brand/sample-brand",
"image": {
"link": "https://example.com/media/sample-brand.jpg"
}
},
"icoTags": [],
"labels": [],
"dimension1": {
"id": "Attribute Dimension Id",
"attributeId": "Attribute Id",
"name": "Sample dimension name",
"translation": {},
"type": "Size",
"usedAsFilter": true,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true,
"items": [
{
"id": "Attribute Item Id",
"value": "Sample value"
},
"..."
]
},
"startPriceText": "Sample price text",
"startPrice": 120,
"hasPriceRange": true,
"inWishlist": false
},
"..."
],
"productMixData": {
"mode": "Manual",
"storefrontBehavior": "OptionalFilter",
"isUserAuthenticated": true,
"showToggle": true,
"isEnabled": false,
"enableUrl": "/collection/sample?productMix=true&page=1",
"disableUrl": "/collection/sample?productMix=false&page=1"
},
"name": "FilterList",
"view": "Default",
"section": "SectionA",
"settings": {
"sortOptions": [
"SortDate",
"Title",
"Price"
],
"previewOptions": [
"Grid",
"List"
],
"pageSizeOptions": [
12,
24,
100
],
"name": "",
"configuredInContentApi": false
},
"translations": {
"gridView": "Sample text",
"catalog": "Sample text",
"listView": "Sample text",
"...": "..."
}
}initComponent(previewOptions, page, pageSize, sort, totalPages, defaultView, products, headerTitle)view: resolved by priority:defaultViewpreviewOptions?.[0]"Grid" fallbackfiltersOn: default open/closed based on breakpoint:page, pageSize, sort, totalPages, products, headerTitlefiltersWrapper()filterPanel(slug, wrapperEl)priceSlider(...)searchComponent(lines, slug)setColor(textColor)productMixToggle(initialEnabled, enableUrl, disableUrl)redirectToState(newPage)1440 is used consistently across the theme to define “desktop”. If design breakpoints change, update it everywhere to keep behavior consistent.products is passed in so analytics can fire viewItemList on init. If you pass an empty list, analytics will still send an event with empty items.init()prepareListProducts(this.products)sendGAEvent("viewItemList", { listName: this.headerTitle, items })headerTitle becomes the list name. Make sure it’s stable (e.g., “Products”, “Search results”) so reporting stays clean.products is large, consider whether prepareListProducts already trims fields (it usually does). Avoid sending unnecessary payload.filtersWrapper()isMobile: window.innerWidth < 1440active: currently active filter panel slug (mobile only)init()isMobileresize handler that updates isMobilehandleResize()isMobile!isMobile), resets:active = nullhandleOutside()this.filtersOn = falsethis.active = nullfiltersOn is in the parent component, not inside wrapper. Alpine resolves it due to scope chaining. Keep wrapper nested under the main component.filterPanel(slug, wrapperEl)slug: the panel slugopen: default open on desktop, closed on mobilewrapper: reference to wrapper data scopeshowAll: “show more” state (used when you want to display more than the default number of lines)mobileFullView: if true, panel uses a full-screen mobile view modeinit()this.wrapper = Alpine.$data(wrapperEl)wrapper.active:active === slugwrapper.filtersOn:overflow: hidden)handleResizetogglePanel()wrapper.active between this panel slug and nullopenhandleResize()open = (wrapper.active === slug)showAll = falseopen = truemobileFullView = falseopenMobileFullView() / closeMobileFullView()mobileFullView to enter/exit full-screen mode for an individual filter group on mobile.priceSlider(minPrice, maxPrice, minInput, maxInput)noUiSlider if it’s available.minPrice, maxPrice: absolute range limits from backendcurrentMin, currentMax: user-selected valuessliderUpdateTimeout: used to debounce typing → slider updatesinit()minPrice, maxPrice from window.location.search#minPrice and #maxPriceinitializeSlider()minPrice, maxPrice) are hard dependencies.initializeSlider()#price-filter-wrappernoUiSlider instance only if:window.noUiSlider exists, and!wrapper.noUiSlider)update:currentMin/currentMaxnoUiSlider isn’t loaded, the inputs still work.updatePriceFilter(minValue, maxValue)currentMin/currentMax without constraining while the user is typing (better UX).updateSliderPosition().updateSliderPosition()wrapper.noUiSlider.set([finalMin, finalMax])console.debug to console.warn in dev builds only.applyPriceFilter()minPrice and maxPricepage=1window.location.href = newUrlsearchComponent(lines, slug)searchTextsluglines: compressed list containing only { value, slug }lineVisible(value, index)showAll is enabledvalue includes the search term (case-insensitive)this.showAll. That typically comes from the parent panel scope (Alpine scope chaining). Ensure this is nested correctly.value can be null, guard before calling .toLowerCase().triggerVoiceSearch()SpeechRecognition / webkitSpeechRecognitionalert(...)lang = "en-US"searchText to transcriptrecognition.lang dynamically based on site language (e.g., el-GR) for better results.alert with $store.toast if you want consistent theme UX.setColor(textColor)textColor values.rawColor: the provided textColor stringcolor: array of 0/1/2 colorsbodyBg: computed body background colorinit()rawColor is:#ffffff)getColorClass(colorVal, i)normalizeColor(color)hexToRgb.hexToRgb(hex)rgb(r, g, b).productMixToggle(initialEnabled, enableUrl, disableUrl)true or string "true"enableUrl or disableUrltoggleProductMix()redirectToState(newPage = null)newPage is a string:newPage is a number:page=<n>pathname?page=<n>newPage is null/undefined:URLSearchParams() from scratch.const params = new URLSearchParams(window.location.search); params.set("page", Number(newPage));pageData.paginationUrls.pageUrls when available. That guarantees consistency with backend routing.noUiSlider) that’s already loaded.filterUrl is the “contract”. If the backend URL format changes, it must remain compatible with the theme.RetailPrice group may appear even with no lines, because it’s range-driven.Attribute groups may need specialized UI (colors vs sizes), but navigation remains the same (via filterUrl).line.selected: true in filterData[*].lines[*]pageData.activeFilterspageData.clearAllFiltersUrl