Skip to content

Commit 1a02798

Browse files
author
Alexandre Stanislawski
committed
feat(connector): add range-slider
1 parent d8bed96 commit 1a02798

File tree

5 files changed

+259
-196
lines changed

5 files changed

+259
-196
lines changed

src/components/Slider/Slider.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const cssPrefix = 'ais-range-slider--';
77

88
import isEqual from 'lodash/isEqual';
99

10-
class Slider extends React.Component {
10+
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
11+
import headerFooterHOC from '../../decorators/headerFooter.js';
12+
13+
export class RawSlider extends React.Component {
1114
componentWillMount() {
1215
this.handleChange = this.handleChange.bind(this);
1316
}
@@ -60,7 +63,7 @@ class Slider extends React.Component {
6063
}
6164
}
6265

63-
Slider.propTypes = {
66+
RawSlider.propTypes = {
6467
onChange: React.PropTypes.func,
6568
onSlide: React.PropTypes.func,
6669
pips: React.PropTypes.oneOfType([
@@ -79,4 +82,4 @@ Slider.propTypes = {
7982
]),
8083
};
8184

82-
export default Slider;
85+
export default autoHideContainerHOC(headerFooterHOC(RawSlider));

src/components/Slider/__tests__/Slider-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import expect from 'expect';
66
import TestUtils from 'react-addons-test-utils';
77

88
import expectJSX from 'expect-jsx';
9-
import Slider from '../Slider';
9+
import {RawSlider as Slider} from '../Slider';
1010
import Nouislider from 'react-nouislider';
1111
expect.extend(expectJSX);
1212

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import {
2+
bemHelper,
3+
prepareTemplateProps,
4+
getContainerNode,
5+
} from '../../lib/utils.js';
6+
import find from 'lodash/find';
7+
import cx from 'classnames';
8+
9+
const bem = bemHelper('ais-range-slider');
10+
const defaultTemplates = {
11+
header: '',
12+
footer: '',
13+
};
14+
15+
/**
16+
* Instantiate a slider based on a numeric attribute.
17+
* This is a wrapper around [noUiSlider](http://refreshless.com/nouislider/)
18+
* @function rangeSlider
19+
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
20+
* @param {string} options.attributeName Name of the attribute for faceting
21+
* @param {boolean|Object} [options.tooltips=true] Should we show tooltips or not.
22+
* The default tooltip will show the raw value.
23+
* You can also provide
24+
* `tooltips: {format: function(rawValue) {return '$' + Math.round(rawValue).toLocaleString()}}`
25+
* So that you can format the tooltip display value as you want
26+
* @param {Object} [options.templates] Templates to use for the widget
27+
* @param {string|Function} [options.templates.header=''] Header template
28+
* @param {string|Function} [options.templates.footer=''] Footer template
29+
* @param {boolean} [options.autoHideContainer=true] Hide the container when no refinements available
30+
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements
31+
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
32+
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
33+
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
34+
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
35+
* @param {boolean|object} [options.pips=true] Show slider pips.
36+
* @param {boolean|object} [options.step=1] Every handle move will jump that number of steps.
37+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
38+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
39+
* @param {number} [options.min] Minimal slider value, default to automatically computed from the result set
40+
* @param {number} [options.max] Maximal slider value, defaults to automatically computed from the result set
41+
* @return {Object}
42+
*/
43+
const usage = `Usage:
44+
rangeSlider({
45+
container,
46+
attributeName,
47+
[ tooltips=true ],
48+
[ templates.{header, footer} ],
49+
[ cssClasses.{root, header, body, footer} ],
50+
[ step=1 ],
51+
[ pips=true ],
52+
[ autoHideContainer=true ],
53+
[ collapsible=false ],
54+
[ min ],
55+
[ max ]
56+
});
57+
`;
58+
const connectRangeSlider = rangeSliderRendering => ({
59+
container,
60+
attributeName,
61+
tooltips = true,
62+
templates = defaultTemplates,
63+
collapsible = false,
64+
cssClasses: userCssClasses = {},
65+
step = 1,
66+
pips = true,
67+
autoHideContainer = true,
68+
min: userMin,
69+
max: userMax,
70+
precision = 2,
71+
} = {}) => {
72+
if (!container || !attributeName) {
73+
throw new Error(usage);
74+
}
75+
76+
const formatToNumber = v => Number(Number(v).toFixed(precision));
77+
78+
const sliderFormatter = {
79+
from: v => v,
80+
to: v => formatToNumber(v).toLocaleString(),
81+
};
82+
83+
const containerNode = getContainerNode(container);
84+
85+
const cssClasses = {
86+
root: cx(bem(null), userCssClasses.root),
87+
header: cx(bem('header'), userCssClasses.header),
88+
body: cx(bem('body'), userCssClasses.body),
89+
footer: cx(bem('footer'), userCssClasses.footer),
90+
};
91+
92+
return {
93+
getConfiguration: originalConf => {
94+
const conf = {
95+
disjunctiveFacets: [attributeName],
96+
};
97+
98+
if (
99+
(userMin !== undefined || userMax !== undefined)
100+
&&
101+
(!originalConf ||
102+
originalConf.numericRefinements &&
103+
originalConf.numericRefinements[attributeName] === undefined)
104+
) {
105+
conf.numericRefinements = {[attributeName]: {}};
106+
107+
if (userMin !== undefined) {
108+
conf.numericRefinements[attributeName]['>='] = [userMin];
109+
}
110+
111+
if (userMax !== undefined) {
112+
conf.numericRefinements[attributeName]['<='] = [userMax];
113+
}
114+
}
115+
116+
return conf;
117+
},
118+
_getCurrentRefinement(helper) {
119+
let min = helper.state.getNumericRefinement(attributeName, '>=');
120+
let max = helper.state.getNumericRefinement(attributeName, '<=');
121+
122+
if (min && min.length) {
123+
min = min[0];
124+
} else {
125+
min = -Infinity;
126+
}
127+
128+
if (max && max.length) {
129+
max = max[0];
130+
} else {
131+
max = Infinity;
132+
}
133+
134+
return {
135+
min,
136+
max,
137+
};
138+
},
139+
init({helper, templatesConfig}) {
140+
this._templateProps = prepareTemplateProps({
141+
defaultTemplates,
142+
templatesConfig,
143+
templates,
144+
});
145+
this._refine = oldValues => newValues => {
146+
helper.clearRefinements(attributeName);
147+
if (newValues[0] > oldValues.min) {
148+
helper.addNumericRefinement(attributeName, '>=', formatToNumber(newValues[0]));
149+
}
150+
if (newValues[1] < oldValues.max) {
151+
helper.addNumericRefinement(attributeName, '<=', formatToNumber(newValues[1]));
152+
}
153+
helper.search();
154+
};
155+
156+
const stats = {
157+
min: userMin || null,
158+
max: userMax || null,
159+
};
160+
const currentRefinement = this._getCurrentRefinement(helper);
161+
162+
rangeSliderRendering({
163+
collapsible,
164+
cssClasses,
165+
onChange: this._refine(stats),
166+
pips,
167+
range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)},
168+
shouldAutoHideContainer: autoHideContainer && stats.min === stats.max,
169+
start: [currentRefinement.min, currentRefinement.max],
170+
step,
171+
templateProps: this._templateProps,
172+
tooltips,
173+
format: sliderFormatter,
174+
containerNode,
175+
}, true);
176+
},
177+
render({results, helper}) {
178+
const facet = find(results.disjunctiveFacets, {name: attributeName});
179+
const stats = facet !== undefined && facet.stats !== undefined ? facet.stats : {
180+
min: null,
181+
max: null,
182+
};
183+
184+
if (userMin !== undefined) stats.min = userMin;
185+
if (userMax !== undefined) stats.max = userMax;
186+
187+
const currentRefinement = this._getCurrentRefinement(helper);
188+
189+
if (tooltips.format !== undefined) {
190+
tooltips = [{to: tooltips.format}, {to: tooltips.format}];
191+
}
192+
193+
rangeSliderRendering({
194+
collapsible,
195+
cssClasses,
196+
onChange: this._refine(stats),
197+
pips,
198+
range: {min: Math.floor(stats.min), max: Math.ceil(stats.max)},
199+
shouldAutoHideContainer: autoHideContainer && stats.min === stats.max,
200+
start: [currentRefinement.min, currentRefinement.max],
201+
step,
202+
templateProps: this._templateProps,
203+
tooltips,
204+
format: sliderFormatter,
205+
containerNode,
206+
}, false);
207+
},
208+
};
209+
};
210+
211+
export default connectRangeSlider;

src/widgets/range-slider/__tests__/range-slider-test.js

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,9 @@ describe('rangeSlider()', () => {
3030
let results;
3131
let helper;
3232

33-
let autoHideContainer;
34-
let headerFooter;
35-
3633
beforeEach(() => {
3734
ReactDOM = {render: sinon.spy()};
3835
rangeSlider.__Rewire__('ReactDOM', ReactDOM);
39-
autoHideContainer = sinon.stub().returns(Slider);
40-
rangeSlider.__Rewire__('autoHideContainerHOC', autoHideContainer);
41-
headerFooter = sinon.stub().returns(Slider);
42-
rangeSlider.__Rewire__('headerFooterHOC', headerFooter);
4336

4437
container = document.createElement('div');
4538

@@ -111,8 +104,6 @@ describe('rangeSlider()', () => {
111104
const props = defaultProps;
112105

113106
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
114-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
115-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
116107
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Slider {...props} />);
117108
});
118109

@@ -137,8 +128,6 @@ describe('rangeSlider()', () => {
137128
};
138129

139130
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
140-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
141-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
142131
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Slider {...props} />);
143132
});
144133
});
@@ -206,8 +195,6 @@ describe('rangeSlider()', () => {
206195
};
207196

208197
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
209-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
210-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
211198
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Slider {...props} />);
212199
});
213200
});
@@ -248,8 +235,6 @@ describe('rangeSlider()', () => {
248235
};
249236

250237
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
251-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
252-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
253238
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Slider {...props} />);
254239
});
255240
});
@@ -348,8 +333,6 @@ describe('rangeSlider()', () => {
348333
};
349334

350335
expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
351-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
352-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
353336
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Slider {...props} />);
354337
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
355338
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<Slider {...props} />);
@@ -369,10 +352,10 @@ describe('rangeSlider()', () => {
369352
const targetValue = stats.min + 1;
370353

371354
const state0 = helper.state;
372-
widget._refine(helper, stats, [targetValue, stats.max]);
355+
widget._refine(stats)([targetValue, stats.max]);
373356
const state1 = helper.state;
374357

375-
expect(helper.search.calledOnce).toBe(true, 'search called once');
358+
expect(helper.search.callCount).toBe(1);
376359
expect(state1).toEqual(state0.addNumericRefinement('aNumAttr', '>=', targetValue));
377360
});
378361

@@ -381,10 +364,10 @@ describe('rangeSlider()', () => {
381364
const targetValue = stats.max - 1;
382365

383366
const state0 = helper.state;
384-
widget._refine(helper, stats, [stats.min, targetValue]);
367+
widget._refine(stats)([stats.min, targetValue]);
385368
const state1 = helper.state;
386369

387-
expect(helper.search.calledOnce).toBe(true, 'search called once');
370+
expect(helper.search.callCount).toBe(1);
388371
expect(state1).toEqual(state0.addNumericRefinement('aNumAttr', '<=', targetValue));
389372
});
390373

@@ -393,12 +376,12 @@ describe('rangeSlider()', () => {
393376
const targetValue = [stats.min + 1, stats.max - 1];
394377

395378
const state0 = helper.state;
396-
widget._refine(helper, stats, targetValue);
379+
widget._refine(stats)(targetValue);
397380
const state1 = helper.state;
398381

399382
const expectedState = state0.
400-
addNumericRefinement('aNumAttr', '>=', targetValue[0]).
401-
addNumericRefinement('aNumAttr', '<=', targetValue[1]);
383+
addNumericRefinement('aNumAttr', '>=', targetValue[0]).
384+
addNumericRefinement('aNumAttr', '<=', targetValue[1]);
402385

403386
expect(state1).toEqual(expectedState);
404387
expect(helper.search.calledOnce).toBe(true, 'search called once');

0 commit comments

Comments
 (0)