Skip to content

Commit ad58d7a

Browse files
committed
feat(priceRanges): Add BEM classes and tests
Fixes #387 BREAKING CHANGE: `ais-price-ranges--range` are now named `ais-price-ranges--item` and are wrapped in a `ais-price-ranges--list`. I've moved the bottom form into it's own PriceRangesForm component, along with its own tests. I've fixed a minor typo where the component was internally named PriceRange (without the final __s__). I factorize some logic form the render in individual methods and manage to individually test them. This was not an easy task. I had to mock the default `render` (so it does nothing) before instanciating the component. Then, I was able to call each inner method individually. This requires to stub prototype methods in beforeEach, then restore them in afterEach. I've added a few helper methods, this can surely be simplified again but this gives nice granularity in testing. I've renamed the `range` items to `item` and wrapped them in a `list`. I've also added classes to all elements we add (`label`, `separator`, etc). I've removed the empty `span`s.
1 parent 09e8f7f commit ad58d7a

12 files changed

+650
-273
lines changed

README.md

+70-12
Original file line numberDiff line numberDiff line change
@@ -1047,20 +1047,27 @@ search.addWidget(
10471047
* Instantiate a price ranges on a numerical facet
10481048
* @param {string|DOMElement} options.container Valid CSS Selector as a string or DOMElement
10491049
* @param {string} options.facetName Name of the attribute for faceting
1050-
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements: root, range
1051-
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
1052-
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
1053-
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
1054-
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
1055-
* @param {string|string[]} [options.cssClasses.range] CSS class to add to the range element
1056-
* @param {string|string[]} [options.cssClasses.input] CSS class to add to the min/max input elements
1057-
* @param {string|string[]} [options.cssClasses.button] CSS class to add to the button element
1050+
* @param {Object} [options.cssClasses] CSS classes to add
1051+
* @param {string} [options.cssClasses.root] CSS class to add to the root element
1052+
* @param {string} [options.cssClasses.header] CSS class to add to the header element
1053+
* @param {string} [options.cssClasses.body] CSS class to add to the body element
1054+
* @param {string} [options.cssClasses.list] CSS class to add to the wrapping list element
1055+
* @param {string} [options.cssClasses.item] CSS class to add to each item element
1056+
* @param {string} [options.cssClasses.active] CSS class to add to the active item element
1057+
* @param {string} [options.cssClasses.link] CSS class to add to each link element
1058+
* @param {string} [options.cssClasses.form] CSS class to add to the form element
1059+
* @param {string} [options.cssClasses.label] CSS class to add to each wrapping label of the form
1060+
* @param {string} [options.cssClasses.input] CSS class to add to each input of the form
1061+
* @param {string} [options.cssClasses.currency] CSS class to add to each currency element of the form
1062+
* @param {string} [options.cssClasses.separator] CSS class to add to the separator of the form
1063+
* @param {string} [options.cssClasses.button] CSS class to add to the submit button of the form
1064+
* @param {string} [options.cssClasses.footer] CSS class to add to the footer element
10581065
* @param {Object} [options.templates] Templates to use for the widget
1059-
* @param {string|Function} [options.templates.range] Range template
1066+
* @param {string|Function} [options.templates.item] Item template
10601067
* @param {Object} [options.labels] Labels to use for the widget
1061-
* @param {string|Function} [options.labels.button] Button label
10621068
* @param {string|Function} [options.labels.currency] Currency label
1063-
* @param {string|Function} [options.labels.to] To label
1069+
* @param {string|Function} [options.labels.separator] Separator labe, between min and max
1070+
* @param {string|Function} [options.labels.button] Button label
10641071
* @param {boolean} [hideContainerWhenNoResults=true] Hide the container when no results match
10651072
* @return {Object}
10661073
*/
@@ -1080,10 +1087,61 @@ search.addWidget(
10801087
#### Styling
10811088

10821089
```html
1083-
1090+
<div class="ais-price-ranges">
1091+
<div class="ais-price-ranges--header ais-header">Header</div>
1092+
<div class="ais-price-ranges--body">
1093+
<div class="ais-price-ranges--item">
1094+
<a class="ais-price-ranges--range" href="...">$3 - $13</a>
1095+
</div>
1096+
<div class="ais-price-ranges--item ais-price-ranges--item__active">
1097+
<a class="ais-price-ranges--range" href="...">$13 - $40</a>
1098+
</div>
1099+
<form class="ais-price-ranges--form">
1100+
<label class="ais-price-ranges--label">
1101+
<span class="ais-price-ranges--currency>$ </span>
1102+
<input class="ais-price-ranges--input" />
1103+
</label>
1104+
<span class="ais-price-ranges--separator"> to </span>
1105+
<label class="ais-price-ranges--label">
1106+
<span class="ais-price-ranges--currency>$ </span>
1107+
<input class="ais-price-ranges--input" />
1108+
</label>
1109+
<button class="ais-price-ranges--button">Go</button>
1110+
</form>
1111+
</div>
1112+
<div class="ais-price-ranges--footer ais-footer">Footer</div>
1113+
</div>
10841114
```
10851115

10861116
```css
1117+
.ais-price-ranges {
1118+
}
1119+
.ais-price-ranges--header {
1120+
}
1121+
.ais-price-ranges--body {
1122+
}
1123+
.ais-price-ranges--list {
1124+
}
1125+
.ais-price-ranges--item {
1126+
}
1127+
.ais-price-ranges--item__active {
1128+
}
1129+
.ais-price-ranges--link {
1130+
}
1131+
.ais-price-ranges--form {
1132+
}
1133+
.ais-price-ranges--label {
1134+
}
1135+
.ais-price-ranges--currency {
1136+
}
1137+
.ais-price-ranges--input {
1138+
}
1139+
.ais-price-ranges--separator {
1140+
}
1141+
.ais-price-ranges--button {
1142+
}
1143+
.ais-price-ranges--footer {
1144+
}
10871145

10881146
```
10891147

components/PriceRanges.js

-83
This file was deleted.

components/PriceRanges/PriceRanges.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
var React = require('react');
2+
3+
var Template = require('../Template');
4+
var PriceRangesForm = require('./PriceRangesForm');
5+
var cx = require('classnames');
6+
7+
class PriceRanges extends React.Component {
8+
getForm() {
9+
return (
10+
<PriceRangesForm
11+
cssClasses={this.props.cssClasses}
12+
labels={this.props.labels}
13+
refine={this.refine.bind(this)}
14+
/>
15+
);
16+
}
17+
18+
getURLFromFacetValue(facetValue) {
19+
if (!this.props.createURL) {
20+
return '#';
21+
}
22+
return this.props.createURL(facetValue.from, facetValue.to, facetValue.isRefined);
23+
}
24+
25+
getItemFromFacetValue(facetValue) {
26+
let cssClassItem = cx(
27+
this.props.cssClasses.item,
28+
{[this.props.cssClasses.active]: facetValue.isRefined}
29+
);
30+
let url = this.getURLFromFacetValue(facetValue);
31+
let key = facetValue.from + '_' + facetValue.to;
32+
let handleClick = this.refine.bind(this, facetValue.from, facetValue.to);
33+
return (
34+
<div className={cssClassItem} key={key}>
35+
<a
36+
className={this.props.cssClasses.link}
37+
href={url}
38+
onClick={handleClick}
39+
>
40+
<Template data={facetValue} templateKey="item" {...this.props.templateProps} />
41+
</a>
42+
</div>
43+
);
44+
}
45+
46+
refine(from, to, event) {
47+
event.preventDefault();
48+
this.setState({
49+
formFromValue: null,
50+
formToValue: null
51+
});
52+
this.props.refine(from, to);
53+
}
54+
55+
render() {
56+
let form = this.getForm();
57+
return (
58+
<div>
59+
<div className={this.props.cssClasses.list}>
60+
{this.props.facetValues.map(facetValue => {
61+
return this.getItemFromFacetValue(facetValue);
62+
})}
63+
</div>
64+
{form}
65+
</div>
66+
);
67+
}
68+
}
69+
70+
PriceRanges.propTypes = {
71+
createURL: React.PropTypes.func.isRequired,
72+
cssClasses: React.PropTypes.shape({
73+
active: React.PropTypes.string,
74+
button: React.PropTypes.string,
75+
form: React.PropTypes.string,
76+
input: React.PropTypes.string,
77+
item: React.PropTypes.string,
78+
label: React.PropTypes.string,
79+
link: React.PropTypes.string,
80+
list: React.PropTypes.string,
81+
separator: React.PropTypes.string
82+
}),
83+
facetValues: React.PropTypes.array,
84+
labels: React.PropTypes.shape({
85+
button: React.PropTypes.string,
86+
currency: React.PropTypes.string,
87+
to: React.PropTypes.string
88+
}),
89+
refine: React.PropTypes.func.isRequired,
90+
templateProps: React.PropTypes.object.isRequired
91+
};
92+
93+
PriceRanges.defaultProps = {
94+
cssClasses: {}
95+
};
96+
97+
module.exports = PriceRanges;
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
var React = require('react');
2+
3+
class PriceRangesForm extends React.Component {
4+
getInput(type) {
5+
return (
6+
<label className={this.props.cssClasses.label}>
7+
<span className={this.props.cssClasses.currency}>{this.props.labels.currency} </span>
8+
<input className={this.props.cssClasses.input} ref={type} type="number" />
9+
</label>
10+
);
11+
}
12+
13+
handleSubmit(event) {
14+
let from = +this.refs.from.value || undefined;
15+
let to = +this.refs.to.value || undefined;
16+
this.props.refine(from, to, event);
17+
}
18+
19+
render() {
20+
let fromInput = this.getInput('from');
21+
let toInput = this.getInput('to');
22+
let onSubmit = this.handleSubmit.bind(this);
23+
return (
24+
<form className={this.props.cssClasses.form} onSubmit={onSubmit} ref="form">
25+
{fromInput}
26+
<span className={this.props.cssClasses.separator}> {this.props.labels.separator} </span>
27+
{toInput}
28+
<button className={this.props.cssClasses.button} type="submit">{this.props.labels.button}</button>
29+
</form>
30+
);
31+
}
32+
}
33+
34+
PriceRangesForm.propTypes = {
35+
cssClasses: React.PropTypes.shape({
36+
button: React.PropTypes.string,
37+
currency: React.PropTypes.string,
38+
form: React.PropTypes.string,
39+
input: React.PropTypes.string,
40+
label: React.PropTypes.string,
41+
separator: React.PropTypes.string
42+
}),
43+
labels: React.PropTypes.shape({
44+
button: React.PropTypes.string,
45+
currency: React.PropTypes.string,
46+
separator: React.PropTypes.string
47+
}),
48+
refine: React.PropTypes.func.isRequired
49+
};
50+
51+
52+
PriceRangesForm.defaultProps = {
53+
cssClasses: {},
54+
labels: {}
55+
};
56+
57+
58+
module.exports = PriceRangesForm;
59+

0 commit comments

Comments
 (0)