Leveraging facets, sorting, and pagination

This is for:

Developer

Facets, sorting, and pagination are essential components of any commerce interface. These features enhance the user experience by making it easier for users to find what they’re looking for when interacting with lists of products.

Headless provides a way to use these features through sub-controllers.

Note

In the following sections, you’ll notice that the Headless front-end doesn’t specify details about what should be returned by the Commerce API, such as which facets are available, the types of sorting criteria, and how many items are displayed per page. Due to the declarative nature of the Commerce API, this information is determined by the configuration associated with the Commerce API request

Using the sub-controllers

Common functionalities like facets, sorting, and pagination are implemented using sub-controllers in Headless.

Depending on the solution you’re implementing—whether it’s Search, Listing, or Recommendations—you can use the appropriate controller to access the sub-controller you need:

To access the sub-controllers, you first need to build the parent controller.

Example

The following example showcases how to use the Search controller to access the Sort sub-controller:

import { buildSearch } from '@coveo/headless/commerce';

const searchController = buildSearch(engine); 1
const searchSortSubController = searchController.sort(); 2
1 Build the Search controller by passing in the previously initialized engine to the buildSearch method.
2 Access the Sort sub-controller by calling the sort method on the parent controller.

The sections in the remainder of this article assume the relevant controller has been built and the sub-controller has been accessed. The code samples are agnostic to whether the sub-controller is accessed through the Search, Listing, or Recommendations controller.

For a complete example of how to build a controller and access sub-controllers, see the usage of the Search controller in this sample project in the Headless repository.

Facets

Facets allow users to filter search results by specific attributes, making it easier to find relevant items.

Facet generator

Facets are accessed using the FacetGenerator sub-controller on your Search or ProductListing controller. This sub-controller provides a list of facets that can be rendered in the UI.

There are four different types of facets:

  • Regular

  • NumericalRange

  • Category

  • DateRange

The following code snippet showcases how to render facets using the FacetGenerator based on their type:

import { FacetGenerator as HeadlessFacetGenerator } from '@coveo/headless/commerce';
import { useEffect, useState } from 'react';
import CategoryFacet from '../category-facet/category-facet';
import DateFacet from '../date-facet/date-facet';
import NumericFacet from '../numeric-facet/numeric-facet';
import RegularFacet from '../regular-facet/regular-facet';

interface IFacetGeneratorProps {
  controller: HeadlessFacetGenerator;
}

export default function FacetGenerator(props: IFacetGeneratorProps) {
  const {controller} = props;

  const [facetState, setFacetState] = useState(controller.facets);

  useEffect(() => { 1
    controller.subscribe(() => {
      setFacetState(controller.facets);
    });
  }, [controller]);

  if (facetState.length === 0) {
    return null;
  }

  return (
    <ul className="Facets">
      {facetState.map((facet, index) => { 2
        switch (facet.type) {
          case 'regular':
            return <RegularFacet key={index} controller={facet}/>;
          case 'numericalRange':
            return <NumericFacet key={index} controller={facet}/>;
          case 'dateRange':
            return <DateFacet key={index} controller={facet}/>;
          case 'hierarchical':
            return (
              <CategoryFacet key={index} controller={facet}/>;
            );
          default:
            return null;
        }
      })}
    </ul>
  );
}
1 Subscribe to the controller to update the state when the facets change.
2 Get the current list of facets array returned by the facet generator and render the appropriate component based on the facet type.

Next, let’s look at how to implement the different types of facet components.

Implementing facet components

As an example, we’ll now look at how to implement the Regular facet component. Implementation details for the other types of facets can be found in sample project in the Headless repository.

import {RegularFacet as HeadlessRegularFacet} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface IRegularFacetProps {
  controller: HeadlessRegularFacet;
}

export default function RegularFacet(props: IRegularFacetProps) {
  const {controller} = props;

  const [state, setState] = useState(controller.state);

  useEffect(() => {
    controller.subscribe(() => setState(controller.state));
  }, [controller]);

  const renderFacetValues = () => { 1
    return (
      <ul className="FacetValues">
        {state.values.map((value) => ( 2
          <li className="FacetValue" key={value.value}>
            <input 3
              className="FacetValueCheckbox"
              type="checkbox"
              checked={value.state !== 'idle'}
              onChange={() => controller.toggleSelect(value)}
            ></input>
            <label className="FacetValueName">{value.value}</label>
            <span className="FacetValueNumberOfResults">
              {' '}
              ({value.numberOfResults})
            </span>
          </li>
        ))}
      </ul>
    );
  };

  return (
    <li className="RegularFacet">
      <h3 className="FacetDisplayName">{state.displayName ?? state.facetId}</h3>
      <button 4
        className="FacetClear"
        disabled={!state.hasActiveValues}
        onClick={controller.deselectAll}
      >
        Clear
      </button>
      {renderFacetValues()}
      <button
        className="FacetShowMore"
        disabled={!state.canShowMoreValues}
        onClick={controller.showMoreValues}
      >
        Show more
      </button>
      <button
        className="FacetShowLess"
        disabled={!state.canShowLessValues}
        onClick={controller.showLessValues}
      >
        Show less
      </button>
    </li>
  );
}
1 Create a renderFacetValues function to render the facet values to the UI.
2 Iterate through each value in the state.values array and render the checkbox, label, and number of results. If the user selects a value, call the toggleSelect method on the controller to select or deselect it.
3 Render the checkbox input element and bind the toggleSelect method to the onChange event. Additionally, if the value is selected, set the checked attribute to true.
4 Create buttons for various user functionalities. When a user clicks the buttons, call the corresponding method on the controller to interact with the facet.

Sorting

Sorting lets users order results based on certain criteria like relevance, popularity, or price.

Important

Metadata keys defined in variant and availability data can be used for filtering with facets, but can’t be used for sorting results.

import {
  Sort as HeadlessSort,
  SortBy,
  SortCriterion,
} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface ISortProps {
  controller: HeadlessSort;
}

export default function Sort(props: ISortProps) {
  const {controller} = props;

  const [state, setState] = useState(controller.state);

  useEffect(() => {
    controller.subscribe(() => setState(controller.state));
  }, [controller]);

  if (state.availableSorts.length === 0) {
    return null;
  }

  const getSortLabel = (criterion: SortCriterion) => { 1
    switch (criterion.by) {
      case SortBy.Relevance:
        return 'Relevance';
      case SortBy.Fields:
        return criterion.fields.map((field) => field.displayName).join(', ');
    }
  };

  return (
    <div className="Sort">
      <label htmlFor="sport-select">Sort by: </label>
      <select 2
        name="sorts"
        id="sorts-select"
        value={JSON.stringify(state.appliedSort)}
        onChange={(e) => controller.sortBy(JSON.parse(e.target.value))}
      >
        {state.availableSorts.map((sort, index) => ( 3
          <option
            key={index}
            value={JSON.stringify(sort)}
            onSelect={() => controller.sortBy(sort)}
          >
            {getSortLabel(sort)}
          </option>
        ))}
      </select>
    </div>
  );
}
1 Create a getSortLabel function to return the label for the sort criterion based on its type. If current results are sorted by relevance, return Relevance, otherwise stringify the fields and return the value.
2 Use a select element to display the available sort options, using the string representation of the applied sort as the value. When the user selects a new sort criterion, call the sortBy method on the controller to sort the results by the selected criterion.
3 Iterate through the availableSorts and render an option element for each sort criterion. When the user selects a sort criterion, call the sortBy method on the controller to sort the results by the selected criterion.

Pagination

Pagination breaks large sets of results into smaller, manageable pages, improving navigation.

import {Pagination as HeadlessPagination} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';

interface IPaginationProps {
  controller: HeadlessPagination;
}

export default function Pagination(props: IPaginationProps) {
  const {controller} = props;

  const [state, setState] = useState(controller.state);

  useEffect(() => {
    controller.subscribe(() => setState(controller.state));
  }, [controller]);

  const renderPageRadioButtons = () => { 1
    return Array.from({length: state.totalPages}, (_, i) => {
      const page = i + 1;
      return (
        <label className="SelectPage" key={page}>
          <input
            type="radio"
            name="page"
            value={page - 1}
            checked={state.page === page - 1}
            onChange={() => controller.selectPage(page - 1)}
          />
          {page}
        </label>
      );
    });
  };

  return (
    <div className="Pagination">
      <button 2
        className="PreviousPage"
        disabled={state.page === 0}
        onClick={controller.previousPage}
      >
        {'<'}
      </button>
      {renderPageRadioButtons()}
      <button
        className="NextPage"
        disabled={state.page === state.totalPages - 1}
        onClick={controller.nextPage}
      >
        {'>'}
      </button>
    </div>
  );
}
1 Create a renderPageRadioButtons function to render radio buttons for each page. When the user selects a page, call the selectPage method on the controller to navigate to the selected page.
2 Display buttons to navigate to the previous and next pages. When the user clicks the buttons, call the previousPage and nextPage methods on the controller to navigate to the previous and next pages, respectively.