Last active
October 20, 2025 14:54
-
-
Save Fronix/cbcc8ee66c00fa6986ce76ae95f29512 to your computer and use it in GitHub Desktop.
Custom useRefinementList with operator switch support. It includes a modified version of connectClearRefinements to also make it clear the operator paramaters from the URL, not required for the refinments to work..
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; | |
| import type { | |
| TransformItems, | |
| CreateURL, | |
| WidgetRenderState, | |
| Connector, | |
| ScopedResult | |
| } from 'instantsearch.js'; | |
| import { | |
| createDocumentationMessageGenerator, | |
| checkRendering, | |
| mergeSearchParameters, | |
| getRefinements | |
| } from 'instantsearch.js/es/lib/utils'; | |
| import { noop, uniq } from 'lodash'; | |
| const withUsage = createDocumentationMessageGenerator({ | |
| name: 'clear-refinements', | |
| connector: true | |
| }); | |
| export type ClearRefinementsConnectorParams = { | |
| /** | |
| * The attributes to include in the refinements to clear (all by default). Cannot be used with `excludedAttributes`. | |
| */ | |
| includedAttributes?: string[]; | |
| /** | |
| * The attributes to exclude from the refinements to clear. Cannot be used with `includedAttributes`. | |
| */ | |
| excludedAttributes?: string[]; | |
| /** | |
| * Function to transform the items passed to the templates. | |
| */ | |
| transformItems?: TransformItems<string>; | |
| }; | |
| export type ClearRefinementsRenderState = { | |
| /** | |
| * Triggers the clear of all the currently refined values. | |
| */ | |
| refine: () => void; | |
| /** | |
| * Indicates if search state is refined. | |
| * @deprecated prefer reading canRefine | |
| */ | |
| hasRefinements: boolean; | |
| /** | |
| * Indicates if search state can be refined. | |
| */ | |
| canRefine: boolean; | |
| /** | |
| * Creates a url for the next state when refinements are cleared. | |
| */ | |
| createURL: CreateURL<void>; | |
| }; | |
| export type ClearRefinementsWidgetDescription = { | |
| $$type: 'ais.clearRefinements'; | |
| renderState: ClearRefinementsRenderState; | |
| indexRenderState: { | |
| clearRefinements: WidgetRenderState< | |
| ClearRefinementsRenderState, | |
| ClearRefinementsConnectorParams | |
| >; | |
| }; | |
| }; | |
| export type ClearRefinementsConnector = Connector< | |
| ClearRefinementsWidgetDescription, | |
| ClearRefinementsConnectorParams | |
| >; | |
| type AttributesToClear = { | |
| helper: AlgoliaSearchHelper; | |
| items: string[]; | |
| }; | |
| const connectClearRefinements: ClearRefinementsConnector = function connectClearRefinements( | |
| renderFn, | |
| unmountFn = noop | |
| ) { | |
| checkRendering(renderFn, withUsage()); | |
| return (widgetParams) => { | |
| const { | |
| includedAttributes = [], | |
| excludedAttributes = ['query'], | |
| transformItems = ((items) => items) as NonNullable< | |
| ClearRefinementsConnectorParams['transformItems'] | |
| > | |
| } = widgetParams || {}; | |
| if (widgetParams && widgetParams.includedAttributes && widgetParams.excludedAttributes) { | |
| throw new Error( | |
| withUsage( | |
| 'The options `includedAttributes` and `excludedAttributes` cannot be used together.' | |
| ) | |
| ); | |
| } | |
| type ConnectorState = { | |
| refine: () => void; | |
| createURL: () => string; | |
| attributesToClear: AttributesToClear[]; | |
| }; | |
| const connectorState: ConnectorState = { | |
| refine: noop, | |
| createURL: () => '', | |
| attributesToClear: [] | |
| }; | |
| const cachedRefine = () => connectorState.refine(); | |
| const cachedCreateURL = () => connectorState.createURL(); | |
| return { | |
| $$type: 'ais.clearRefinements', | |
| init(initOptions) { | |
| const { instantSearchInstance } = initOptions; | |
| renderFn( | |
| { | |
| ...this.getWidgetRenderState(initOptions), | |
| instantSearchInstance | |
| }, | |
| true | |
| ); | |
| }, | |
| render(renderOptions) { | |
| const { instantSearchInstance } = renderOptions; | |
| renderFn( | |
| { | |
| ...this.getWidgetRenderState(renderOptions), | |
| instantSearchInstance | |
| }, | |
| false | |
| ); | |
| }, | |
| dispose() { | |
| unmountFn(); | |
| }, | |
| getRenderState(renderState, renderOptions) { | |
| return { | |
| ...renderState, | |
| clearRefinements: this.getWidgetRenderState(renderOptions) | |
| }; | |
| }, | |
| getWidgetRenderState({ createURL, scopedResults, results }) { | |
| connectorState.attributesToClear = scopedResults.reduce< | |
| Array<ReturnType<typeof getAttributesToClear>> | |
| >((attributesToClear, scopedResult) => { | |
| return attributesToClear.concat( | |
| getAttributesToClear({ | |
| scopedResult, | |
| includedAttributes, | |
| excludedAttributes, | |
| transformItems, | |
| results | |
| }) | |
| ); | |
| }, []); | |
| connectorState.refine = () => { | |
| connectorState.attributesToClear.forEach(({ helper: indexHelper, items }) => { | |
| // Use helper methods directly instead of clearRefinements widget | |
| let newState = indexHelper.state; | |
| items.forEach((attribute) => { | |
| if (attribute === 'query') { | |
| newState = newState.setQuery(''); | |
| } else { | |
| newState = newState.clearRefinements(attribute); | |
| } | |
| }); | |
| indexHelper.setState(newState).search(); | |
| }); | |
| }; | |
| connectorState.createURL = () => { | |
| return createURL( | |
| mergeSearchParameters( | |
| ...connectorState.attributesToClear.map(({ helper: indexHelper, items }) => { | |
| // Use the same approach as refine | |
| let newState = indexHelper.state; | |
| items.forEach((attribute) => { | |
| if (attribute === 'query') { | |
| newState = newState.setQuery(''); | |
| } else { | |
| newState = newState.clearRefinements(attribute); | |
| } | |
| }); | |
| return newState; | |
| }) | |
| ) | |
| ); | |
| }; | |
| const canRefine = connectorState.attributesToClear.some( | |
| (attributeToClear) => attributeToClear.items.length > 0 | |
| ); | |
| return { | |
| canRefine, | |
| hasRefinements: canRefine, | |
| refine: cachedRefine, | |
| createURL: cachedCreateURL, | |
| widgetParams | |
| }; | |
| } | |
| }; | |
| }; | |
| }; | |
| function getAttributesToClear({ | |
| scopedResult, | |
| includedAttributes, | |
| excludedAttributes, | |
| transformItems, | |
| results | |
| }: { | |
| scopedResult: ScopedResult; | |
| includedAttributes: string[]; | |
| excludedAttributes: string[]; | |
| transformItems: TransformItems<string>; | |
| results: SearchResults | undefined | null; | |
| }): AttributesToClear { | |
| const includesQuery = | |
| includedAttributes.indexOf('query') !== -1 || excludedAttributes.indexOf('query') === -1; | |
| return { | |
| helper: scopedResult.helper, | |
| items: transformItems( | |
| uniq( | |
| getRefinements(scopedResult.results, scopedResult.helper.state, includesQuery) | |
| .map((refinement) => refinement.attribute) | |
| .filter( | |
| (attribute) => | |
| // If the array is empty (default case), we keep all the attributes | |
| includedAttributes.length === 0 || | |
| // Otherwise, only add the specified attributes | |
| includedAttributes.indexOf(attribute) !== -1 | |
| ) | |
| .filter( | |
| (attribute) => | |
| // If the query is included, we ignore the default `excludedAttributes = ['query']` | |
| (attribute === 'query' && includesQuery) || | |
| // Otherwise, ignore the excluded attributes | |
| excludedAttributes.indexOf(attribute) === -1 | |
| ) | |
| ), | |
| { results } | |
| ) | |
| }; | |
| } | |
| export default connectClearRefinements; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| This is based on connectRefinementList.ts with changes to handle switching operators | |
| **/ | |
| import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; | |
| import type { | |
| SortBy, | |
| TransformItems, | |
| CreateURL, | |
| WidgetRenderState, | |
| Connector, | |
| Widget, | |
| RenderOptions, | |
| InitOptions, | |
| FacetHit, | |
| IndexUiState | |
| } from 'instantsearch.js'; | |
| import type { SendEventForFacet } from 'instantsearch.js/es/lib/utils'; | |
| import { | |
| createDocumentationMessageGenerator, | |
| checkRendering, | |
| TAG_PLACEHOLDER, | |
| TAG_REPLACEMENT, | |
| escapeFacets, | |
| createSendEventForFacet, | |
| warning | |
| } from 'instantsearch.js/es/lib/utils'; | |
| import { noop } from 'lodash'; | |
| const withUsage = createDocumentationMessageGenerator({ | |
| name: 'refinement-list-with-operator', | |
| connector: true | |
| }); | |
| const DEFAULT_SORT = ['isRefined', 'count:desc', 'name:asc']; | |
| export type RefinementListWithOperatorItem = { | |
| /** | |
| * The value of the refinement list item. | |
| */ | |
| value: string; | |
| /** | |
| * Human-readable value of the refinement list item. | |
| */ | |
| label: string; | |
| /** | |
| * Human-readable value of the searched refinement list item. | |
| */ | |
| highlighted?: string; | |
| /** | |
| * Number of matched results after refinement is applied. | |
| */ | |
| count: number; | |
| /** | |
| * Indicates if the list item is refined. | |
| */ | |
| isRefined: boolean; | |
| }; | |
| export type RefinementListWithOperatorConnectorParams = { | |
| /** | |
| * The name of the attribute in the records. | |
| */ | |
| attribute: string; | |
| /** | |
| * How the filters are combined together. | |
| */ | |
| operator?: 'and' | 'or'; | |
| /** | |
| * The max number of items to display when | |
| * `showMoreLimit` is not set or if the widget is showing less value. | |
| */ | |
| limit?: number; | |
| /** | |
| * Whether to display a button that expands the number of items. | |
| */ | |
| showMore?: boolean; | |
| /** | |
| * The max number of items to display if the widget | |
| * is showing more items. | |
| */ | |
| showMoreLimit?: number; | |
| /** | |
| * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. | |
| */ | |
| sortBy?: SortBy<SearchResults.FacetValue>; | |
| /** | |
| * Escapes the content of the facet values. | |
| */ | |
| escapeFacetValues?: boolean; | |
| /** | |
| * Function to transform the items passed to the templates. | |
| */ | |
| transformItems?: TransformItems<RefinementListWithOperatorItem>; | |
| }; | |
| export type RefinementListWithOperatorRenderState = { | |
| /** | |
| * The list of filtering values returned from Algolia API. | |
| */ | |
| items: RefinementListWithOperatorItem[]; | |
| /** | |
| * indicates whether the results are exhaustive (complete) | |
| */ | |
| hasExhaustiveItems: boolean; | |
| /** | |
| * Creates the next state url for a selected refinement. | |
| */ | |
| createURL: CreateURL<string>; | |
| /** | |
| * Action to apply selected refinements. | |
| */ | |
| refine: (value: string) => void; | |
| /** | |
| * Current operator being used ('and' or 'or'). | |
| */ | |
| operator: 'and' | 'or'; | |
| /** | |
| * Function to change the operator and trigger URL update. | |
| */ | |
| setOperator: (operator: 'and' | 'or') => void; | |
| /** | |
| * Send event to insights middleware | |
| */ | |
| sendEvent: SendEventForFacet; | |
| /** | |
| * Searches for values inside the list. | |
| */ | |
| searchForItems: (query: string) => void; | |
| /** | |
| * `true` if the values are from an index search. | |
| */ | |
| isFromSearch: boolean; | |
| /** | |
| * `true` if a refinement can be applied. | |
| */ | |
| canRefine: boolean; | |
| /** | |
| * `true` if the toggleShowMore button can be activated. | |
| */ | |
| canToggleShowMore: boolean; | |
| /** | |
| * True if the menu is displaying all the menu items. | |
| */ | |
| isShowingMore: boolean; | |
| /** | |
| * Toggles the number of values displayed between `limit` and `showMoreLimit`. | |
| */ | |
| toggleShowMore: () => void; | |
| }; | |
| export type RefinementListWithOperatorWidgetDescription = { | |
| $$type: 'ais.refinementListWithOperator'; | |
| renderState: RefinementListWithOperatorRenderState; | |
| indexRenderState: { | |
| refinementListWithOperator: { | |
| [attribute: string]: WidgetRenderState< | |
| RefinementListWithOperatorRenderState, | |
| RefinementListWithOperatorConnectorParams | |
| >; | |
| }; | |
| }; | |
| indexUiState: { | |
| refinementList: { | |
| [attribute: string]: string[]; | |
| }; | |
| refinementListOperators: { | |
| [attribute: string]: 'and' | 'or'; | |
| }; | |
| }; | |
| }; | |
| export type RefinementListWithOperatorConnector = Connector< | |
| RefinementListWithOperatorWidgetDescription, | |
| RefinementListWithOperatorConnectorParams | |
| >; | |
| const connectRefinementListWithOperator: RefinementListWithOperatorConnector = | |
| function connectRefinementListWithOperator(renderFn, unmountFn = noop) { | |
| checkRendering(renderFn, withUsage()); | |
| return (widgetParams) => { | |
| const { | |
| attribute, | |
| operator: defaultOperator = 'or', | |
| limit = 10, | |
| showMore = false, | |
| showMoreLimit = 20, | |
| sortBy = DEFAULT_SORT, | |
| escapeFacetValues = true, | |
| transformItems = ((items) => items) as NonNullable< | |
| RefinementListWithOperatorConnectorParams['transformItems'] | |
| > | |
| } = widgetParams || {}; | |
| type ThisWidget = Widget< | |
| RefinementListWithOperatorWidgetDescription & { | |
| widgetParams: typeof widgetParams; | |
| } | |
| >; | |
| if (!attribute) { | |
| throw new Error(withUsage('The `attribute` option is required.')); | |
| } | |
| if (!/^(and|or)$/.test(defaultOperator)) { | |
| throw new Error( | |
| withUsage(`The \`operator\` must one of: \`"and"\`, \`"or"\` (got "${defaultOperator}").`) | |
| ); | |
| } | |
| if (showMore === true && showMoreLimit <= limit) { | |
| throw new Error(withUsage('`showMoreLimit` should be greater than `limit`.')); | |
| } | |
| const formatItems = ({ | |
| name: label, | |
| escapedValue: value, | |
| ...item | |
| }: SearchResults.FacetValue): RefinementListWithOperatorItem => ({ | |
| ...item, | |
| value, | |
| label, | |
| highlighted: label | |
| }); | |
| let lastResultsFromMainSearch: SearchResults; | |
| let lastItemsFromMainSearch: RefinementListWithOperatorItem[] = []; | |
| let hasExhaustiveItems = true; | |
| let triggerRefine: RefinementListWithOperatorRenderState['refine'] | undefined; | |
| let sendEvent: RefinementListWithOperatorRenderState['sendEvent'] | undefined; | |
| let isShowingMore = false; | |
| let toggleShowMore = () => {}; | |
| function cachedToggleShowMore() { | |
| toggleShowMore(); | |
| } | |
| function createToggleShowMore(renderOptions: RenderOptions, widget: ThisWidget) { | |
| return () => { | |
| isShowingMore = !isShowingMore; | |
| widget.render!(renderOptions); | |
| }; | |
| } | |
| function getLimit() { | |
| return isShowingMore ? showMoreLimit : limit; | |
| } | |
| // Helper function to determine current operator from search state | |
| function getCurrentOperator(state: any): 'and' | 'or' { | |
| return state.isDisjunctiveFacet(attribute) ? 'or' : 'and'; | |
| } | |
| let searchForFacetValues: ( | |
| renderOptions: RenderOptions | InitOptions | |
| ) => RefinementListWithOperatorRenderState['searchForItems'] = () => () => {}; | |
| const createSearchForFacetValues = function ( | |
| helper: AlgoliaSearchHelper, | |
| widget: ThisWidget | |
| ) { | |
| return (renderOptions: RenderOptions | InitOptions) => (query: string) => { | |
| const { instantSearchInstance, results: searchResults } = renderOptions; | |
| if (query === '' && lastItemsFromMainSearch) { | |
| renderFn( | |
| { | |
| ...widget.getWidgetRenderState({ | |
| ...renderOptions, | |
| results: lastResultsFromMainSearch | |
| }), | |
| instantSearchInstance | |
| }, | |
| false | |
| ); | |
| } else { | |
| const tags = { | |
| highlightPreTag: escapeFacetValues | |
| ? TAG_PLACEHOLDER.highlightPreTag | |
| : TAG_REPLACEMENT.highlightPreTag, | |
| highlightPostTag: escapeFacetValues | |
| ? TAG_PLACEHOLDER.highlightPostTag | |
| : TAG_REPLACEMENT.highlightPostTag | |
| }; | |
| helper | |
| .searchForFacetValues(attribute, query, Math.min(getLimit(), 100), tags) | |
| .then((results) => { | |
| const facetValues = escapeFacetValues | |
| ? escapeFacets(results.facetHits) | |
| : results.facetHits; | |
| const normalizedFacetValues = transformItems( | |
| facetValues.map(({ escapedValue, value, ...item }) => ({ | |
| ...item, | |
| value: escapedValue, | |
| label: value | |
| })), | |
| { results: searchResults } | |
| ); | |
| renderFn( | |
| { | |
| ...widget.getWidgetRenderState({ | |
| ...renderOptions, | |
| results: lastResultsFromMainSearch | |
| }), | |
| items: normalizedFacetValues, | |
| canToggleShowMore: false, | |
| canRefine: true, | |
| isFromSearch: true, | |
| instantSearchInstance | |
| }, | |
| false | |
| ); | |
| }); | |
| } | |
| }; | |
| }; | |
| const createSetOperator = function (helper: AlgoliaSearchHelper, widget: ThisWidget) { | |
| return (newOperator: 'and' | 'or') => { | |
| const currentState = helper.state; | |
| const actualCurrentOperator = getCurrentOperator(currentState); | |
| if (newOperator === actualCurrentOperator) { | |
| return; | |
| } | |
| // Get current refinements before changing configuration | |
| const currentRefinements = | |
| actualCurrentOperator === 'or' | |
| ? currentState.getDisjunctiveRefinements(attribute) | |
| : currentState.getConjunctiveRefinements(attribute); | |
| // Start with a clean state and clear all refinements for the current attribute | |
| let nextState = currentState.clearRefinements(attribute); | |
| // Remove existing facet configurations, ensure we check both conjunctive and disjunctive | |
| if (nextState.facets && nextState.facets.indexOf(attribute) !== -1) { | |
| nextState = nextState.removeFacet(attribute); | |
| } | |
| if (nextState.isDisjunctiveFacet(attribute)) { | |
| nextState = nextState.removeDisjunctiveFacet(attribute); | |
| } | |
| // Configure the facet based on the new operator | |
| if (newOperator === 'or') { | |
| nextState = nextState.addDisjunctiveFacet(attribute); | |
| } else { | |
| nextState = nextState.addFacet(attribute); | |
| } | |
| // Re-add all existing refinements using the appropriate method | |
| currentRefinements.forEach((value) => { | |
| if (newOperator === 'or') { | |
| nextState = nextState.addDisjunctiveFacetRefinement(attribute, value); | |
| } else { | |
| nextState = nextState.addFacetRefinement(attribute, value); | |
| } | |
| }); | |
| helper.setState(nextState).search(); | |
| }; | |
| }; | |
| return { | |
| $$type: 'ais.refinementListWithOperator' as const, | |
| init(initOptions) { | |
| renderFn( | |
| { | |
| ...this.getWidgetRenderState(initOptions), | |
| instantSearchInstance: initOptions.instantSearchInstance | |
| }, | |
| true | |
| ); | |
| }, | |
| render(renderOptions) { | |
| renderFn( | |
| { | |
| ...this.getWidgetRenderState(renderOptions), | |
| instantSearchInstance: renderOptions.instantSearchInstance | |
| }, | |
| false | |
| ); | |
| }, | |
| getRenderState(renderState, renderOptions) { | |
| return { | |
| ...renderState, | |
| refinementListWithOperator: { | |
| ...renderState.refinementListWithOperator, | |
| [attribute]: this.getWidgetRenderState(renderOptions) | |
| } | |
| }; | |
| }, | |
| getWidgetRenderState(renderOptions) { | |
| const { results, state, createURL, instantSearchInstance, helper } = renderOptions; | |
| let items: RefinementListWithOperatorItem[] = []; | |
| let facetValues: SearchResults.FacetValue[] | FacetHit[] = []; | |
| // Get current operator from state | |
| const currentOperator = getCurrentOperator(state); | |
| if (!sendEvent || !triggerRefine || !searchForFacetValues) { | |
| sendEvent = createSendEventForFacet({ | |
| instantSearchInstance, | |
| helper, | |
| attribute, | |
| widgetType: this.$$type | |
| }); | |
| triggerRefine = (facetValue) => { | |
| sendEvent!('click:internal', facetValue); | |
| helper.toggleFacetRefinement(attribute, facetValue).search(); | |
| }; | |
| searchForFacetValues = createSearchForFacetValues(helper, this); | |
| } | |
| if (results) { | |
| const values = results.getFacetValues(attribute, { | |
| sortBy, | |
| facetOrdering: sortBy === DEFAULT_SORT | |
| }); | |
| facetValues = values && Array.isArray(values) ? values : []; | |
| items = transformItems(facetValues.slice(0, getLimit()).map(formatItems), { results }); | |
| const maxValuesPerFacetConfig = state.maxValuesPerFacet; | |
| const currentLimit = getLimit(); | |
| hasExhaustiveItems = | |
| maxValuesPerFacetConfig! > currentLimit | |
| ? facetValues.length <= currentLimit | |
| : facetValues.length < currentLimit; | |
| lastResultsFromMainSearch = results; | |
| lastItemsFromMainSearch = items; | |
| if (renderOptions.results) { | |
| toggleShowMore = createToggleShowMore(renderOptions, this); | |
| } | |
| } | |
| const searchFacetValues = searchForFacetValues && searchForFacetValues(renderOptions); | |
| const canShowLess = isShowingMore && lastItemsFromMainSearch.length > limit; | |
| const canShowMore = showMore && !hasExhaustiveItems; | |
| const canToggleShowMore = canShowLess || canShowMore; | |
| return { | |
| createURL: (facetValue: string) => { | |
| return createURL((uiState) => { | |
| // Ensure we have proper facet configuration before toggling | |
| let nextState = state.resetPage(); | |
| // Make sure the facet is properly configured | |
| const isConfiguredAsDisjunctive = nextState.isDisjunctiveFacet(attribute); | |
| const isConfiguredAsConjunctive = | |
| nextState.facets && nextState.facets.indexOf(attribute) !== -1; | |
| if (!isConfiguredAsDisjunctive && !isConfiguredAsConjunctive) { | |
| // Configure the facet based on current operator | |
| if (currentOperator === 'or') { | |
| nextState = nextState.addDisjunctiveFacet(attribute); | |
| } else { | |
| nextState = nextState.addFacet(attribute); | |
| } | |
| } | |
| // Now safely toggle the refinement | |
| nextState = nextState.toggleFacetRefinement(attribute, facetValue); | |
| return this.getWidgetUiState(uiState, { | |
| searchParameters: nextState, | |
| helper | |
| }); | |
| }); | |
| }, | |
| items, | |
| refine: triggerRefine, | |
| operator: currentOperator, | |
| setOperator: createSetOperator(helper, this), | |
| searchForItems: searchFacetValues, | |
| isFromSearch: false, | |
| canRefine: items.length > 0, | |
| widgetParams, | |
| isShowingMore, | |
| canToggleShowMore, | |
| toggleShowMore: cachedToggleShowMore, | |
| sendEvent, | |
| hasExhaustiveItems | |
| }; | |
| }, | |
| dispose({ state }) { | |
| unmountFn(); | |
| const currentOperator = getCurrentOperator(state); | |
| const withoutMaxValuesPerFacet = state.setQueryParameter('maxValuesPerFacet', undefined); | |
| if (currentOperator === 'and') { | |
| return withoutMaxValuesPerFacet.removeFacet(attribute); | |
| } | |
| return withoutMaxValuesPerFacet.removeDisjunctiveFacet(attribute); | |
| }, | |
| getWidgetUiState(uiState, { searchParameters }) { | |
| const currentOperator = getCurrentOperator(searchParameters); | |
| const values = | |
| currentOperator === 'or' | |
| ? searchParameters.getDisjunctiveRefinements(attribute) | |
| : searchParameters.getConjunctiveRefinements(attribute); | |
| // Store operator state in URL when it differs from default | |
| const shouldStoreOperator = currentOperator !== defaultOperator; | |
| // If there are no refinements, don't store the operator either | |
| const hasRefinements = values && values.length > 0; | |
| return removeEmptyRefinementsFromUiState( | |
| { | |
| ...uiState, | |
| refinementList: { | |
| ...uiState.refinementList, | |
| [attribute]: values | |
| }, | |
| // Only store operator state in URL when there are refinements AND it differs from default | |
| ...(shouldStoreOperator && | |
| hasRefinements && { | |
| refinementListOperators: { | |
| ...uiState.refinementListOperators, | |
| [attribute]: currentOperator | |
| } | |
| }) | |
| }, | |
| attribute | |
| ); | |
| }, | |
| getWidgetSearchParameters(searchParameters, { uiState }) { | |
| // Get operator from UI state first, then fall back to default | |
| const operatorFromUiState = uiState.refinementListOperators?.[attribute]; | |
| const targetOperator = operatorFromUiState || defaultOperator; | |
| if (searchParameters.isHierarchicalFacet(attribute)) { | |
| warning( | |
| false, | |
| `RefinementListWithOperator: Attribute "${attribute}" is already used by another widget applying hierarchical faceting.` | |
| ); | |
| return searchParameters; | |
| } | |
| const values = uiState.refinementList?.[attribute] || []; | |
| // Start fresh - clear all refinements and configurations for this attribute | |
| let withFacetConfiguration = searchParameters.clearRefinements(attribute); | |
| // Remove existing configurations if they exist | |
| if ( | |
| withFacetConfiguration.facets && | |
| withFacetConfiguration.facets.indexOf(attribute) !== -1 | |
| ) { | |
| withFacetConfiguration = withFacetConfiguration.removeFacet(attribute); | |
| } | |
| if (withFacetConfiguration.isDisjunctiveFacet(attribute)) { | |
| withFacetConfiguration = withFacetConfiguration.removeDisjunctiveFacet(attribute); | |
| } | |
| // Configure facet based on target operator (from URL or default) | |
| if (targetOperator === 'or') { | |
| withFacetConfiguration = withFacetConfiguration.addDisjunctiveFacet(attribute); | |
| } else { | |
| withFacetConfiguration = withFacetConfiguration.addFacet(attribute); | |
| } | |
| // Set max values per facet | |
| const currentMaxValuesPerFacet = withFacetConfiguration.maxValuesPerFacet || 0; | |
| const nextMaxValuesPerFacet = Math.max( | |
| currentMaxValuesPerFacet, | |
| showMore ? showMoreLimit : limit | |
| ); | |
| const withMaxValuesPerFacet = withFacetConfiguration.setQueryParameter( | |
| 'maxValuesPerFacet', | |
| nextMaxValuesPerFacet | |
| ); | |
| // Add refinements based on target operator | |
| return values.reduce((parameters, value) => { | |
| if (targetOperator === 'or') { | |
| return parameters.addDisjunctiveFacetRefinement(attribute, value); | |
| } else { | |
| return parameters.addFacetRefinement(attribute, value); | |
| } | |
| }, withMaxValuesPerFacet); | |
| } | |
| }; | |
| }; | |
| }; | |
| function removeEmptyRefinementsFromUiState( | |
| indexUiState: IndexUiState & { | |
| refinementListOperators?: { [attribute: string]: 'and' | 'or' }; | |
| }, | |
| attribute: string | |
| ): IndexUiState & { | |
| refinementListOperators?: { [attribute: string]: 'and' | 'or' }; | |
| } { | |
| if (!indexUiState.refinementList) { | |
| return indexUiState; | |
| } | |
| if ( | |
| !indexUiState.refinementList[attribute] || | |
| indexUiState.refinementList[attribute].length === 0 | |
| ) { | |
| delete indexUiState.refinementList[attribute]; | |
| // Also remove the operator for this attribute when refinements are cleared | |
| if (indexUiState.refinementListOperators && indexUiState.refinementListOperators[attribute]) { | |
| delete indexUiState.refinementListOperators[attribute]; | |
| } | |
| } | |
| if (Object.keys(indexUiState.refinementList).length === 0) { | |
| delete indexUiState.refinementList; | |
| } | |
| // Clean up empty operator state | |
| if (indexUiState.refinementListOperators) { | |
| if (Object.keys(indexUiState.refinementListOperators).length === 0) { | |
| delete indexUiState.refinementListOperators; | |
| } | |
| } | |
| return indexUiState; | |
| } | |
| export default connectRefinementListWithOperator; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const RefinmentExample = () => { | |
| const [operator, setOperator] = useState<'and' | 'or'>('and'); | |
| const refinementListProps = useRefinementListWithOperator({ | |
| limit: 100, | |
| showMoreLimit: 5000, | |
| sortBy: ['count'], | |
| // ...rest of props | |
| }); | |
| return ( | |
| <> | |
| <button | |
| onClick={() => { | |
| setOperator((prev) => (prev === 'and' ? 'or' : 'and')); | |
| refinementListProps.setOperator(operator === 'and' ? 'or' : 'and'); | |
| }} | |
| >operator: {operator}</button> | |
| <CustomRefinmentComponent attribute="brand" {...refinementListProps} /> | |
| </> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { | |
| ClearRefinementsConnectorParams, | |
| ClearRefinementsWidgetDescription | |
| } from 'instantsearch.js/es/connectors/clear-refinements/connectClearRefinements'; | |
| import type { AdditionalWidgetProperties } from 'react-instantsearch'; | |
| import { useConnector } from 'react-instantsearch'; | |
| import connectClearRefinements from './connectClearRefinments'; | |
| export type UseClearRefinementsProps = ClearRefinementsConnectorParams; | |
| export function useClearRefinements( | |
| props?: UseClearRefinementsProps, | |
| additionalWidgetProperties?: AdditionalWidgetProperties | |
| ) { | |
| return useConnector<ClearRefinementsConnectorParams, ClearRefinementsWidgetDescription>( | |
| connectClearRefinements, | |
| props, | |
| additionalWidgetProperties | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { AdditionalWidgetProperties } from 'react-instantsearch'; | |
| import { useConnector } from 'react-instantsearch'; | |
| import type { | |
| RefinementListWithOperatorConnectorParams, | |
| RefinementListWithOperatorWidgetDescription | |
| } from './connectRefinementListWithOperator'; | |
| import connectRefinementListWithOperator from './connectRefinementListWithOperator'; | |
| export type UseRefinementListWithOperatorProps = RefinementListWithOperatorConnectorParams; | |
| export function useRefinementListWithOperator( | |
| props: UseRefinementListWithOperatorProps, | |
| additionalWidgetProperties?: AdditionalWidgetProperties | |
| ) { | |
| return useConnector< | |
| RefinementListWithOperatorConnectorParams, | |
| RefinementListWithOperatorWidgetDescription | |
| >(connectRefinementListWithOperator, props, additionalWidgetProperties); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment