diff --git a/dev/app.js b/dev/app.js index cad6cff70d..f3d2e74cd4 100644 --- a/dev/app.js +++ b/dev/app.js @@ -288,6 +288,7 @@ search.addWidget( templates: { header: 'Numeric refinement list (price)', }, + //autoHideContainer: false, }) ); diff --git a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js b/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js index dc53d7da6a..c18ec04f4d 100644 --- a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js +++ b/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js @@ -41,11 +41,6 @@ describe('connectNumericRefinementList', () => { // test if isFirstRendering is true during init expect(rendering.lastCall.args[1]).toBe(true); - const firstRenderingOptions = rendering.lastCall.args[0]; - expect(firstRenderingOptions.shouldAutoHideContainer).toBe(true); - expect(firstRenderingOptions.collapsible).toBe(false); - expect(firstRenderingOptions.containerNode).toBe(container); - widget.render({ results: new SearchResults(helper.state, [{nbHits: 0}]), state: helper.state, @@ -56,11 +51,6 @@ describe('connectNumericRefinementList', () => { // test that rendering has been called during init with isFirstRendering = false expect(rendering.callCount).toBe(2); expect(rendering.lastCall.args[1]).toBe(false); - - const secondRenderingOptions = rendering.lastCall.args[0]; - expect(secondRenderingOptions.shouldAutoHideContainer).toBe(true); - expect(secondRenderingOptions.collapsible).toBe(false); - expect(secondRenderingOptions.containerNode).toBe(container); }); it('Provide a function to update the refinements at each step', () => { @@ -222,4 +212,67 @@ describe('connectNumericRefinementList', () => { toggleRefinement = renderingParameters.toggleRefinement; }); }); + + it('when the state is cleared, the "no value" value should be refined', () => { + const container = document.createElement('div'); + const rendering = sinon.stub(); + const makeWidget = connectNumericRefinementList(rendering); + const listOptions = [ + {name: 'below 10', end: 10}, + {name: '10 - 20', start: 10, end: 20}, + {name: 'more than 20', start: 20}, + {name: '42', start: 42, end: 42}, + {name: 'void'}, + ]; + const widget = makeWidget({ + container, + attributeName: 'numerics', + options: listOptions, + }); + + const helper = jsHelper(fakeClient); + helper.search = sinon.stub(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + onHistoryChange: () => {}, + }); + + const toggleRefinement = rendering.lastCall.args[0].toggleRefinement; + // a user selects a value in the refinement list + toggleRefinement(listOptions[0].name); + + widget.render({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + // No option should be selected + const expectedResults0 = [...listOptions].map(o => ({...o, isRefined: false, attributeName: 'numerics'})); + expectedResults0[0].isRefined = true; + + const renderingParameters0 = rendering.lastCall.args[0]; + expect(renderingParameters0.facetValues).toEqual(expectedResults0); + + // All the refinements are cleared by a third party + helper.removeNumericRefinement('numerics'); + + widget.render({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + // No option should be selected + const expectedResults1 = [...listOptions].map(o => ({...o, isRefined: false, attributeName: 'numerics'})); + expectedResults1[4].isRefined = true; + + const renderingParameters1 = rendering.lastCall.args[0]; + expect(renderingParameters1.facetValues).toEqual(expectedResults1); + }); }); diff --git a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js b/src/connectors/numeric-refinement-list/connectNumericRefinementList.js index 2ef43d3820..f6296d5bb4 100644 --- a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js +++ b/src/connectors/numeric-refinement-list/connectNumericRefinementList.js @@ -1,14 +1,5 @@ -import { - bemHelper, - prepareTemplateProps, - getContainerNode, -} from '../../lib/utils.js'; -import cx from 'classnames'; import find from 'lodash/find'; import includes from 'lodash/includes'; -import defaultTemplates from './defaultTemplates.js'; - -const bem = bemHelper('ais-refinement-list'); /** * Instantiate a list of refinements based on a facet @@ -40,53 +31,20 @@ const bem = bemHelper('ais-refinement-list'); * @return {Object} */ const usage = `Usage: -numericRefinementList({ - container, +connectNumericRefinementList(renderer)({ attributeName, - options, - [ cssClasses.{root,header,body,footer,list,item,active,label,radio,count} ], - [ templates.{header,item,footer} ], - [ transformData.{item} ], - [ autoHideContainer ], - [ collapsible=false ] + options })`; const connectNumericRefinementList = numericRefinementListRendering => ({ - container, attributeName, options, - cssClasses: userCssClasses = {}, - templates = defaultTemplates, - collapsible = false, - transformData, - autoHideContainer = true, }) => { - if (!container || !attributeName || !options) { + if (!attributeName || !options) { throw new Error(usage); } - const containerNode = getContainerNode(container); - - const cssClasses = { - root: cx(bem(null), userCssClasses.root), - header: cx(bem('header'), userCssClasses.header), - body: cx(bem('body'), userCssClasses.body), - footer: cx(bem('footer'), userCssClasses.footer), - list: cx(bem('list'), userCssClasses.list), - item: cx(bem('item'), userCssClasses.item), - label: cx(bem('label'), userCssClasses.label), - radio: cx(bem('radio'), userCssClasses.radio), - active: cx(bem('item', 'active'), userCssClasses.active), - }; - return { - init({templatesConfig, helper, createURL}) { - this._templateProps = prepareTemplateProps({ - transformData, - defaultTemplates, - templatesConfig, - templates, - }); - + init({helper, createURL, instantSearchInstance}) { this._toggleRefinement = facetValue => { const refinedState = refine(helper.state, attributeName, options, facetValue); helper.setState(refinedState).search(); @@ -103,17 +61,14 @@ const connectNumericRefinementList = numericRefinementListRendering => ({ ); numericRefinementListRendering({ - collapsible, createURL: this._createURL(helper.state), - cssClasses, facetValues, - shouldAutoHideContainer: autoHideContainer, - templateProps: this._templateProps, + noResults: true, toggleRefinement: this._toggleRefinement, - containerNode, + instantSearchInstance, }, true); }, - render({results, state}) { + render({results, state, instantSearchInstance}) { const facetValues = options.map(facetValue => ({ ...facetValue, @@ -122,15 +77,14 @@ const connectNumericRefinementList = numericRefinementListRendering => ({ }) ); + const noResults = results.nbHits === 0; + numericRefinementListRendering({ - collapsible, createURL: this._createURL(state), - cssClasses, facetValues, - shouldAutoHideContainer: autoHideContainer && results.nbHits === 0, - templateProps: this._templateProps, + noResults, toggleRefinement: this._toggleRefinement, - containerNode, + instantSearchInstance, }, false); }, }; diff --git a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js b/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js index 106363bbde..dc02250a2a 100644 --- a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js +++ b/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js @@ -77,7 +77,7 @@ describe('numericRefinementList()', () => { helper.state.clearRefinements = sinon.stub().returns(helper.state); helper.state.addNumericRefinement = sinon.stub().returns(helper.state); createURL = () => '#'; - widget.init({helper}); + widget.init({helper, instantSearchInstance: {}}); }); it('calls twice ReactDOM.render(, container)', () => { @@ -186,7 +186,7 @@ describe('numericRefinementList()', () => { }); // The lifeccycle impose all the steps - testWidget.init({helper, createURL: () => ''}); + testWidget.init({helper, createURL: () => '', instantSearchInstance: {}}); // When testWidget.render({state, results, createURL}); diff --git a/src/connectors/numeric-refinement-list/defaultTemplates.js b/src/widgets/numeric-refinement-list/defaultTemplates.js similarity index 100% rename from src/connectors/numeric-refinement-list/defaultTemplates.js rename to src/widgets/numeric-refinement-list/defaultTemplates.js diff --git a/src/widgets/numeric-refinement-list/numeric-refinement-list.js b/src/widgets/numeric-refinement-list/numeric-refinement-list.js index 362be8e15f..46db848ef4 100644 --- a/src/widgets/numeric-refinement-list/numeric-refinement-list.js +++ b/src/widgets/numeric-refinement-list/numeric-refinement-list.js @@ -4,6 +4,56 @@ import RefinementList from '../../components/RefinementList/RefinementList.js'; import connectNumericRefinementList from '../../connectors/numeric-refinement-list/connectNumericRefinementList.js'; +import defaultTemplates from './defaultTemplates.js'; + +import { + bemHelper, + prepareTemplateProps, + getContainerNode, +} from '../../lib/utils.js'; +import cx from 'classnames'; + +const bem = bemHelper('ais-refinement-list'); + +const renderer = ({ + containerNode, + collapsible, + autoHideContainer, + cssClasses, + renderState, + transformData, + templates, +}) => ({ + createURL, + instantSearchInstance, + toggleRefinement, + facetValues, + noResults, +}, isFirstRendering) => { + if (isFirstRendering) { + renderState.templateProps = prepareTemplateProps({ + transformData, + defaultTemplates, + templatesConfig: instantSearchInstance.templatesConfig, + templates, + }); + return; + } + + ReactDOM.render( + , + containerNode + ); +}; + /** * Instantiate a list of refinements based on a facet * @function numericRefinementList @@ -33,29 +83,62 @@ import connectNumericRefinementList from '../../connectors/numeric-refinement-li * @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget * @return {Object} */ -export default connectNumericRefinementList(defaultRendering); +const usage = `Usage: +numericRefinementList({ + container, + attributeName, + options, + [ cssClasses.{root,header,body,footer,list,item,active,label,radio,count} ], + [ templates.{header,item,footer} ], + [ transformData.{item} ], + [ autoHideContainer ], + [ collapsible=false ] +})`; -function defaultRendering({ - collapsible, - createURL, - cssClasses, - facetValues, - shouldAutoHideContainer, - templateProps, - toggleRefinement, - containerNode, -}, isFirstRendering) { - if (isFirstRendering) return; - ReactDOM.render( - , - containerNode - ); +export default function numericRefinementList({ + container, + attributeName, + options, + cssClasses: userCssClasses = {}, + templates = defaultTemplates, + collapsible = false, + transformData, + autoHideContainer = true, +}) { + if (!container || !attributeName || !options) { + throw new Error(usage); + } + + const containerNode = getContainerNode(container); + + const cssClasses = { + root: cx(bem(null), userCssClasses.root), + header: cx(bem('header'), userCssClasses.header), + body: cx(bem('body'), userCssClasses.body), + footer: cx(bem('footer'), userCssClasses.footer), + list: cx(bem('list'), userCssClasses.list), + item: cx(bem('item'), userCssClasses.item), + label: cx(bem('label'), userCssClasses.label), + radio: cx(bem('radio'), userCssClasses.radio), + active: cx(bem('item', 'active'), userCssClasses.active), + }; + + const specializedRenderer = renderer({ + containerNode, + collapsible, + autoHideContainer, + cssClasses, + renderState: {}, + transformData, + templates, + }); + try { + const makeNumericRefinementList = connectNumericRefinementList(specializedRenderer); + return makeNumericRefinementList({ + attributeName, + options, + }); + } catch (e) { + throw new Error(usage); + } }