Home
Wiki
Home
Wiki
  1. 2. Components
  • Back to home
  • 1. Themes
  • Vs Code
    • Getting Started
  • Kitchenware
    • Layout
      • New Layout
      • Legacy Layout
    • Components
      • Announcement
      • Banner Carousel
      • Banner With Products Carousel
      • Blog Category List
      • Blog List
      • Brand List
      • Brands Carousel
      • Breadcrumb
      • Call To Action
      • Cart
      • Categories List
      • Change Password
      • Checkout
      • Cookie Manager
      • Filter list
      • Footer
      • Forgot Password
      • Form
      • Hero Carousel
      • Icon Block
      • Invitation
      • Last Visited Products
      • Layout
      • Login
      • Map
      • Nav Bar
      • Offer
      • Product Attachments
      • Product Attributes
      • Product Documentation
      • Product Expected
      • Product Modal
      • Products Block
      • Products Carousel
      • Product Single
      • Profile
      • Quote
      • Register
      • Related Products
      • Search
      • Stores
      • Subscribe Newsletter
      • Text with Image
      • Top Bar
      • Video
    • Reusables
      • Getting Started
    • Assets
      • Getting Started
    • SDK
      • Products
        • _findProductsByCategory
        • _findProductsByIds
        • _findProductsByTitle
        • _findProductsByFilter
        • _findProductsByCriteria
        • _findProductsAndCalculate
        • _findProductsThenCalculate
        • _getProductAttributeSet
        • _setLastVisited
      • Categories
        • _findCategoryTreeById
        • _findCategoriesByIds
        • _findCategoryByAlias
        • _findCategoryTreeByAlias
        • _getCategoryContent
      • Collections
        • _getCollectionContent
        • _findCollectionsByIds
        • _findCollectionsByIdsThenCalculate
      • Brands
        • _getBrandContent
        • _findBrandsByIds
      • Cart
        • _addToCartMulti
        • _addToCart
        • _setCart
        • _clearCart
        • _setCartListener
        • _removeFromCart
        • _calculateCart
      • Checkout
        • _startCheckout
        • _updateCheckout
        • _completeCheckout
      • Shopping Lists
        • _getShoppingLists
        • _updateShoppingList
        • _createShoppingList
        • _deleteShoppingList
        • _getShoppingListByAlias
      • Navigation
        • _getFooterMenu
        • _getHeaderMenu
      • Users
        • _getUserById
      • Utils
        • _calculateCurrency
        • _getCurrencySymbol
        • _getCulture
        • _subscribeToNewsletter
        • _findUnitsByIds
  • Noir
    • 0. Introduction
    • 1. Structure
      • Overview
      • LayoutA.liquid
      • ComponentsList.liquid
      • Metas.liquid
      • CssVariables.liquid
      • Json.liquid
      • GoogleTagManager.liquid
      • StagingButton.liquid
    • 2. Components
      • Overview
      • Announcement
      • BannerCarousel
      • BlogCategoryList
      • BlogList
      • BrandList
      • Breadcrumb
      • Cart
      • CategoriesList
      • ChangePassword
      • Checkout
      • CookieManager
      • FilterList
      • Footer
      • ForgotPassword
      • Form
      • IconBlock
      • Invitation
      • LastVisitedProducts
      • Login
      • Map
      • NavBar
      • ProductAttachments
      • ProductAttributes
      • ProductComparison
      • ProductDocumentation
      • ProductMixList
      • ProductsBlock
      • ProductsCarousel
      • ProductSingle
      • Profile
      • Register
      • RelatedProducts
      • SingleBlog
      • Stores
      • TextWithImage
      • ThankYouPage
      • TopBar
      • Wishlist
    • 3. Reusables
      • Overview
      • Addresses
      • BillingRetail
      • AddressForm
      • AnnouncementModal
      • BackToTop
      • Company
      • General
      • Login
      • LoginModal
      • Orders
      • Payment
      • ProductAttachments
      • ProductAttributes
      • ProductComparisonButton
      • ProductComparisonFloatingButton
      • ProductGridItem
      • ProductListItem
      • ShoppingListsButton
      • ProductModal
      • ProfileInfo
      • PromptModal
      • Register
      • Shipping
      • ShoppingLists
      • ShoppingListsNavbar
      • Toast
      • Users
      • VariantContent
      • WishlistButton
      • Services
    • 4. Assets
      • Fonts
      • Images
      • Templates
      • Javascript
        • Overview
        • theme.js
      • Css / Scss
        • Overview
        • ThemeClasses
    • 5. SDK
      • Overview
      • LiquidGlobals
      • ServicesSDK
  1. 2. Components

FilterList

Purpose#

The FilterList component is the Noir component that drives the UI of a product listing page (collection / search / category results). It typically covers:
filters (facets)
sorting
page size selection
view toggle (Grid/List)
pagination
(optional) “Product Mix / Assortment” toggle when available

Inputs (model contract)#

Model shape (storefront example)#

{
  "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",
    "...": "..."
  }
}

JavaScript#

Global object#

The FilterList component exposes a global object:
It’s consumed from Liquid via Alpine using a factory call (so Liquid can pass runtime values into JS).
The factory returns an Alpine component object that provides:
listing state (view, page, sort, etc.)
filter UI controllers (mobile wrapper, per-panel state)
helpers for specific filter types (price slider, search-within-lines, color chips)
product-mix toggle navigation
pagination navigation helper

initComponent(previewOptions, page, pageSize, sort, totalPages, defaultView, products, headerTitle)#

What it does
Creates the main Alpine state for the FilterList page:
view: resolved by priority:
1.
defaultView
2.
previewOptions?.[0]
3.
"Grid" fallback
filtersOn: default open/closed based on breakpoint:
open on desktop (≥ 1440)
closed on mobile (< 1440)
stores page, pageSize, sort, totalPages, products, headerTitle
Exposes a bunch of “sub-components” as functions:
filtersWrapper()
filterPanel(slug, wrapperEl)
priceSlider(...)
searchComponent(lines, slug)
setColor(textColor)
productMixToggle(initialEnabled, enableUrl, disableUrl)
redirectToState(newPage)
Advice
The breakpoint 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()#

What it does
Fires a GA4 “view item list” event when the component initializes:
prepareListProducts(this.products)
sendGAEvent("viewItemList", { listName: this.headerTitle, items })
Advice
headerTitle becomes the list name. Make sure it’s stable (e.g., “Products”, “Search results”) so reporting stays clean.
If products is large, consider whether prepareListProducts already trims fields (it usually does). Avoid sending unnecessary payload.

filtersWrapper()#

Returns an Alpine sub-scope that controls the overall filter UI state.
Returned state#
isMobile: window.innerWidth < 1440
active: currently active filter panel slug (mobile only)
init()#
What it does
Initializes isMobile
Registers a resize handler that updates isMobile
Advice
No cleanup is performed (listener removal). OK for full page navigation.
If you introduce partial navigation, add a guard to avoid multiple listeners.
handleResize()#
What it does
Recomputes isMobile
If switching to desktop (!isMobile), resets:
active = null
Advice
This prevents “stuck” mobile state when crossing breakpoints.
handleOutside()#
What it does
On mobile only:
closes the filters overlay by setting:
this.filtersOn = false
this.active = null
Advice
Note that filtersOn 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)#

Returns an Alpine sub-scope for one filter group/panel (e.g., “brands”, “availability”).
Returned state#
slug: the panel slug
open: default open on desktop, closed on mobile
wrapper: reference to wrapper data scope
showAll: “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 mode
init()#
What it does
Grabs the wrapper Alpine scope:
this.wrapper = Alpine.$data(wrapperEl)
Watches wrapper.active:
on mobile, opens only if active === slug
Watches wrapper.filtersOn:
when mobile filters are open → locks body scroll (overflow: hidden)
when closed → restores body scrolling
Registers window resize listener bound to handleResize
Advice
The body scroll lock is important for mobile overlays. If you remove it, the page will scroll behind the filter drawer.
If you create other overlays/modals, coordinate scroll locking so they don’t fight each other.
togglePanel()#
What it does
Mobile:
toggles wrapper.active between this panel slug and null
Desktop:
toggles local open
Advice
On desktop, panels can expand/collapse independently.
On mobile, only one panel is “active” at a time.
handleResize()#
What it does
Re-applies body scroll lock rule depending on state
If mobile:
sets open = (wrapper.active === slug)
If desktop:
resets to desktop defaults:
showAll = false
open = true
mobileFullView = false
Advice
This ensures consistent UX when rotating device or resizing browser.
openMobileFullView() / closeMobileFullView()#
What they do
Toggle mobileFullView to enter/exit full-screen mode for an individual filter group on mobile.
Advice
Useful for long lists (brands, attributes). Keep this if your mobile filter UI supports a “drill-in” experience.

priceSlider(minPrice, maxPrice, minInput, maxInput)#

Controls the price range filter UI and integrates with noUiSlider if it’s available.
Returned state#
minPrice, maxPrice: absolute range limits from backend
currentMin, currentMax: user-selected values
sliderUpdateTimeout: used to debounce typing → slider updates
init()#
What it does
1.
Reads current URL parameters:
minPrice, maxPrice from window.location.search
2.
If URL contains valid values, uses them; otherwise uses defaults.
3.
Constrains values to the allowed range.
4.
Fixes invalid ordering (min > max) by resetting to defaults.
5.
On next tick:
writes values into inputs #minPrice and #maxPrice
calls initializeSlider()
Advice
This is intentionally URL-driven so page refresh/back/forward preserve the price selection.
The input element ids (minPrice, maxPrice) are hard dependencies.
initializeSlider()#
What it does
Finds wrapper element #price-filter-wrapper
Creates a noUiSlider instance only if:
window.noUiSlider exists, and
there isn’t already a slider attached (!wrapper.noUiSlider)
On slider update:
updates currentMin/currentMax
writes values into the min/max input fields
Advice
This degrades gracefully: if noUiSlider isn’t loaded, the inputs still work.
If the slider wrapper id changes in Liquid, update the selector here.
updatePriceFilter(minValue, maxValue)#
What it does
Called when the user types in inputs.
Updates currentMin/currentMax without constraining while the user is typing (better UX).
Requests a slider update via updateSliderPosition().
Advice
Not constraining during typing avoids “fighting” the user when they enter multi-digit numbers.
updateSliderPosition()#
What it does
Debounces slider updates (300ms) to avoid excessive calls while typing.
If slider exists:
constrains values to valid range for slider display
ensures min ≤ max for slider
calls wrapper.noUiSlider.set([finalMin, finalMax])
Catches and ignores slider errors, logging a debug message.
Advice
Silent failure here is intentional: it prevents UI crashes from transient invalid states while typing.
If you want stricter debugging, upgrade console.debug to console.warn in dev builds only.
applyPriceFilter()#
What it does
Applies final constraints (only when the user submits/applies).
Builds a new URL based on the current location:
removes existing minPrice and maxPrice
adds new ones only when different from defaults
resets page=1
Navigates:
window.location.href = newUrl
Advice
Resetting to page 1 is important; otherwise you can land on a page number that doesn’t exist for the filtered result set.
This uses full navigation, not AJAX. That matches the platform approach.

searchComponent(lines, slug)#

Provides “search within filter options” (useful for long brand/attribute lists) and optional voice search.
Returned state#
searchText
slug
lines: compressed list containing only { value, slug }
lineVisible(value, index)#
What it does
If search input is empty:
shows only the first 10 lines unless showAll is enabled
If search input has text:
shows line when value includes the search term (case-insensitive)
Advice
This references this.showAll. That typically comes from the parent panel scope (Alpine scope chaining). Ensure this is nested correctly.
If value can be null, guard before calling .toLowerCase().
triggerVoiceSearch()#
What it does
Uses Web Speech API:
SpeechRecognition / webkitSpeechRecognition
If unsupported:
shows alert(...)
Otherwise starts recognition with:
lang = "en-US"
On result:
sets searchText to transcript
Advice
Consider setting recognition.lang dynamically based on site language (e.g., el-GR) for better results.
Replace alert with $store.toast if you want consistent theme UX.

setColor(textColor)#

Used for “Color” attributes that include textColor values.
Returned state#
rawColor: the provided textColor string
color: array of 0/1/2 colors
bodyBg: computed body background color
init()#
What it does
If rawColor is:
length 7 → treats as one hex (e.g., #ffffff)
longer → splits into two hex colors (supports two-tone swatches)
otherwise → empty
Advice
The “two colors in one string” convention is a theme-specific trick. Keep it documented because it’s not a standard API shape.
getColorClass(colorVal, i)#
What it does
Detects if the swatch color is identical to the page background.
If so, returns classes that draw helper borders so white-on-white swatches remain visible.
Advice
This is an important accessibility/visibility enhancement for very light colors.
normalizeColor(color)#
What it does
Converts transparent to white.
Converts hex to rgb via hexToRgb.
Returns rgb strings for comparison.
hexToRgb(hex)#
What it does
Converts a hex string to rgb(r, g, b).

productMixToggle(initialEnabled, enableUrl, disableUrl)#

What it does
Creates a simple toggle controller used by the assortment feature.
Normalizes enabled:
accepts boolean true or string "true"
On toggle, navigates to:
enableUrl or disableUrl
toggleProductMix()#
What it does
Full navigation to the target URL depending on the toggle state.
Advice
This assumes the backend produces correct URLs that preserve other listing state (sort, pageSize, filters). Make sure enable/disable URLs include all necessary params.

redirectToState(newPage = null)#

What it does
If newPage is a string:
navigates directly to that URL
If newPage is a number:
builds a new querystring containing only page=<n>
navigates to pathname?page=<n>
If newPage is null/undefined:
reloads the current page URL (path + existing search)
Advice
Important: for numeric page navigation this drops all other query params (filters, sort, pageSize, view, etc.), because it creates a new URLSearchParams() from scratch.
If your listing uses query params for filters/sort/pageSize, a safer implementation is:
const params = new URLSearchParams(window.location.search); params.set("page", Number(newPage));
Prefer using the prebuilt pagination URLs from pageData.paginationUrls.pageUrls when available. That guarantees consistency with backend routing.

Dependencies#

URLs must be correctly produced by the backend/storefront layer.
If a price slider UI is used, the theme may rely on a vendor library (e.g. noUiSlider) that’s already loaded.

Notes#

The per-line filterUrl is the “contract”. If the backend URL format changes, it must remain compatible with the theme.
The 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).

Extras#

Template behavior (Liquid + Alpine)#

Active filters & “Clear all”#

Active filters are usually represented by:
line.selected: true in filterData[*].lines[*]
and/or pageData.activeFilters
“Clear all” navigates using:
pageData.clearAllFiltersUrl

Template behavior (Liquid + Alpine)#

Modified at 2026-04-14 13:18:56
Previous
CookieManager
Next
Footer
Built with