Products
Elements

Overview

Facets provides an out of the box UI for filterable navigation for product listings. The Elements package handles:

  • • Facet loading and state management
  • • Automatic rehydration
  • • Out-the-box UI styling for medium to large viewports
  • • Accessibility features

Installation

npm install @thg-altitude/elements

Components

The facets system consists of the following components:

  • FacetWrapper: Required container component (one per page)
  • FacetResults: Wrapper for facet groups - multiple can be nested within the wrapper i.e Desktop UI and Mobile UI
  • FacetList: Template UI for a simple option list
  • FacetRange: Template UI for a ranged option list
  • FacetSearch: Template UI for an option list with search functionality for filtering options
  • FacetSlider: Template UI for min/max values such a price filtering
  • FacetTabs: Template UI to display currently selected options

Basic Implementation

Follow these steps to implement facets in your application:

  1. Import the components and helpers:

    import {
      FacetWrapper,
      FacetResults,
      FacetList,
      FacetSearch,
      FacetRange,
      FacetSlider,
      FacetTabs,
    } from "@thg-altitude/elements/astro";
    import {
      validateFacets,
      formatQueryString,
    } from "@thg-altitude/elements/facets/helper";
  2. Fetch and format your facet data:

    • • Use the validateFacets helper to process and filter unnecessary facets from your application once fetched.
  3. Configure the UI components:

  4. Setup the client script to subscribe to events:

  • • Use the observer to subscribe to channels when events occur (see Configuration for details)

The below example displays the default UI out of the box for both desktop and mobile views. This UI can be customised either through styling or consuming named slots.

---
import { FacetWrapper, FacetResults, FacetList, FacetSearch, FacetRange, FacetSlider, FacetTabs } from "@thg-altitude/elements/astro";
import { validateFacets } from "@thg-altitude/elements/facets/helper";

const response = await fetch(...)

const { data } = await response.json();
const facets = validateFacets(data?.search?.productList?.facets;);
const numberOfProducts = data?.search?.productList?.total
---
// UI setup
<FacetWrapper
filterName="facetFilters"
options={{
  endpoint: "/api/graphql",
  pageType: "search",
  query: "blue",
  shippingDestination: "GB",
  currency: "GBP",
}}
currencySymbol="£"

>

<div class="xl:block hidden">
  <FacetResults
    id="desktop"
    facetPayload={validFacets}
    hydrateOptions="visible"
    singleActiveFacet="true"
    facetRenderCount={4}
    visibleOptions="*"
    text={{
      toggle: "+ Filters",
    }}
  >
    <FacetList slot="facet-list" text={{ clear: "Clear" }}>
      <div slot="elements-facet-options-header">
        <p>
          <span facet-inject="facet-selected-count"></span>{" "}
          Selected
        </p>
        <p facet-inject="facet-selections"></p>
      </div>
    </FacetList>
    <FacetRange slot="facet-range" text={{ clear: "Clear" }}>
      <div slot="elements-facet-options-header">
        <p>
          <span facet-inject="facet-selected-count"></span>{" "}
          Selected
        </p>
      </div>
    </FacetRange>
    <FacetSlider
      slot="facet-slider"
      text={{
        clear: "Clear",
        currency: "£",
        apply: "Apply",
      }}
    />
    <FacetSearch slot="facet-search" text={{ clear: "Clear" }}>
      <div slot="elements-facet-options-header">
        <p>
          <span facet-inject="facet-selected-count"></span>{" "}
          Selected
        </p>
      </div>
    </FacetSearch>
    <FacetTabs slot="facet-tabs-end" text={{ clear: "Clear All" }} />
  </FacetResults>
</div>

<div class="xl:hidden">
  <button
    id="mobile-facet-trigger"
    class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-md mt-2"
    onclick="complexModal.showModal()"
    aria-haspopup="dialog">Open Filters</button
  >
  <dialog
    id="complexModal"
    class="modal modal-fullscreen modal-fullscreen-mobile"
    role="dialog"
    aria-modal="true"
    aria-label="Fullscreen Modal"
  >
    <div class="modal-box min-h-screen h-screen bg-white">
      <form method="dialog" class="modal-control-bar">
        <button class="modal-close-button">
          <span>Close</span>
          <span aria-hidden="true">✕</span>
        </button>
      </form>
      <FacetResults
        id="mobile"
        facetPayload={validFacets}
        keepSelectedGroupActive={false}
        singleActiveFacet={false}
      >
        <FacetList slot="facet-list" text={{ clear: "Clear" }}>
          <div
            slot="elements-facet-options-header"
            class="flex w-full flex-col gap-2"
          >
            <h3
              class="bold flex border-b pb-2 text-lg"
              facet-inject="facet-group-name-display"
            >
            </h3>
          </div>
        </FacetList>
        <FacetRange slot="facet-range" text={{ clear: "Clear" }}>
          <div
            slot="elements-facet-options-header"
            class="flex w-full flex-col gap-2"
          >
            <h3
              class="bold flex border-b pb-2 text-lg"
              facet-inject="facet-group-name-display"
            >
            </h3>
          </div>
        </FacetRange>
        <FacetSlider
          slot="facet-slider"
          text={{
            clear: "Clear",
            currency: "£",
            apply: "Apply",
          }}
        >
          <div
            slot="elements-facet-options-header"
            class="flex w-full flex-col gap-2"
          >
            <h3
              class="bold flex border-b pb-2 text-lg"
              facet-inject="facet-group-name-display"
            >
            </h3>
          </div>
        </FacetSlider>
        <FacetSearch slot="facet-search" text={{ clear: "Clear" }}>
          <div
            slot="elements-facet-options-header"
            class="flex w-full flex-col gap-2"
          >
            <h3
              class="bold flex border-b pb-2 text-lg"
              facet-inject="facet-group-name-display"
            >
            </h3>
          </div>
        </FacetSearch>
        <FacetTabs slot="facet-tabs-start" text={{ clear: "Clear All" }} />
        <span slot="facet-button" facet-inject="facet-selections"></span>
      </FacetResults>
      <form
        method="dialog"
        class="modal-control-bar pb-0 block mt-auto w-full"
      >
        <button
          id="view-products-button"
          class="bg-blue-950 rounded text-white w-full p-2"
        >
          {
            numberOfProducts > 1
              ? `View ${numberOfProducts} Products`
              : `View ${numberOfProducts} Product`
          }
        </button>
      </form>
    </div>
  </dialog>
</div>

</FacetWrapper>

// Client side script setup

<script>
class ElementsFacetWrapper extends HTMLElement {
constructor() {
  super();
  this.extractSSRContext = this.extractSSRContext.bind(this);
}

async connectedCallback() {
  window.addEventListener("elements-facet-wrapper", this.extractSSRContext);
}

toggleFacet(facetOpen: boolean) {
  const facetToggleSlot = this.querySelector(
    ".elements-facets-more-filters-toggle"
  );
  if (facetToggleSlot) {
    facetToggleSlot.innerHTML = `${!facetOpen ? "+ Filters" : "- Filters"}`;
  }
}

async extractSSRContext() {
  window.removeEventListener(
    "elements-facet-wrapper",
    this.extractSSRContext
  );
  const observer = window.elements.facets.observer;

  observer.subscribe("facet-toggle", (facetOpen: boolean) => {
    this.toggleFacet(facetOpen);
  });
}
}

customElements.get("elements-facet-wrapper") ||
customElements.define("elements-facet-wrapper", ElementsFacetWrapper);
</script>

Custom Styling

Components that the elements package exports all contain default styling using vanilla CSS. The styles can be overriden per application by using class based css selectors. The default styling uses lowest specificity possible. To override a specific class, the wrapper class for the element i.e .elements-facets-wrapper should prefix all selectors in your stylesheet.

.elements-facets-wrapper .elements-facets-dropdown {
  /* Your custom styles */
}

/* Responsive breakpoints handle layout changes */
@media (width < 64rem) {
  .elements-facets-wrapper .elements-facets-dropdown {
      /* Small viewport styles */
  }
}

The default styling aligns with larger viewports due to it’s horizontal layout. But the below code can be added to your applications stylesheet for a vertical layout which might be suitable for smaller viewports.