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:
-
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"; -
Fetch and format your facet data:
- • Use the
validateFacetshelper to process and filter unnecessary facets from your application once fetched.
- • Use the
-
Configure the UI components:
- • Set the required props (see Configuration for details)
-
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.