Integrate instant products and filter suggestions with a search box (CSR)
Integrate instant products and filter suggestions with a search box (CSR)
This is for:
DeveloperAfter building your instant products and filter suggestions components, integrate them into your search box to create a rich search-as-you-type experience.
This is the final step of the instant products and filter suggestions guide.
Initialize the target controllers
In the pages where you include your search box, initialize the InstantProducts and FilterSuggestionsGenerator controllers and pass them to the search box component.
See the layout.tsx file in the sample project for a full example.
// src/layout/layout.tsx
import {
buildFilterSuggestionsGenerator,
buildInstantProducts,
buildStandaloneSearchBox,
type CommerceEngine,
} from '@coveo/headless/commerce';
import StandaloneSearchBox from '../components/standalone-search-box/standalone-search-box.js';
const highlightOptions = {
notMatchDelimiters: {open: '<span>', close: '</span>'},
correctionDelimiters: {open: '<i>', close: '</i>'},
};
interface ILayoutProps {
engine: CommerceEngine;
navigate: (url: string) => void;
}
export default function Layout(props: ILayoutProps) {
const {engine, navigate} = props;
const standaloneSearchBoxId = 'standalone-search-box';
return (
<div className="Layout">
<h1>Coveo Headless Commerce — Search Box Features</h1>
<StandaloneSearchBox
navigate={navigate}
controller={buildStandaloneSearchBox(engine, {
options: {
redirectionUrl: '/search',
id: standaloneSearchBoxId,
highlightOptions,
},
})}
instantProductsController={buildInstantProducts(engine, {
options: {searchBoxId: standaloneSearchBoxId},
})}
filterSuggestionsGeneratorController={buildFilterSuggestionsGenerator(
engine
)}
/>
</div>
);
}
Uses the buildInstantProducts function to create the InstantProducts controller. |
|
Pass a searchBoxId option to tie instant products to this specific search box. |
|
Uses the buildFilterSuggestionsGenerator function to create the FilterSuggestionsGenerator controller. |
Use your components in your search box
In your standalone search box component, use the InstantProducts component to display the instant products and the FilterSuggestionsGenerator component to display the filter suggestions.
See the standalone-search-box.tsx file in the sample project.
Set up imports and props
Start by importing the required Headless controllers and child components.
The search box component accepts the StandaloneSearchBox, InstantProducts, and FilterSuggestionsGenerator controllers as props, along with a navigate function for routing.
import type {
CategoryFacetSearchResult,
CategoryFilterSuggestions,
FilterSuggestions,
FilterSuggestionsGenerator as HeadlessFilterSuggestionsGenerator,
InstantProducts as HeadlessInstantProducts,
StandaloneSearchBox as HeadlessStandaloneSearchBox,
RegularFacetSearchResult,
Suggestion,
} from '@coveo/headless/commerce';
import {useEffect, useRef, useState} from 'react';
import FilterSuggestionsGenerator from '../filter-suggestions/filter-suggestions-generator.js';
import InstantProducts from '../instant-products/instant-products.js';
import './standalone-search-box.css';
interface IStandaloneSearchBoxProps {
navigate: (url: string) => void;
controller: HeadlessStandaloneSearchBox;
instantProductsController: HeadlessInstantProducts;
filterSuggestionsGeneratorController: HeadlessFilterSuggestionsGenerator;
}
Manage component state
Subscribe to the StandaloneSearchBox controller state to keep the component in sync, and track whether the dropdown is visible.
const [state, setState] = useState(controller.state);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
controller.subscribe(() => setState({...controller.state}));
}, [controller]);
const hideDropdown = () => setIsDropdownVisible(false);
const showDropdown = () => setIsDropdownVisible(true);
Define filter suggestion helpers
Create helper functions that iterate over all FilterSuggestions sub-controllers to update or clear filter suggestions whenever the query changes.
const fetchFilterSuggestions = (value: string) => {
for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
filterSuggestions.updateQuery(value);
}
};
const clearFilterSuggestions = () => {
for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
filterSuggestions.clear();
}
};
Define a function to fetch filter suggestions by updating the query on each FilterSuggestions sub-controller. |
|
| Define a function to clear all filter suggestions. |
Handle user input
Define handlers for text input, keyboard events, clearing the search box, and interacting with query suggestions.
Each handler coordinates the StandaloneSearchBox, InstantProducts, and FilterSuggestionsGenerator controllers so that all features update together.
const onSearchBoxInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value === '') {
hideDropdown();
controller.clear();
return;
}
controller.updateText(e.target.value);
controller.showSuggestions();
instantProductsController.updateQuery(e.target.value);
fetchFilterSuggestions(e.target.value);
showDropdown();
};
const onSearchBoxInputKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>
) => {
switch (e.key) {
case 'Escape':
if (isDropdownVisible) {
hideDropdown();
break;
}
if (state.value !== '') {
controller.clear();
clearFilterSuggestions();
instantProductsController.updateQuery('');
break;
}
break;
case 'Enter':
hideDropdown();
controller.submit();
break;
}
};
const onClickSearchBoxClear = () => {
searchInputRef.current!.focus();
hideDropdown();
controller.clear();
clearFilterSuggestions();
instantProductsController.updateQuery('');
};
const onFocusSuggestion = (suggestion: Suggestion) => {
instantProductsController.updateQuery(suggestion.rawValue);
fetchFilterSuggestions(suggestion.rawValue);
};
const onSelectSuggestion = (suggestion: Suggestion) => {
controller.selectSuggestion(suggestion.rawValue);
hideDropdown();
};
| Update instant products with the new query to show relevant products in real time. | |
| When the user types in the search box, update filter suggestions with the new query. | |
| Clear instant products and filter suggestions when the user presses Escape. |
Render the dropdown
Build the dropdown that appears under the search input, displaying query suggestions, instant products, and filter suggestions side by side.
const renderDropdown = () => {
return (
<div className="SearchBoxDropdown row">
{state.suggestions.length > 0 && (
<div className="QuerySuggestion column small">
<p>Query suggestions</p>
<ul>
{state.suggestions.map((suggestion) => (
<li key={`${suggestion.rawValue}-suggestion`}>
<button
type="button"
onMouseOver={() => onFocusSuggestion(suggestion)}
onFocus={() => onFocusSuggestion(suggestion)}
onClick={() => onSelectSuggestion(suggestion)}
// biome-ignore lint/security/noDangerouslySetInnerHtml: highlightedValue is sanitized by Headless
dangerouslySetInnerHTML={{
__html: suggestion.highlightedValue,
}}
/>
</li>
))}
</ul>
</div>
)}
<div className="InstantProducts column small">
<InstantProducts
controller={instantProductsController}
navigate={navigate}
/>
</div>
<div className="FilterSuggestions column small">
<FilterSuggestionsGenerator
controller={filterSuggestionsGeneratorController}
onClickFilterSuggestion={(
fsController: FilterSuggestions | CategoryFilterSuggestions,
value: RegularFacetSearchResult | CategoryFacetSearchResult
) => {
hideDropdown();
const parameters =
fsController.type === 'hierarchical'
? fsController.getSearchParameters(
value as CategoryFacetSearchResult
)
: fsController.getSearchParameters(
value as RegularFacetSearchResult
);
navigate(`/search#${parameters}`);
}}
/>
</div>
</div>
);
};
| Render the dropdown menu displaying query suggestions, instant products, and filter suggestions side by side. | |
| Handle filter suggestion clicks by hiding the dropdown and navigating to the search page with the selected filter applied. | |
| Navigate to the search page with the chosen filter value selected. |
Render the search box
Assemble the final component by combining the search input, clear and submit buttons, and the dropdown.
return (
<div className="Searchbox">
<input
aria-label="Enter query"
className="SearchBoxInput"
id="search-box"
onChange={onSearchBoxInputChange}
onKeyDown={onSearchBoxInputKeyDown}
ref={searchInputRef}
value={state.value}
/>
<button
aria-label="Clear query"
className="SearchBoxClear"
disabled={
state.isLoadingSuggestions || state.isLoading || state.value === ''
}
onClick={onClickSearchBoxClear}
type="reset"
>
X
</button>
<button
aria-label="Submit query"
className="SearchBoxSubmit"
disabled={state.isLoading}
onClick={() => {
controller.submit();
hideDropdown();
}}
title="Submit query"
type="submit"
>
Search
</button>
{isDropdownVisible && renderDropdown()}
</div>
);
| Wire up event handlers that update both instant products and filter suggestions on every user interaction. |
Complete example
Click to expand the complete standalone-search-box.tsx file.
// src/components/standalone-search-box/standalone-search-box.tsx
import type {
CategoryFacetSearchResult,
CategoryFilterSuggestions,
FilterSuggestions,
FilterSuggestionsGenerator as HeadlessFilterSuggestionsGenerator,
InstantProducts as HeadlessInstantProducts,
StandaloneSearchBox as HeadlessStandaloneSearchBox,
RegularFacetSearchResult,
Suggestion,
} from '@coveo/headless/commerce';
import {useEffect, useRef, useState} from 'react';
import FilterSuggestionsGenerator from '../filter-suggestions/filter-suggestions-generator.js';
import InstantProducts from '../instant-products/instant-products.js';
import './standalone-search-box.css';
interface IStandaloneSearchBoxProps {
navigate: (url: string) => void;
controller: HeadlessStandaloneSearchBox;
instantProductsController: HeadlessInstantProducts;
filterSuggestionsGeneratorController: HeadlessFilterSuggestionsGenerator;
}
export default function StandaloneSearchBox(props: IStandaloneSearchBoxProps) {
const {
navigate,
controller,
instantProductsController,
filterSuggestionsGeneratorController,
} = props;
const [state, setState] = useState(controller.state);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
controller.subscribe(() => setState({...controller.state}));
}, [controller]);
const hideDropdown = () => setIsDropdownVisible(false);
const showDropdown = () => setIsDropdownVisible(true);
const fetchFilterSuggestions = (value: string) => {
for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
filterSuggestions.updateQuery(value);
}
};
const clearFilterSuggestions = () => {
for (const filterSuggestions of filterSuggestionsGeneratorController.filterSuggestions) {
filterSuggestions.clear();
}
};
const onSearchBoxInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value === '') {
hideDropdown();
controller.clear();
return;
}
controller.updateText(e.target.value);
controller.showSuggestions();
instantProductsController.updateQuery(e.target.value);
fetchFilterSuggestions(e.target.value);
showDropdown();
};
const onSearchBoxInputKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>
) => {
switch (e.key) {
case 'Escape':
if (isDropdownVisible) {
hideDropdown();
break;
}
if (state.value !== '') {
controller.clear();
clearFilterSuggestions();
instantProductsController.updateQuery('');
break;
}
break;
case 'Enter':
hideDropdown();
controller.submit();
break;
}
};
const onClickSearchBoxClear = () => {
searchInputRef.current!.focus();
hideDropdown();
controller.clear();
clearFilterSuggestions();
instantProductsController.updateQuery('');
};
const onFocusSuggestion = (suggestion: Suggestion) => {
instantProductsController.updateQuery(suggestion.rawValue);
fetchFilterSuggestions(suggestion.rawValue);
};
const onSelectSuggestion = (suggestion: Suggestion) => {
controller.selectSuggestion(suggestion.rawValue);
hideDropdown();
};
const renderDropdown = () => {
return (
<div className="SearchBoxDropdown row">
{state.suggestions.length > 0 && (
<div className="QuerySuggestion column small">
<p>Query suggestions</p>
<ul>
{state.suggestions.map((suggestion) => (
<li key={`${suggestion.rawValue}-suggestion`}>
<button
type="button"
onMouseOver={() => onFocusSuggestion(suggestion)}
onFocus={() => onFocusSuggestion(suggestion)}
onClick={() => onSelectSuggestion(suggestion)}
// biome-ignore lint/security/noDangerouslySetInnerHtml: highlightedValue is sanitized by Headless
dangerouslySetInnerHTML={{
__html: suggestion.highlightedValue,
}}
/>
</li>
))}
</ul>
</div>
)}
<div className="InstantProducts column small">
<InstantProducts
controller={instantProductsController}
navigate={navigate}
/>
</div>
<div className="FilterSuggestions column small">
<FilterSuggestionsGenerator
controller={filterSuggestionsGeneratorController}
onClickFilterSuggestion={(
fsController: FilterSuggestions | CategoryFilterSuggestions,
value: RegularFacetSearchResult | CategoryFacetSearchResult
) => {
hideDropdown();
const parameters =
fsController.type === 'hierarchical'
? fsController.getSearchParameters(
value as CategoryFacetSearchResult
)
: fsController.getSearchParameters(
value as RegularFacetSearchResult
);
navigate(`/search#${parameters}`);
}}
/>
</div>
</div>
);
};
return (
<div className="Searchbox">
<input
aria-label="Enter query"
className="SearchBoxInput"
id="search-box"
onChange={onSearchBoxInputChange}
onKeyDown={onSearchBoxInputKeyDown}
ref={searchInputRef}
value={state.value}
/>
<button
aria-label="Clear query"
className="SearchBoxClear"
disabled={
state.isLoadingSuggestions || state.isLoading || state.value === ''
}
onClick={onClickSearchBoxClear}
type="reset"
>
X
</button>
<button
aria-label="Submit query"
className="SearchBoxSubmit"
disabled={state.isLoading}
onClick={() => {
controller.submit();
hideDropdown();
}}
title="Submit query"
type="submit"
>
Search
</button>
{isDropdownVisible && renderDropdown()}
</div>
);
}
Define a function to fetch filter suggestions by updating the query on each FilterSuggestions sub-controller. |
|
| Define a function to clear all filter suggestions. | |
| Update instant products with the new query to show relevant products in real time. | |
| When the user types in the search box, update filter suggestions with the new query. | |
| Clear instant products and filter suggestions when the user presses Escape. | |
| Render the dropdown menu displaying query suggestions, instant products, and filter suggestions side by side. | |
| Handle filter suggestion clicks by hiding the dropdown and navigating to the search page with the selected filter applied. | |
| Navigate to the search page with the chosen filter value selected. | |
| Wire up event handlers that update both instant products and filter suggestions on every user interaction. |
What’s next
With instant products and filter suggestions integrated into your search box, you have a complete search-as-you-type experience. To continue building your commerce interface, see Displaying products to render full product listings on your search and product listing pages.