Skip to content

Commit d8bed96

Browse files
author
Alexandre Stanislawski
committed
feat(connector): price ranges connector
1 parent 7a876f3 commit d8bed96

File tree

8 files changed

+244
-188
lines changed

8 files changed

+244
-188
lines changed

src/components/PriceRanges/PriceRanges.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import PriceRangesForm from './PriceRangesForm.js';
55
import cx from 'classnames';
66
import isEqual from 'lodash/isEqual';
77

8-
class PriceRanges extends React.Component {
8+
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
9+
import headerFooterHOC from '../../decorators/headerFooter.js';
10+
11+
export class RawPriceRanges extends React.Component {
912
componentWillMount() {
1013
this.refine = this.refine.bind(this);
1114
}
@@ -81,7 +84,7 @@ class PriceRanges extends React.Component {
8184
}
8285
}
8386

84-
PriceRanges.propTypes = {
87+
RawPriceRanges.propTypes = {
8588
cssClasses: React.PropTypes.shape({
8689
active: React.PropTypes.string,
8790
button: React.PropTypes.string,
@@ -103,8 +106,8 @@ PriceRanges.propTypes = {
103106
templateProps: React.PropTypes.object.isRequired,
104107
};
105108

106-
PriceRanges.defaultProps = {
109+
RawPriceRanges.defaultProps = {
107110
cssClasses: {},
108111
};
109112

110-
export default PriceRanges;
113+
export default autoHideContainerHOC(headerFooterHOC(RawPriceRanges));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import sinon from 'sinon';
77
import expectJSX from 'expect-jsx';
88
expect.extend(expectJSX);
99
import Template from '../../Template';
10-
import PriceRanges from '../PriceRanges';
10+
import {RawPriceRanges as PriceRanges} from '../PriceRanges';
1111
import PriceRangesForm from '../PriceRangesForm';
1212

1313
describe('PriceRanges', () => {

src/widgets/price-ranges/__tests__/generate-ranges-test.js renamed to src/connectors/price-ranges/__tests__/generate-ranges-test.js

File renamed without changes.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import {
2+
bemHelper,
3+
prepareTemplateProps,
4+
getContainerNode,
5+
} from '../../lib/utils.js';
6+
import generateRanges from './generate-ranges.js';
7+
import defaultTemplates from './defaultTemplates.js';
8+
import cx from 'classnames';
9+
10+
const bem = bemHelper('ais-price-ranges');
11+
12+
/**
13+
* Instantiate a price ranges on a numerical facet
14+
* @function priceRanges
15+
* @param {string|DOMElement} options.container Valid CSS Selector as a string or DOMElement
16+
* @param {string} options.attributeName Name of the attribute for faceting
17+
* @param {Object} [options.templates] Templates to use for the widget
18+
* @param {string|Function} [options.templates.item] Item template. Template data: `from`, `to` and `currency`
19+
* @param {string} [options.currency='$'] The currency to display
20+
* @param {Object} [options.labels] Labels to use for the widget
21+
* @param {string|Function} [options.labels.separator] Separator label, between min and max
22+
* @param {string|Function} [options.labels.button] Button label
23+
* @param {boolean} [options.autoHideContainer=true] Hide the container when no refinements available
24+
* @param {Object} [options.cssClasses] CSS classes to add
25+
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
26+
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
27+
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
28+
* @param {string|string[]} [options.cssClasses.list] CSS class to add to the wrapping list element
29+
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each item element
30+
* @param {string|string[]} [options.cssClasses.active] CSS class to add to the active item element
31+
* @param {string|string[]} [options.cssClasses.link] CSS class to add to each link element
32+
* @param {string|string[]} [options.cssClasses.form] CSS class to add to the form element
33+
* @param {string|string[]} [options.cssClasses.label] CSS class to add to each wrapping label of the form
34+
* @param {string|string[]} [options.cssClasses.input] CSS class to add to each input of the form
35+
* @param {string|string[]} [options.cssClasses.currency] CSS class to add to each currency element of the form
36+
* @param {string|string[]} [options.cssClasses.separator] CSS class to add to the separator of the form
37+
* @param {string|string[]} [options.cssClasses.button] CSS class to add to the submit button of the form
38+
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
39+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
40+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
41+
* @return {Object}
42+
*/
43+
const usage = `Usage:
44+
priceRanges({
45+
container,
46+
attributeName,
47+
[ currency=$ ],
48+
[ cssClasses.{root,header,body,list,item,active,link,form,label,input,currency,separator,button,footer} ],
49+
[ templates.{header,item,footer} ],
50+
[ labels.{currency,separator,button} ],
51+
[ autoHideContainer=true ],
52+
[ collapsible=false ]
53+
})`;
54+
const connectPriceRanges = priceRangesRendering => ({
55+
container,
56+
attributeName,
57+
cssClasses: userCssClasses = {},
58+
templates = defaultTemplates,
59+
collapsible = false,
60+
labels: userLabels = {},
61+
currency: userCurrency = '$',
62+
autoHideContainer = true,
63+
} = {}) => {
64+
let currency = userCurrency;
65+
66+
if (!container || !attributeName) {
67+
throw new Error(usage);
68+
}
69+
70+
const containerNode = getContainerNode(container);
71+
72+
const labels = {
73+
button: 'Go',
74+
separator: 'to',
75+
...userLabels,
76+
};
77+
78+
const cssClasses = {
79+
root: cx(bem(null), userCssClasses.root),
80+
header: cx(bem('header'), userCssClasses.header),
81+
body: cx(bem('body'), userCssClasses.body),
82+
list: cx(bem('list'), userCssClasses.list),
83+
link: cx(bem('link'), userCssClasses.link),
84+
item: cx(bem('item'), userCssClasses.item),
85+
active: cx(bem('item', 'active'), userCssClasses.active),
86+
form: cx(bem('form'), userCssClasses.form),
87+
label: cx(bem('label'), userCssClasses.label),
88+
input: cx(bem('input'), userCssClasses.input),
89+
currency: cx(bem('currency'), userCssClasses.currency),
90+
button: cx(bem('button'), userCssClasses.button),
91+
separator: cx(bem('separator'), userCssClasses.separator),
92+
footer: cx(bem('footer'), userCssClasses.footer),
93+
};
94+
95+
// before we had opts.currency, you had to pass labels.currency
96+
if (userLabels.currency !== undefined && userLabels.currency !== currency) currency = userLabels.currency;
97+
98+
return {
99+
getConfiguration: () => ({
100+
facets: [attributeName],
101+
}),
102+
103+
_generateRanges(results) {
104+
const stats = results.getFacetStats(attributeName);
105+
return generateRanges(stats);
106+
},
107+
108+
_extractRefinedRange(helper) {
109+
const refinements = helper.getRefinements(attributeName);
110+
let from;
111+
let to;
112+
113+
if (refinements.length === 0) {
114+
return [];
115+
}
116+
117+
refinements.forEach(v => {
118+
if (v.operator.indexOf('>') !== -1) {
119+
from = Math.floor(v.value[0]);
120+
} else if (v.operator.indexOf('<') !== -1) {
121+
to = Math.ceil(v.value[0]);
122+
}
123+
});
124+
return [{from, to, isRefined: true}];
125+
},
126+
127+
_refine(helper, from, to) {
128+
const facetValues = this._extractRefinedRange(helper);
129+
130+
helper.clearRefinements(attributeName);
131+
if (facetValues.length === 0 || facetValues[0].from !== from || facetValues[0].to !== to) {
132+
if (typeof from !== 'undefined') {
133+
helper.addNumericRefinement(attributeName, '>=', Math.floor(from));
134+
}
135+
if (typeof to !== 'undefined') {
136+
helper.addNumericRefinement(attributeName, '<=', Math.ceil(to));
137+
}
138+
}
139+
140+
helper.search();
141+
},
142+
143+
init({helper, templatesConfig}) {
144+
this._refine = this._refine.bind(this, helper);
145+
this._templateProps = prepareTemplateProps({
146+
defaultTemplates,
147+
templatesConfig,
148+
templates,
149+
});
150+
151+
priceRangesRendering({
152+
collapsible,
153+
cssClasses,
154+
currency,
155+
facetValues: [],
156+
labels,
157+
refine: this._refine,
158+
shouldAutoHideContainer: autoHideContainer,
159+
templateProps: this._templateProps,
160+
containerNode,
161+
}, true);
162+
},
163+
164+
render({results, helper, state, createURL}) {
165+
let facetValues;
166+
167+
if (results.hits.length > 0) {
168+
facetValues = this._extractRefinedRange(helper);
169+
170+
if (facetValues.length === 0) {
171+
facetValues = this._generateRanges(results);
172+
}
173+
} else {
174+
facetValues = [];
175+
}
176+
177+
facetValues.map(facetValue => {
178+
let newState = state.clearRefinements(attributeName);
179+
if (!facetValue.isRefined) {
180+
if (facetValue.from !== undefined) {
181+
newState = newState.addNumericRefinement(attributeName, '>=', Math.floor(facetValue.from));
182+
}
183+
if (facetValue.to !== undefined) {
184+
newState = newState.addNumericRefinement(attributeName, '<=', Math.ceil(facetValue.to));
185+
}
186+
}
187+
facetValue.url = createURL(newState);
188+
return facetValue;
189+
});
190+
191+
priceRangesRendering({
192+
collapsible,
193+
cssClasses,
194+
currency,
195+
facetValues,
196+
labels,
197+
refine: this._refine,
198+
shouldAutoHideContainer: autoHideContainer && facetValues.length === 0,
199+
templateProps: this._templateProps,
200+
containerNode,
201+
}, false);
202+
},
203+
};
204+
};
205+
206+
export default connectPriceRanges;
File renamed without changes.
File renamed without changes.

src/widgets/price-ranges/__tests__/price-ranges-test.js

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import sinon from 'sinon';
66
import expectJSX from 'expect-jsx';
77
expect.extend(expectJSX);
88
import priceRanges from '../price-ranges.js';
9-
import generateRanges from '../generate-ranges.js';
9+
import generateRanges from '../../../connectors/price-ranges/generate-ranges.js';
1010
import PriceRanges from '../../../components/PriceRanges/PriceRanges.js';
11-
import defaultTemplates from '../defaultTemplates.js';
11+
import defaultTemplates from '../../../connectors/price-ranges/defaultTemplates.js';
1212

1313
describe('priceRanges call', () => {
1414
it('throws an exception when no container', () => {
@@ -29,18 +29,12 @@ describe('priceRanges()', () => {
2929
let results;
3030
let helper;
3131
let state;
32-
let autoHideContainer;
33-
let headerFooter;
3432
let createURL;
3533

3634
beforeEach(() => {
3735
ReactDOM = {render: sinon.spy()};
38-
autoHideContainer = sinon.stub().returns(PriceRanges);
39-
headerFooter = sinon.stub().returns(PriceRanges);
4036

4137
priceRanges.__Rewire__('ReactDOM', ReactDOM);
42-
priceRanges.__Rewire__('autoHideContainerHOC', autoHideContainer);
43-
priceRanges.__Rewire__('headerFooterHOC', headerFooter);
4438

4539
container = document.createElement('div');
4640
widget = priceRanges({container, attributeName: 'aNumAttr', cssClasses: {root: ['root', 'cx']}});
@@ -128,12 +122,6 @@ describe('priceRanges()', () => {
128122
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
129123
});
130124

131-
it('calls the decorators', () => {
132-
widget.render({results, helper, state, createURL});
133-
expect(headerFooter.calledOnce).toBe(true);
134-
expect(autoHideContainer.calledOnce).toBe(true);
135-
});
136-
137125
it('calls getRefinements to check if there are some refinements', () => {
138126
widget.render({results, helper, state, createURL});
139127
expect(helper.getRefinements.calledOnce).toBe(true, 'getRefinements called once');

0 commit comments

Comments
 (0)