Facets, Filters and Attributes

As a Developer within Frontastic, you'll come across the terms Facet, Filter and Attribute quite frequently and they can often be used as synonyms. However, they're also used with multiple meanings. We'll try to clear up the confusion here but we can't promise anything...

In our stack, the meanings for each are:

  • Attribute - A property of a data entity that might be displayed on lists, detail pages etc. For example, size can be an Attribute of a product variant and the product detail page shows the size variant as the text XL.
  • Facet - A search for data entities might return statistics about attribute values among the search result. These statistics are called a Facet. For example, a category has 12 product variants with the color red and eight product variants with the color blue.
  • Filter - A filter is used to restrict the results of a search for data entities, typically using one if its attributes. For example, a customer could filter all the products that have a size equal to XL.

Usually, the Filter possibilities displayed are Facets and the user can Filter by selecting a Facet value which is then applied as a Filter to the previously shown result. In the below example, the Facets are the count of results per attribute value:

Filter Facet Live Example

Within the Facets App in Backstage, a Frontend Manager can select which Attributes should be available to users for Facets and Filters. They can also change the order that they appear to users. All Attributes marked as searchable in the API can be used but they must be activated in the Facet App in Backstage. However, both of these can be overridden if it's written into your code, make sure you talk to your Frontend Manager to make sure there's no overlap. See this article for more info on how Frontend Managers can edit Facets.

Whenever someone opens the Facets App in Backstage, it asks the Catwalk for all searchable attributes with the  ProjectAPI and syncs these to Backstage, these are then stored in the Backstage database of the project as potential Facets in the Facet App and are automatically updated each time it's opened. Any new Facets will be added as deactivated by default.

Catwalk hosts the implementation of the ProjectAPI and it has a database of all enabled Facets from the Facet App in Backstage so it can communicate with your API providers. This means that your Catwalk also hosts a database of your Facets.

Streams can be parametrized on top of what is defined in Backstage, for example, with paging (offset/limit) and filtering (Facets). This allows end-users to interact with streams on a Page by filtering its results or scrolling to later results.

The data fetching from the backend through streams depends on the URL requested by the end-user through the browser (which identifies a Node or Master Page). The Frontastic frontend uses the same URL presented in the browser to fetch the data. Therefore, in order to parametrize a stream, the URL needs to be manipulated.

Facet Architecture Example

The above image shows this flow:

  • The URL of a node is requested from the browser
  • The URL change in the frontend app results into a request for data to the Catwalk backend
  • A Stream of items is returned together with metadata such is totaloffsetlimit and the Facets statistics
  • The Facets from the Stream can be used to render drill down functionality for the end-user
  • If the user selects a filter from such a drill down selection, the URL changes which results in a data request to update the information state of the stream
  • Paging works exactly the same, based on the other stream metadata (see this article for more info on Pagination)

Reading and Displaying Facets

A product-list Stream doesn't only contain items but also facets. This property is filled by the commerce backend with statistics about the products in the stream. The facets array contains an object for each attribute that has Facet calculation enabled (through the Facets app). In the below code, there are the Facets for color and size extracted from this array as examples. In a real-world application you'll probably iterate all Facets to render all possible filters.

import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { compose } from 'redux'
import PropTypes from 'prop-types'
import _ from 'lodash'
import Entity from '../../app/entity'
import UrlHandler from '../../app/urlHandler'

import app from '../../app/app'
import facetConnector from '../../app/connector/facet'
import categoryConnector from '../../app/connector/category'
import urlHandlerConnector from '../../app/connector/urlHandler'

class ExampleProductListFilterTastic extends Component {

    render () {
        const productList = this.props.data.stream

        // ... sanity checks ...

        const colorFacet = _.find(productList.facets, { handle: 'variants.attributes.color' })

        const streamId = this.props.tastic.configuration.stream

        return (<Fragment>
            <div style={{ width: '50%', 'float': 'left' }}>
            <h1>Color Filter:</h1>
            <ul>
                {colorFacet.terms.map((term) => {
                    return this.renderTerm(term, (event) => {
                        event.preventDefault()
                        this.toggleFacetTerm(streamId, colorFacet, term)
                    })
                })}
            </ul>
            </div>
        </Fragment>)
    }

    renderTerm = (term, toggleCallback) => {
        return (<li>
            <a href='#' onClick={toggleCallback}>{term.value}</a>
            ({term.count}) {term.selected ? '+' : ''}
        </li>)
    }

    // ... handlers, see below ...
}

ExampleProductListFilterTastic.propTypes = {
    data: PropTypes.object.isRequired,
    tastic: PropTypes.object.isRequired,

    // ... more props needed for handling, see below ...

    // From facetConnector
    facets: PropTypes.instanceOf(Entity).isRequired,
}

export default compose(
    connect(facetConnector),
    // ... more connectors, see below ...
)(ExampleProductListFilterTastic)

The basic data structure of a Facet looks like this:

{
     "type": "range",
     "handle": "variants.price",
     "key":"variants.price",
     // ... more type specific properties
}

When querying the ProductAPI Catwalk returns Facets which can be grouped into two counting Facets. Term counting is used when you have a finite number of values that it can count the number of times that term comes into play, for example, Color or Brand, which is represented as an object:

{
     "terms": [ 
	{
	     "handle": "Damen",
             "name": "Damen",
             "value": "Damen",
             "count": 903,
             "selected": false
	}
     ]
}

Where handle is the identifier by which the server recognizes the term and value is the (localized) value to be displayed to the user. count is the number of items in the Stream that satisfy a filter for this term and selected indicates if the term is applied as filter right now.

Range counting is when there's an infinite number of values that can be displayed so you can group them together, for example, Price:

{
     "min":3125,
     "max":86125,
     "step":null,
     "value": {
         "min":0,
         "max":0
     },
     "selected": false
}

Where the min and max values on root level determine what values are possible in general for that Facet (depending on current filters) and the value property contains the currently selected range values.

These should then be applied to your Attribute types. For example, a Boolean Attribute type will most likely be a Term Facet and a Number Attribute type will most likely be a Range Facet. If you don't specify this, it will become a Term Facet by default.

The example shows how to simply display a list of all Facet terms including their count and if the term is currently selected for filtering. Displaying a range would work accordingly, mostly with advanced slider elements or input fields.

Filtering by Facet

The above example used a function toggleFacetTerm to select/deselect a Facet term for filtering. This function and the required boilerplate code are shown below:

NOTE: To determine if a filter is applied or not, we don't use the server result but rather the URL parameters. The reason is to avoid race conditions when a user clicks faster than the server handles the requests. It also gives faster feedback after the user has clicked.

import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { compose } from 'redux'
import PropTypes from 'prop-types'
import _ from 'lodash'
import Entity from '../../app/entity'
import UrlHandler from '../../app/urlHandler'

import app from '../../app/app'
import facetConnector from '../../app/connector/facet'
import categoryConnector from '../../app/connector/category'
import urlHandlerConnector from '../../app/connector/urlHandler'

class ExampleProductListFilterTastic extends Component {
    // ... render method ...

    toggleFacetTerm = (streamId, facet, term) => {
        const parameters = this.props.urlHandler.deriveParameters((urlState) => {
            const streamState = urlState.getStream(streamId)
            let filterTerms = this.getSelectedTerms(streamState, facet)

            if (filterTerms.includes(term.handle)) {
                filterTerms = filterTerms.splice(filterTerms.indexOf(term.handle), 1)
            } else {
                filterTerms.push(term.handle)
            }

            streamState.setFilter(
                facet.handle,
                _.isEmpty(filterTerms) ? null : { terms: filterTerms })
        })

        app.getRouter().push(this.props.route.route, parameters)
    }

    getSelectedTerms = (streamState, facet) => {
        return ((streamState.getParameters().facets || {})[facet.handle] || {}).terms || []
    }
}

ExampleProductListFilterTastic.propTypes = {
    data: PropTypes.object.isRequired,
    tastic: PropTypes.object.isRequired,

    // From connector
    route: PropTypes.object.isRequired,

    // From urlHandlerConnector
    urlHandler: PropTypes.instanceOf(UrlHandler),

    // ... more props ...
}

export default compose(
    // ... more connectors ...
    connect(urlHandlerConnector),
    connect((globalState) => {
        let streamParameters = globalState.app.route.parameters.s || {}

        return {
            route: globalState.app.route,
            streamParameters: streamParameters,
        }
    }),
)(ExampleProductListFilterTastic)

Working with Stream URL parameters is done through an object called urlHandler. This encapsulates the parameters from the URL for all Streams in a Node. In order to change Stream parameters you call it method deriveParameters() giving it a callback that will actually do the mutation on a clone of the current parameter urlState. The reason for this is to not change the parameters directly but to receive a new set of (clean) parameters for either direct use (router push) or link creation.

The urlState has methods to access the parameters of a specific stream (there can be an arbitrary amount of Streams in a Node). You need to get hold of the ID of your Stream in question in order to access its parameters: this is possible through the Tastic configuration (see previous example).

You'll still need to extract the currently selected terms of a Facet filter from the parameters yourself. After updating the selection you can call methods on the streamState to manipulate the parameters.

For working with offset and limit there are corresponding methods available on the streamState.

Note: The urlHandler only takes care for parameters of streams! If you have custom parameters in the URL (such as q for search), you need to merge them to the derived parameters before updating the route, otherwise they'll be lost!

For more info, see the Facets App article in the Glossary or Using the Facets App in the Frontend Managers section.


‹ Back to Article List

Next Article ›

Pagination and Sorting with Commerce Backends

Still need help? Contact Us Contact Us