From fe1fbee4ad59c5f24831ed38a419906bbd7d2c15 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Thu, 4 Jun 2020 17:44:30 +0200 Subject: [PATCH] feat(connectToggleRefinement): add support for array values (#4420) Co-authored-by: Haroen Viaene Co-authored-by: Eunjae Lee Co-authored-by: eunjae-lee --- .../__tests__/connectToggleRefinement-test.js | 75 ++++++++++++ .../connectToggleRefinement.js | 107 ++++++++++++------ src/lib/utils/__tests__/toArray-test.ts | 11 ++ src/lib/utils/index.ts | 1 + src/lib/utils/toArray.ts | 5 + .../toggle-refinement/toggle-refinement.js | 4 +- 6 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 src/lib/utils/__tests__/toArray-test.ts create mode 100644 src/lib/utils/toArray.ts diff --git a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js index d74a99b912..56023cbcac 100644 --- a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js +++ b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js @@ -520,6 +520,29 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi ); }); + it('sets user-provided "off" value by default (array)', () => { + const makeWidget = connectToggleRefinement(() => {}); + const widget = makeWidget({ + attribute: 'whatever', + off: ['a', 'b'], + }); + + const helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters({}), { + uiState: {}, + }) + ); + widget.init({ helper, state: helper.state }); + + expect(helper.state.disjunctiveFacetsRefinements).toEqual( + expect.objectContaining({ + whatever: ['a', 'b'], + }) + ); + }); + it('sets user-provided "on" value on refine (falsy)', () => { let caughtRefine; const makeWidget = connectToggleRefinement(({ refine }) => { @@ -571,6 +594,58 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi ); }); + it('sets user-provided "on" value on refine (array)', () => { + let caughtRefine; + const makeWidget = connectToggleRefinement(({ refine }) => { + caughtRefine = refine; + }); + const widget = makeWidget({ + attribute: 'whatever', + on: ['a', 'b'], + }); + + const helper = jsHelper( + { search() {} }, + '', + widget.getWidgetSearchParameters(new SearchParameters({}), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + + widget.init({ helper, state: helper.state }); + + expect(helper.state.disjunctiveFacetsRefinements).toEqual({ + whatever: [], + }); + + widget.render({ + results: new SearchResults(helper.state, [ + { + facets: { + whatever: { + a: 45, + b: 20, + c: 20, + }, + }, + nbHits: 85, + }, + ]), + state: helper.state, + helper, + }); + + // toggle the value + caughtRefine(); + + expect(helper.state.disjunctiveFacetsRefinements).toEqual( + expect.objectContaining({ + whatever: ['a', 'b'], + }) + ); + }); + describe('dispose', () => { test('calls the unmount function', () => { const render = jest.fn(); diff --git a/src/connectors/toggle-refinement/connectToggleRefinement.js b/src/connectors/toggle-refinement/connectToggleRefinement.js index df50f7698f..eae28e985d 100644 --- a/src/connectors/toggle-refinement/connectToggleRefinement.js +++ b/src/connectors/toggle-refinement/connectToggleRefinement.js @@ -5,6 +5,7 @@ import { createDocumentationMessageGenerator, find, noop, + toArray, } from '../../lib/utils'; const withUsage = createDocumentationMessageGenerator({ @@ -99,8 +100,10 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { const hasAnOffValue = userOff !== undefined; const hasAnOnValue = userOn !== undefined; - const on = hasAnOnValue ? escapeRefinement(userOn) : undefined; - const off = hasAnOffValue ? escapeRefinement(userOff) : undefined; + const on = hasAnOnValue ? toArray(userOn).map(escapeRefinement) : undefined; + const off = hasAnOffValue + ? toArray(userOff).map(escapeRefinement) + : undefined; return { $$type: 'ais.toggleRefinement', @@ -109,14 +112,22 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { // Checking if (!isRefined) { if (hasAnOffValue) { - helper.removeDisjunctiveFacetRefinement(attribute, off); + off.forEach(v => + helper.removeDisjunctiveFacetRefinement(attribute, v) + ); } - helper.addDisjunctiveFacetRefinement(attribute, on); + + on.forEach(v => helper.addDisjunctiveFacetRefinement(attribute, v)); } else { // Unchecking - helper.removeDisjunctiveFacetRefinement(attribute, on); + on.forEach(v => + helper.removeDisjunctiveFacetRefinement(attribute, v) + ); + if (hasAnOffValue) { - helper.addDisjunctiveFacetRefinement(attribute, off); + off.forEach(v => + helper.addDisjunctiveFacetRefinement(attribute, v) + ); } } @@ -124,33 +135,43 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { }, init({ state, helper, createURL, instantSearchInstance }) { - this._createURL = isCurrentlyRefined => () => - createURL( - state - .removeDisjunctiveFacetRefinement( - attribute, - isCurrentlyRefined ? on : off - ) - .addDisjunctiveFacetRefinement( - attribute, - isCurrentlyRefined ? off : on - ) - ); + this._createURL = isCurrentlyRefined => () => { + const valuesToRemove = isCurrentlyRefined ? on : off; + if (valuesToRemove) { + valuesToRemove.forEach(v => { + state.removeDisjunctiveFacetRefinement(attribute, v); + }); + } + + const valuesToAdd = isCurrentlyRefined ? off : on; + if (valuesToAdd) { + valuesToAdd.forEach(v => { + state.addDisjunctiveFacetRefinement(attribute, v); + }); + } + + return createURL(state); + }; this.toggleRefinement = opts => { this._toggleRefinement(helper, opts); }; - const isRefined = state.isDisjunctiveFacetRefined(attribute, on); + const isRefined = + on && on.every(v => state.isDisjunctiveFacetRefined(attribute, v)); // no need to refine anything at init if no custom off values if (hasAnOffValue) { // Add filtering on the 'off' value if set if (!isRefined) { const currentPage = helper.state.page; - helper - .addDisjunctiveFacetRefinement(attribute, off) - .setPage(currentPage); + if (off) { + off.forEach(v => + helper.addDisjunctiveFacetRefinement(attribute, v) + ); + } + + helper.setPage(currentPage); } } @@ -185,7 +206,9 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { }, render({ helper, results, state, instantSearchInstance }) { - const isRefined = helper.state.isDisjunctiveFacetRefined(attribute, on); + const isRefined = + on && + on.every(v => helper.state.isDisjunctiveFacetRefined(attribute, v)); const offValue = off === undefined ? false : off; const allFacetValues = results.getFacetValues(attribute) || []; @@ -246,10 +269,11 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { }, getWidgetState(uiState, { searchParameters }) { - const isRefined = searchParameters.isDisjunctiveFacetRefined( - attribute, - on - ); + const isRefined = + on && + on.every(v => + searchParameters.isDisjunctiveFacetRefined(attribute, v) + ); if (!isRefined) { return uiState; @@ -265,25 +289,36 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { }, getWidgetSearchParameters(searchParameters, { uiState }) { - const withFacetConfiguration = searchParameters + let withFacetConfiguration = searchParameters .clearRefinements(attribute) .addDisjunctiveFacet(attribute); const isRefined = Boolean(uiState.toggle && uiState.toggle[attribute]); if (isRefined) { - return withFacetConfiguration.addDisjunctiveFacetRefinement( - attribute, - on - ); + if (on) { + on.forEach(v => { + withFacetConfiguration = withFacetConfiguration.addDisjunctiveFacetRefinement( + attribute, + v + ); + }); + } + + return withFacetConfiguration; } // It's not refined with an `off` value if (hasAnOffValue) { - return withFacetConfiguration.addDisjunctiveFacetRefinement( - attribute, - off - ); + if (off) { + off.forEach(v => { + withFacetConfiguration = withFacetConfiguration.addDisjunctiveFacetRefinement( + attribute, + v + ); + }); + } + return withFacetConfiguration; } // It's not refined without an `off` value diff --git a/src/lib/utils/__tests__/toArray-test.ts b/src/lib/utils/__tests__/toArray-test.ts new file mode 100644 index 0000000000..ab92ae8a51 --- /dev/null +++ b/src/lib/utils/__tests__/toArray-test.ts @@ -0,0 +1,11 @@ +import toArray from '../toArray'; + +describe('toArray', () => { + test('cast to array if necessary', () => { + expect(toArray).toBeInstanceOf(Function); + expect(toArray('a')).toEqual(['a']); + expect(toArray(['a'])).toEqual(['a']); + // Checks that `toArray` acts like a function. + expect(toArray.toString).toBe(Function.prototype.toString); + }); +}); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 63ee9c421b..edbad053e2 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -24,6 +24,7 @@ export { default as find } from './find'; export { default as findIndex } from './findIndex'; export { default as mergeSearchParameters } from './mergeSearchParameters'; export { default as resolveSearchParameters } from './resolveSearchParameters'; +export { default as toArray } from './toArray'; export { warning, deprecate } from './logger'; export { createDocumentationLink, diff --git a/src/lib/utils/toArray.ts b/src/lib/utils/toArray.ts new file mode 100644 index 0000000000..615a666413 --- /dev/null +++ b/src/lib/utils/toArray.ts @@ -0,0 +1,5 @@ +function toArray(value: any) { + return Array.isArray(value) ? value : [value]; +} + +export default toArray; diff --git a/src/widgets/toggle-refinement/toggle-refinement.js b/src/widgets/toggle-refinement/toggle-refinement.js index 8c26e0ef40..20d9041994 100644 --- a/src/widgets/toggle-refinement/toggle-refinement.js +++ b/src/widgets/toggle-refinement/toggle-refinement.js @@ -64,8 +64,8 @@ const renderer = ({ containerNode, cssClasses, renderState, templates }) => ( * @typedef {Object} ToggleWidgetOptions * @property {string|HTMLElement} container Place where to insert the widget in your webpage. * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping"). - * @property {string|number|boolean} on Value to filter on when checked. - * @property {string|number|boolean} off Value to filter on when unchecked. + * @property {string|number|boolean|array} on Value to filter on when checked. + * @property {string|number|boolean|array} off Value to filter on when unchecked. * element (when using the default template). By default when switching to `off`, no refinement will be asked. So you * will get both `true` and `false` results. If you set the off value to `false` then you will get only objects * having `false` has a value for the selected attribute.