From 6073d94e333ef813289added9fb942a4f61536c8 Mon Sep 17 00:00:00 2001 From: iam4x Date: Tue, 21 Mar 2017 16:18:29 +0100 Subject: [PATCH] feat(connectors): connectRangeSlider (iteration2) --- .../__tests__/connectRangeSlider-test.js | 19 +- .../range-slider/connectRangeSlider.js | 291 ++++++++---------- .../__tests__/range-slider-test.js | 14 +- src/widgets/range-slider/range-slider.js | 159 ++++++++-- 4 files changed, 265 insertions(+), 218 deletions(-) diff --git a/src/connectors/range-slider/__tests__/connectRangeSlider-test.js b/src/connectors/range-slider/__tests__/connectRangeSlider-test.js index 7a4c5e13b3..c4840b97e0 100644 --- a/src/connectors/range-slider/__tests__/connectRangeSlider-test.js +++ b/src/connectors/range-slider/__tests__/connectRangeSlider-test.js @@ -1,5 +1,3 @@ - - import sinon from 'sinon'; import jsHelper from 'algoliasearch-helper'; @@ -36,6 +34,7 @@ describe('connectRangeSlider', () => { state: helper.state, createURL: () => '#', onHistoryChange: () => {}, + instantSearchInstance: {templatesConfig: undefined}, }); { // should call the rendering once with isFirstRendering to true @@ -44,20 +43,16 @@ describe('connectRangeSlider', () => { expect(isFirstRendering).toBe(true); // should provide good values for the first rendering - const {range, collapsible, start, - shouldAutoHideContainer, containerNode} = rendering.lastCall.args[0]; + const {range, start} = rendering.lastCall.args[0]; expect(range).toEqual({min: 0, max: 0}); expect(start).toEqual([-Infinity, Infinity]); - expect(collapsible).toBe(false); - expect(shouldAutoHideContainer).toBe(true); - expect(containerNode).toBe(container); } widget.render({ results: new SearchResults(helper.state, [{ hits: [{test: 'oneTime'}], facets: {price: {10: 1, 20: 1, 30: 1}}, - facets_stats: { // eslint-disable-line + facets_stats: { // eslint-disable-line price: { avg: 20, max: 30, @@ -80,13 +75,9 @@ describe('connectRangeSlider', () => { expect(isFirstRendering).toBe(false); // should provide good values for the first rendering - const {range, collapsible, start, - shouldAutoHideContainer, containerNode} = rendering.lastCall.args[0]; + const {range, start} = rendering.lastCall.args[0]; expect(range).toEqual({min: 10, max: 30}); expect(start).toEqual([-Infinity, Infinity]); - expect(collapsible).toBe(false); - expect(shouldAutoHideContainer).toBe(false); - expect(containerNode).toBe(container); } }); @@ -158,7 +149,7 @@ describe('connectRangeSlider', () => { results: new SearchResults(helper.state, [{ hits: [{test: 'oneTime'}], facets: {price: {10: 1, 20: 1, 30: 1}}, - facets_stats: { // eslint-disable-line + facets_stats: { // eslint-disable-line price: { avg: 20, max: 30, diff --git a/src/connectors/range-slider/connectRangeSlider.js b/src/connectors/range-slider/connectRangeSlider.js index 114c919c85..b8b9070da8 100644 --- a/src/connectors/range-slider/connectRangeSlider.js +++ b/src/connectors/range-slider/connectRangeSlider.js @@ -1,16 +1,24 @@ -import { - bemHelper, - prepareTemplateProps, - getContainerNode, -} from '../../lib/utils.js'; -import find from 'lodash/find'; -import cx from 'classnames'; - -const bem = bemHelper('ais-range-slider'); -const defaultTemplates = { - header: '', - footer: '', -}; +import {checkRendering} from '../../lib/utils.js'; + +const usage = `Usage: +var customRangeSlider = connectRangeSlider(function render(params, isFirstRendering) { + // params = { + // refine, + // range, + // start, + // instantSearchInstance, + // } +}); +search.addWidget( + customRangeSlider({ + attributeName, + min, + max, + precision + }); +); +Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectRangeSlider.html +`; /** * Instantiate a slider based on a numeric attribute. @@ -40,165 +48,118 @@ const defaultTemplates = { * @param {number} [options.max] Maximal slider value, defaults to automatically computed from the result set * @return {Object} */ -const usage = `Usage: -rangeSlider({ - container, - attributeName, - [ tooltips=true ], - [ templates.{header, footer} ], - [ cssClasses.{root, header, body, footer} ], - [ step=1 ], - [ pips=true ], - [ autoHideContainer=true ], - [ collapsible=false ], - [ min ], - [ max ] -}); -`; -const connectRangeSlider = rangeSliderRendering => ({ - container, + +export default function connectRangeSlider(renderFn) { + checkRendering(renderFn, usage); + + return ({ attributeName, - tooltips = true, - templates = defaultTemplates, - collapsible = false, - cssClasses: userCssClasses = {}, - step = 1, - pips = true, - autoHideContainer = true, min: userMin, max: userMax, precision = 2, - } = {}) => { - if (!container || !attributeName) { - throw new Error(usage); - } + }) => { + if (!attributeName) { + throw new Error(usage); + } + + const formatToNumber = v => Number(Number(v).toFixed(precision)); + + const sliderFormatter = { + from: v => v, + to: v => formatToNumber(v).toLocaleString(), + }; + + return { + getConfiguration: originalConf => { + const conf = { + disjunctiveFacets: [attributeName], + }; + + const hasUserBounds = userMin !== undefined || userMax !== undefined; + const boundsNotAlreadyDefined = !originalConf || + originalConf.numericRefinements && + originalConf.numericRefinements[attributeName] === undefined; + + if (hasUserBounds && boundsNotAlreadyDefined) { + conf.numericRefinements = {[attributeName]: {}}; + if (userMin !== undefined) conf.numericRefinements[attributeName]['>='] = [userMin]; + if (userMax !== undefined) conf.numericRefinements[attributeName]['<='] = [userMax]; + } - const formatToNumber = v => Number(Number(v).toFixed(precision)); + return conf; + }, - const sliderFormatter = { - from: v => v, - to: v => formatToNumber(v).toLocaleString(), - }; + _getCurrentRefinement(helper) { + let min = helper.state.getNumericRefinement(attributeName, '>='); + let max = helper.state.getNumericRefinement(attributeName, '<='); - 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), - }; - - return { - getConfiguration: originalConf => { - const conf = { - disjunctiveFacets: [attributeName], - }; - - const hasUserBounds = userMin !== undefined || userMax !== undefined; - const boundsNotAlreadyDefined = !originalConf || - originalConf.numericRefinements && - originalConf.numericRefinements[attributeName] === undefined; - - if (hasUserBounds && boundsNotAlreadyDefined) { - conf.numericRefinements = {[attributeName]: {}}; - if (userMin !== undefined) conf.numericRefinements[attributeName]['>='] = [userMin]; - if (userMax !== undefined) conf.numericRefinements[attributeName]['<='] = [userMax]; - } - - return conf; - }, - _getCurrentRefinement(helper) { - let min = helper.state.getNumericRefinement(attributeName, '>='); - let max = helper.state.getNumericRefinement(attributeName, '<='); - - if (min && min.length) { - min = min[0]; - } else { - min = -Infinity; - } - - if (max && max.length) { - max = max[0]; - } else { - max = Infinity; - } - - return { - min, - max, - }; - }, - init({helper, templatesConfig}) { - this._templateProps = prepareTemplateProps({ - defaultTemplates, - templatesConfig, - templates, - }); - this._refine = bounds => newValues => { - helper.clearRefinements(attributeName); - if (!bounds.min || newValues[0] > bounds.min) { - helper.addNumericRefinement(attributeName, '>=', formatToNumber(newValues[0])); + if (min && min.length) { + min = min[0]; + } else { + min = -Infinity; } - if (!bounds.max || newValues[1] < bounds.max) { - helper.addNumericRefinement(attributeName, '<=', formatToNumber(newValues[1])); + + if (max && max.length) { + max = max[0]; + } else { + max = Infinity; } - helper.search(); - }; - - const stats = { - min: userMin || null, - max: userMax || null, - }; - const currentRefinement = this._getCurrentRefinement(helper); - - rangeSliderRendering({ - collapsible, - cssClasses, - refine: this._refine(stats), - pips, - range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)}, - shouldAutoHideContainer: autoHideContainer && stats.min === stats.max, - start: [currentRefinement.min, currentRefinement.max], - step, - templateProps: this._templateProps, - tooltips, - format: sliderFormatter, - containerNode, - }, true); - }, - render({results, helper}) { - const facet = find(results.disjunctiveFacets, {name: attributeName}); - const stats = facet !== undefined && facet.stats !== undefined ? facet.stats : { - min: null, - max: null, - }; - - if (userMin !== undefined) stats.min = userMin; - if (userMax !== undefined) stats.max = userMax; - - const currentRefinement = this._getCurrentRefinement(helper); - - if (tooltips.format !== undefined) { - tooltips = [{to: tooltips.format}, {to: tooltips.format}]; - } - - rangeSliderRendering({ - collapsible, - cssClasses, - refine: this._refine(stats), - pips, - range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)}, - shouldAutoHideContainer: autoHideContainer && stats.min === stats.max, - start: [currentRefinement.min, currentRefinement.max], - step, - templateProps: this._templateProps, - tooltips, - format: sliderFormatter, - containerNode, - }, false); - }, - }; -}; -export default connectRangeSlider; + return { + min, + max, + }; + }, + + init({helper, instantSearchInstance}) { + this._instantSearchInstance = instantSearchInstance; + + this._refine = bounds => newValues => { + helper.clearRefinements(attributeName); + if (!bounds.min || newValues[0] > bounds.min) { + helper.addNumericRefinement(attributeName, '>=', formatToNumber(newValues[0])); + } + if (!bounds.max || newValues[1] < bounds.max) { + helper.addNumericRefinement(attributeName, '<=', formatToNumber(newValues[1])); + } + helper.search(); + }; + + const stats = { + min: userMin || null, + max: userMax || null, + }; + const currentRefinement = this._getCurrentRefinement(helper); + + renderFn({ + refine: this._refine(stats), + range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)}, + start: [currentRefinement.min, currentRefinement.max], + format: sliderFormatter, + instantSearchInstance: this._instantSearchInstance, + }, true); + }, + + render({results, helper}) { + const facet = (results.disjunctiveFacets || []).find(({name}) => name === attributeName); + const stats = facet !== undefined && facet.stats !== undefined ? facet.stats : { + min: null, + max: null, + }; + + if (userMin !== undefined) stats.min = userMin; + if (userMax !== undefined) stats.max = userMax; + + const currentRefinement = this._getCurrentRefinement(helper); + + renderFn({ + refine: this._refine(stats), + range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)}, + start: [currentRefinement.min, currentRefinement.max], + format: sliderFormatter, + instantSearchInstance: this._instantSearchInstance, + }, false); + }, + }; + }; +} diff --git a/src/widgets/range-slider/__tests__/range-slider-test.js b/src/widgets/range-slider/__tests__/range-slider-test.js index 5ef66bc27b..82a5768f00 100644 --- a/src/widgets/range-slider/__tests__/range-slider-test.js +++ b/src/widgets/range-slider/__tests__/range-slider-test.js @@ -7,6 +7,8 @@ import Slider from '../../../components/Slider/Slider.js'; import AlgoliasearchHelper from 'algoliasearch-helper'; expect.extend(expectJSX); +const instantSearchInstance = {templatesConfig: undefined}; + describe('rangeSlider call', () => { it('throws an exception when no container', () => { const attributeName = ''; @@ -95,7 +97,7 @@ describe('rangeSlider()', () => { results = {}; widget = rangeSlider({container, attributeName: 'aNumAttr', min: 100, max: 200}); helper.setState(widget.getConfiguration()); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); widget.render({results, helper}); const props = defaultProps; @@ -115,7 +117,7 @@ describe('rangeSlider()', () => { }; widget = rangeSlider({container, attributeName: 'aNumAttr', min: 100}); helper.setState(widget.getConfiguration()); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); widget.render({results, helper}); const props = { ...defaultProps, @@ -182,7 +184,7 @@ describe('rangeSlider()', () => { }; widget = rangeSlider({container, attributeName: 'aNumAttr', max: 100}); helper.setState(widget.getConfiguration()); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); widget.render({results, helper}); const props = { ...defaultProps, @@ -200,7 +202,7 @@ describe('rangeSlider()', () => { beforeEach(() => { results = {}; widget = rangeSlider({container, attributeName: 'aNumAttr', cssClasses: {root: ['root', 'cx']}}); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); }); it('calls ReactDOM.render(, container)', () => { @@ -238,7 +240,7 @@ describe('rangeSlider()', () => { describe('when rangestats min === stats max', () => { beforeEach(() => { widget = rangeSlider({container, attributeName: 'aNumAttr', cssClasses: {root: ['root', 'cx']}}); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); results = { disjunctiveFacets: [{ name: 'aNumAttr', @@ -284,7 +286,7 @@ describe('rangeSlider()', () => { describe('with results', () => { beforeEach(() => { widget = rangeSlider({container, attributeName: 'aNumAttr', cssClasses: {root: ['root', 'cx']}}); - widget.init({helper}); + widget.init({helper, instantSearchInstance}); results = { disjunctiveFacets: [{ name: 'aNumAttr', diff --git a/src/widgets/range-slider/range-slider.js b/src/widgets/range-slider/range-slider.js index 022b141dfc..bb844fc033 100644 --- a/src/widgets/range-slider/range-slider.js +++ b/src/widgets/range-slider/range-slider.js @@ -1,8 +1,89 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import cx from 'classnames'; + import Slider from '../../components/Slider/Slider.js'; import connectRangeSlider from '../../connectors/range-slider/connectRangeSlider.js'; +import { + bemHelper, + prepareTemplateProps, + getContainerNode, +} from '../../lib/utils.js'; + +const defaultTemplates = { + header: '', + footer: '', +}; + +const bem = bemHelper('ais-range-slider'); + +const renderer = ({ + containerNode, + cssClasses, + tooltips, + renderState, + autoHideContainer, + pips, + step, + collapsible, + templates, +}) => ({ + refine, + range, + start, + instantSearchInstance, + format, +}, isFirstRendering) => { + if (isFirstRendering) { + renderState.templateProps = prepareTemplateProps({ + defaultTemplates, + templatesConfig: instantSearchInstance.templatesConfig, + templates, + }); + return; + } + + const shouldAutoHideContainer = autoHideContainer && range.min === range.max; + + if (tooltips.format !== undefined) { + tooltips = [{to: tooltips.format}, {to: tooltips.format}]; + } + + ReactDOM.render( + , + containerNode + ); +}; + +const usage = `Usage: +rangeSlider({ + container, + attributeName, + [ tooltips=true ], + [ templates.{header, footer} ], + [ cssClasses.{root, header, body, footer} ], + [ step=1 ], + [ pips=true ], + [ autoHideContainer=true ], + [ collapsible=false ], + [ min ], + [ max ] +}); +`; + /** * Instantiate a slider based on a numeric attribute. * This is a wrapper around [noUiSlider](http://refreshless.com/nouislider/) @@ -29,39 +110,51 @@ import connectRangeSlider from '../../connectors/range-slider/connectRangeSlider * @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget * @param {number} [options.min] Minimal slider value, default to automatically computed from the result set * @param {number} [options.max] Maximal slider value, defaults to automatically computed from the result set - * @return {Object} + * @return {Object} widget */ +export default function rangeSlider({ + container, + attributeName, + tooltips = true, + templates = defaultTemplates, + collapsible = false, + cssClasses: userCssClasses = {}, + step = 1, + pips = true, + autoHideContainer = true, + min, + max, + precision = 2, +} = {}) { + if (!container) { + throw new Error(usage); + } -export default connectRangeSlider(defaultRendering); -function defaultRendering({ - collapsible, - cssClasses, - refine, - pips, - range, - shouldAutoHideContainer, - start, - step, - templateProps, - tooltips, - format, - containerNode, -}, isFirstRendering) { - if (isFirstRendering) return; - ReactDOM.render( - , - containerNode - ); + 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), + }; + + const specializedRenderer = renderer({ + containerNode, + cssClasses, + tooltips, + templates, + renderState: {}, + collapsible, + step, + pips, + autoHideContainer, + }); + + try { + const makeWidget = connectRangeSlider(specializedRenderer); + return makeWidget({attributeName, min, max, precision}); + } catch (e) { + throw new Error(usage); + } }