Standard Filters Backend and Frontend Technical Details

Edit on GitHub

Backend 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.