Standard Filters Backend and Frontend Technical Details
Edit on GitHubBackend technical details
The backend part of the Standard Filters feature is located in the following modules:
- ProductCategoryFilter (
spryker/product-category-filter
) - ProductCategoryFilterGui (
spryker/product-category-filter-gui
) - ProductCategoryFilterStorage (
spryker/product-category-filter-storage
)
Category Filters management is described in the Back Office guide.
Frontend technical details
The CatalogPage
module (spryker-shop/catalog-page
) provides all applicable product filters and a basic set of templates used by all pages.
The core of each page is page-layout-catalog.twig
, which extends another global template—page-layout-main.twig
.
The general look of the page-layout-catalog.twig
template is as follows:
src/Pyz/Yves/CatalogPage/Theme/default/templates/page-layout-catalog/page-layout-catalog.twig
{% extends model('component') %}
{% define config = {
name: 'filter-section',
tag: 'section',
} %}
{% define data = {
facets: [],
filterPath: null,
categories: [],
isEmptyCategoryFilterValueVisible: null,
} %}
{% set isContentPresent = data.facets | length > 0 %}
{% block class %}
{{ parent() }}
{{ config.jsName }}
{% endblock %}
{% block body %}
{% if isContentPresent %}
<h3 class="{{ config.name ~ '__title' }} is-hidden-lg-xxl">{{ 'catalog.filter.and.sorting.button' | trans }}</h3>
<button class="{{ config.name ~ '__close' }} is-hidden-lg-xxl js-catalog-filters-trigger">
{% include atom('icon') with {
data: {
name: 'cross',
},
} only %}
</button>
<div class="{{ config.name ~ '__sorting ' ~ config.jsName ~ '__sorting' }} is-hidden-lg-xxl"></div>
<div class="{{ config.name ~ '__holder' }}">
{% for filter in data.facets %}
{% set filterHasValues = filter.values is not defined or filter.values | length > 0 %}
{% set togglerClass = '' %}
{% if filterHasValues %}
{% block filters %}
{% if filter.config.type == 'price-range' and can('SeePricePermissionPlugin') is empty %}
{% else %}
<div class="{{ config.name ~ '__item' }} {% if filter.name == 'category' %}{{ config.name ~ '__item--hollow' }}{% endif %}">
<h6 class="{{ config.name ~ '__item-title toggler-accordion__item ' ~ config.jsName ~ '__trigger' ~ '-' ~ filter.name}} {% if filter.name == 'category' %}{{ 'is-hidden-lg-xxl' }}{% endif %}">
{{ ('product.filter.' ~ filter.name | lower) | trans }}
{% include atom('icon') with {
class: 'toggler-accordion__icon',
modifiers: ['small'],
data: {
name: 'caret-down',
},
} only %}
</h6>
{% set contentModifier = filter.name == 'category' ? config.name ~ '__item-content--hollow' : '' %}
{% set hiddenClassToToggleSections = filter.name == 'category' ? 'is-hidden-sm-md' : 'is-hidden' %}
{% set toglerClass = config.name ~ '__item-content ' ~ config.jsName ~ '__' ~ filter.name ~ ' ' ~ hiddenClassToToggleSections ~ ' ' ~ contentModifier %}
{% include [
molecule('filter-' ~ filter.config.name, 'CatalogPage'),
molecule('filter-' ~ filter.config.type, 'CatalogPage'),
] ignore missing with {
data: {
filterPath: data.filterPath,
categories: data.categories,
filter: filter,
parameter: filter.config.parameterName | default(''),
min: filter.min | default(0),
max: filter.max | default(0),
activeMin: filter.activeMin | default(0),
activeMax: filter.activeMax | default(0),
isEmptyCategoryFilterValueVisible: data.isEmptyCategoryFilterValueVisible,
},
class: toglerClass,
} only %}
{% include molecule('toggler-click') with {
attributes: {
'trigger-selector': '.' ~ config.jsName ~ '__trigger-' ~ filter.name,
'target-selector': '.' ~ config.jsName ~ '__' ~ filter.name,
'class-to-toggle': hiddenClassToToggleSections,
'trigger-class-to-toggle': 'active',
},
} only %}
</div>
{% endif %}
{% endblock %}
{% endif %}
{% endfor %}
</div>
<button type="submit" class="button button--expand button--big {{ config.name ~ '__button' }}">{{ 'catalog.filter.button' | trans }}</button>
{% endif %}
{% endblock %}
<br>
```twig
{% extends template('page-layout-main') %}
{% define data = {
products: required,
facets: required,
category: null,
categories: [],
categoryId: null,
filterPath: null,
viewMode: null,
pagination: {
currentPage: required,
maxPage: required,
parameters: app.request.query.all(),
paginationPath: app.request.getPathInfo(),
showAlwaysFirstAndLast: true
}
} %}
{% macro renderBreadcrumbSteps(categoryNode, isLastLeaf, filterPath) %}
{% import _self as self %}
{% set categoryUrl = categoryNode.url | default %}
{% set categoryUrl = filterPath is not empty ? url(filterPath, {categoryPath: categoryUrl}) : categoryUrl %}
{% set categoryLabel = categoryNode.name | default %}
{% set categoryParentNodes = categoryNode.parents | default %}
{% if categoryParentNodes is not empty %}
{{ self.renderBreadcrumbSteps(categoryParentNodes | first, false, filterPath) }}
{% if not isLastLeaf %}
{% include molecule('breadcrumb-step') with {
data: {
url: categoryUrl,
label: categoryLabel
}
} only %}
{% endif %}
{% endif %}
{% endmacro %}
{% block breadcrumbs %}
{% import _self as self %}
{% embed molecule('breadcrumb') with {
embed: {
breadcrumbs: self.renderBreadcrumbSteps(data.category, false, data.filterPath)
}
} only %}
{% block breadcrumbs %}
{{ embed.breadcrumbs }}
{% endblock %}
{% endembed %}
{% endblock %}
{% block contentClass %}page-layout-main page-layout-main--catalog-page{% endblock %}
{% block content %}
<form method="GET" class="grid grid--gap js-form-input-default-value-disabler__catalog-form page-layout-main--catalog-page-content">
{% block form %}
{% include molecule('form-input-default-value-disabler') with {
attributes: {
'form-selector': '.js-form-input-default-value-disabler__catalog-form',
'input-selector': '.js-form-input-default-value-disabler__catalog-input'
}
} only %}
<div class="col col--sm-12 col--lg-4 col--xl-3">
{% block filterBar %}
{% include molecule('view-mode-switch', 'CatalogPage') with {
class: 'is-hidden-sm-md',
data: {
viewMode: data.viewMode
}
} only %}
<button class="button button--justify button--additional js-catalog-filters-trigger is-hidden-lg-xxl spacing-bottom spacing-bottom--big">
{{ 'catalog.filter.and.sorting.button' | trans }}
{% include atom('icon') with {
modifiers: ['filter'],
data: {
name: 'filter'
}
} only %}
</button>
{% include molecule('toggler-click') with {
attributes: {
'trigger-selector': '.js-catalog-filters-trigger',
'target-selector': '.js-filter-section',
'class-to-toggle': 'is-hidden-sm-md',
'fix-body': 'true',
'class-to-fix-body': 'is-locked-mobile'
}
} only %}
{% include organism('filter-section', 'CatalogPage') with {
class: 'is-hidden-sm-md',
data: {
facets: data.facets,
filterPath: data.filterPath,
categories: data.categories
}
} only %}
{% endblock %}
</div>
<div class="col col--sm-12 col--lg-8 col--xl-9">
<div class="grid grid--column-mob-reverse">
<div class="col col--sm-12">
<div class="grid grid--justify grid--nowrap">
<div class="col col--lg-12">
{% include molecule('sort', 'CatalogPage') only %}
</div>
<div class="col">
{% include molecule('view-mode-switch', 'CatalogPage') with {
class: 'is-hidden-lg-xxl',
data: {
viewMode: data.viewMode
}
} only %}
</div>
</div>
</div>
<div class="col col--sm-12">
{% include organism('active-filter-section', 'CatalogPage') with {
data: {
facets: data.facets
}
} only %}
</div>
</div>
<div class="grid grid--stretch grid--gap">
{% for product in data.products %}
{% widget 'CatalogPageProductWidget' args [
product,
data.viewMode
] only %}
{% endwidget %}
{% endfor %}
</div>
{% include molecule('pagination') with {
data: data.pagination
} only %}
</div>
{% endblock %}
</form>
{% endblock %}
Standard product filters are represented in the form of filter-section organism (filter-section.twig
in particular) inclusion.
Related code is located in the filterBar
section, as shown in the following example (extra code removed):
src/Pyz/Yves/CatalogPage/Theme/default/templates/page-layout-catalog/page-layout-catalog.twig
{% block content %}
<form method="GET" class="grid grid--gap js-form-input-default-value-disabler__catalog-form page-layout-main--catalog-page-content">
{% block form %}
<div class="col col--sm-12 col--lg-4 col--xl-3">
{% block filterBar %}
{% include organism('filter-section', 'CatalogPage') with {
class: 'is-hidden-sm-md',
data: {
facets: data.facets,
filterPath: data.filterPath,
categories: data.categories
}
} only %}
{% endblock %}
</div>
{% endblock %}
</form>
{% endblock %}
When you look closer at the filter-section.twig
template, you may notice that this template is responsible for rendering both Filters and Categories (another feature):
src/Pyz/Yves/CatalogPage/Theme/default/components/organisms/filter-section/filter-section.twig
{% extends model('component') %}
{% define config = {
name: 'filter-section',
tag: 'section',
} %}
{% define data = {
facets: [],
filterPath: null,
categories: [],
isEmptyCategoryFilterValueVisible: null,
} %}
{% set isContentPresent = data.facets | length > 0 %}
{% block class %}
{{ parent() }}
{{ config.jsName }}
{% endblock %}
{% block body %}
{% if isContentPresent %}
<h3 class="{{ config.name ~ '__title' }} is-hidden-lg-xxl">{{ 'catalog.filter.and.sorting.button' | trans }}</h3>
<button class="{{ config.name ~ '__close' }} is-hidden-lg-xxl js-catalog-filters-trigger">
{% include atom('icon') with {
data: {
name: 'cross',
},
} only %}
</button>
<div class="{{ config.name ~ '__sorting ' ~ config.jsName ~ '__sorting' }} is-hidden-lg-xxl"></div>
<div class="{{ config.name ~ '__holder' }}">
{% for filter in data.facets %}
{% set filterHasValues = filter.values is not defined or filter.values | length > 0 %}
{% set togglerClass = '' %}
{% if filterHasValues %}
{% block filters %}
{% if filter.config.type == 'price-range' and can('SeePricePermissionPlugin') is empty %}
{% else %}
<div class="{{ config.name ~ '__item' }} {% if filter.name == 'category' %}{{ config.name ~ '__item--hollow' }}{% endif %}">
<h6 class="{{ config.name ~ '__item-title toggler-accordion__item ' ~ config.jsName ~ '__trigger' ~ '-' ~ filter.name}} {% if filter.name == 'category' %}{{ 'is-hidden-lg-xxl' }}{% endif %}">
{{ ('product.filter.' ~ filter.name | lower) | trans }}
{% include atom('icon') with {
class: 'toggler-accordion__icon',
modifiers: ['small'],
data: {
name: 'caret-down',
},
} only %}
</h6>
{% set contentModifier = filter.name == 'category' ? config.name ~ '__item-content--hollow' : '' %}
{% set hiddenClassToToggleSections = filter.name == 'category' ? 'is-hidden-sm-md' : 'is-hidden' %}
{% set toglerClass = config.name ~ '__item-content ' ~ config.jsName ~ '__' ~ filter.name ~ ' ' ~ hiddenClassToToggleSections ~ ' ' ~ contentModifier %}
{% include [
molecule('filter-' ~ filter.config.name, 'CatalogPage'),
molecule('filter-' ~ filter.config.type, 'CatalogPage'),
] ignore missing with {
data: {
filterPath: data.filterPath,
categories: data.categories,
filter: filter,
parameter: filter.config.parameterName | default(''),
min: filter.min | default(0),
max: filter.max | default(0),
activeMin: filter.activeMin | default(0),
activeMax: filter.activeMax | default(0),
isEmptyCategoryFilterValueVisible: data.isEmptyCategoryFilterValueVisible,
},
class: toglerClass,
} only %}
{% include molecule('toggler-click') with {
attributes: {
'trigger-selector': '.' ~ config.jsName ~ '__trigger-' ~ filter.name,
'target-selector': '.' ~ config.jsName ~ '__' ~ filter.name,
'class-to-toggle': hiddenClassToToggleSections,
'trigger-class-to-toggle': 'active',
},
} only %}
</div>
{% endif %}
{% endblock %}
{% endif %}
{% endfor %}
</div>
<button type="submit" class="button button--expand button--big {{ config.name ~ '__button' }}">{{ 'catalog.filter.button' | trans }}</button>
{% endif %}
{% endblock %}
As you may see from the following code snippet, this part is responsible for rendering a single filter (extra code removed):
src/Pyz/Yves/CatalogPage/Theme/default/components/organisms/filter-section/filter-section.twig
{% block body %}
{% if isContentPresent %}
<div class="{{ config.name ~ '__holder' }}">
{% for filter in data.facets %}
{% if filterHasValues %}
{% block filters %}
{% if filter.config.type == 'price-range' and can('SeePricePermissionPlugin') is empty %}
{% else %}
<div class="{{ config.name ~ '__item' }} {% if filter.name == 'category' %}{{ config.name ~ '__item--hollow' }}{% endif %}">
{% include [
molecule('filter-' ~ filter.config.name, 'CatalogPage'),
molecule('filter-' ~ filter.config.type, 'CatalogPage'),
] ignore missing with {
data: {
filterPath: data.filterPath,
categories: data.categories,
filter: filter,
parameter: filter.config.parameterName | default(''),
min: filter.min | default(0),
max: filter.max | default(0),
activeMin: filter.activeMin | default(0),
activeMax: filter.activeMax | default(0),
isEmptyCategoryFilterValueVisible: data.isEmptyCategoryFilterValueVisible,
},
class: toglerClass,
} only %}
</div>
{% endif %}
{% endblock %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
Thus, each filter is rendered by another molecule according to its name and type.
You can see the list of all available filters by going into the vendor/spryker-shop/catalog-page/src/SprykerShop/Yves/CatalogPage/Theme/default/components/molecules
directory.
Thank you!
For submitting the form