Skip to content

Commit 0994e6f

Browse files
author
vvo
committed
feat(priceRanges): polish priceRanges widget
1 parent e5fe344 commit 0994e6f

File tree

11 files changed

+192
-77
lines changed

11 files changed

+192
-77
lines changed

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ instantsearch({
305305
[hierarchicalMenu]: ./widgets-screenshots/hierarchicalMenu.png
306306
[menu]: ./widgets-screenshots/menu.png
307307
[rangeSlider]: ./widgets-screenshots/range-slider.png
308+
[priceRanges]: ./widgets-screenshots/price-ranges.png
308309

309310
### searchBox
310311

@@ -963,6 +964,57 @@ search.addWidget(
963964
);
964965
```
965966

967+
### priceRanges
968+
969+
![Example of the pricesRanges widget][priceRanges]
970+
971+
#### API
972+
973+
```js
974+
/**
975+
* Instantiate a price ranges on a numerical facet
976+
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
977+
* @param {String} options.facetName Name of the attribute for faceting
978+
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements: root, range
979+
* @param {String|String[]} [options.cssClasses.root] CSS class to add to the root element
980+
* @param {String|String[]} [options.cssClasses.header] CSS class to add to the header element
981+
* @param {String|String[]} [options.cssClasses.body] CSS class to add to the body element
982+
* @param {String|String[]} [options.cssClasses.footer] CSS class to add to the footer element
983+
* @param {String|String[]} [options.cssClasses.range] CSS class to add to the range element
984+
* @param {String|String[]} [options.cssClasses.input] CSS class to add to the min/max input elements
985+
* @param {String|String[]} [options.cssClasses.button] CSS class to add to the button element
986+
* @param {Object} [options.templates] Templates to use for the widget
987+
* @param {String|Function} [options.templates.range] Range template
988+
* @param {Object} [options.labels] Labels to use for the widget
989+
* @param {String|Function} [options.labels.button] Button label
990+
* @param {String|Function} [options.labels.currency] Currency label
991+
* @param {String|Function} [options.labels.to] To label
992+
* @param {boolean} [hideWhenNoResults=true] Hide the container when no results match
993+
* @return {Object}
994+
*/
995+
```
996+
997+
#### Usage
998+
999+
```js
1000+
search.addWidget(
1001+
instantsearch.widgets.priceRanges({
1002+
container: '#price-ranges',
1003+
facetName: 'price'
1004+
})
1005+
);
1006+
```
1007+
1008+
#### Styling
1009+
1010+
```html
1011+
1012+
```
1013+
1014+
```css
1015+
1016+
```
1017+
9661018
### hierarchicalMenu
9671019

9681020
![Example of the hierarchicalMenu widget][hierarchicalMenu]

components/PriceRanges.js

+12-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ class PriceRange extends React.Component {
1010
this.props.refine(from, to);
1111
}
1212

13+
_handleSubmit(e) {
14+
this.refine(+this.refs.from.value || undefined, +this.refs.to.value || undefined, e);
15+
}
16+
1317
render() {
1418
return (
15-
<div className={this.props.cssClasses.root}>
19+
<div>
1620
{this.props.facetValues.map(facetValue => {
1721
var key = facetValue.from + '_' + facetValue.to;
1822
return (
@@ -26,7 +30,11 @@ class PriceRange extends React.Component {
2630
</a>
2731
);
2832
})}
29-
<div className={this.props.cssClasses.inputGroup}>
33+
<form
34+
className={this.props.cssClasses.form}
35+
onSubmit={this._handleSubmit.bind(this)}
36+
ref="form"
37+
>
3038
<label>
3139
{this.props.labels.currency}{' '}
3240
<input className={this.props.cssClasses.input} ref="from" type="number" />
@@ -39,22 +47,16 @@ class PriceRange extends React.Component {
3947
{' '}
4048
<button
4149
className={this.props.cssClasses.button}
42-
onClick={(e) => {
43-
this.refine(+this.refs.from.value || undefined, +this.refs.to.value || undefined, e);
44-
}}
50+
type="submit"
4551
>{this.props.labels.button}</button>
46-
</div>
52+
</form>
4753
</div>
4854
);
4955
}
5056
}
5157

5258
PriceRange.propTypes = {
5359
cssClasses: React.PropTypes.shape({
54-
root: React.PropTypes.oneOfType([
55-
React.PropTypes.string,
56-
React.PropTypes.arrayOf(React.PropTypes.string)
57-
]),
5860
range: React.PropTypes.oneOfType([
5961
React.PropTypes.string,
6062
React.PropTypes.arrayOf(React.PropTypes.string)

components/__tests__/PriceRanges-test.js

+38-16
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
import React from 'react';
44
import expect from 'expect';
55
import TestUtils from 'react-addons-test-utils';
6+
import sinon from 'sinon';
7+
import jsdom from 'mocha-jsdom';
8+
9+
import expectJSX from 'expect-jsx';
10+
expect.extend(expectJSX);
11+
612
import PriceRanges from '../PriceRanges';
713
import generateRanges from '../../widgets/price-ranges/generate-ranges.js';
814

915
describe('PriceRanges', () => {
1016
var renderer;
1117

18+
jsdom({useEach: true});
19+
1220
beforeEach(() => {
1321
let {createRenderer} = TestUtils;
1422
renderer = createRenderer();
@@ -17,6 +25,7 @@ describe('PriceRanges', () => {
1725
context('with stats', () => {
1826
var out;
1927
var facetValues;
28+
var props;
2029

2130
beforeEach(() => {
2231
facetValues = generateRanges({
@@ -26,13 +35,12 @@ describe('PriceRanges', () => {
2635
sum: 2433490.0
2736
});
2837

29-
var props = {
38+
props = {
3039
templateProps: {},
3140
facetValues,
3241
cssClasses: {
33-
root: 'root-class',
3442
range: 'range-class',
35-
inputGroup: 'input-group-class',
43+
form: 'form-class',
3644
button: 'button-class',
3745
input: 'input-class'
3846
},
@@ -41,18 +49,13 @@ describe('PriceRanges', () => {
4149
to: 'to',
4250
button: 'Go'
4351
},
44-
refine: () => {}
52+
refine: sinon.spy()
4553
};
4654

4755
renderer.render(<PriceRanges {...props} />);
4856
out = renderer.getRenderOutput();
4957
});
5058

51-
it('should add the root class', () => {
52-
expect(out.type).toBe('div');
53-
expect(out.props.className).toEqual('root-class');
54-
});
55-
5659
it('should have the right number of children', () => {
5760
expect(out.props.children.length).toEqual(2);
5861
expect(out.props.children[0].length).toEqual(facetValues.length);
@@ -64,16 +67,15 @@ describe('PriceRanges', () => {
6467
});
6568
});
6669

67-
it('should have the input group class', () => {
70+
it('should have the form class', () => {
6871
expect(out.props.children.length).toEqual(2);
69-
expect(out.props.children[1].props.className).toEqual('input-group-class');
72+
expect(out.props.children[1].props.className).toEqual('form-class');
7073
});
7174

7275
it('should display the inputs with the associated class & labels', () => {
7376
expect(out.props.children.length).toEqual(2);
74-
var click = out.props.children[1].props.children[6].props.onClick;
75-
expect(out.props.children[1]).toEqual(
76-
<div className="input-group-class">
77+
expect(out.props.children[1]).toEqualJSX(
78+
<form className="form-class" onSubmit={() => {}}>
7779
<label>
7880
USD{' '}<input className="input-class" ref="from" type="number" />
7981
</label>
@@ -82,9 +84,29 @@ describe('PriceRanges', () => {
8284
USD{' '}<input className="input-class" ref="to" type="number" />
8385
</label>
8486
{' '}
85-
<button className="button-class" onClick={click}>Go</button>
86-
</div>
87+
<button className="button-class" type="submit">Go</button>
88+
</form>
8789
);
8890
});
91+
92+
it('refine on submit', () => {
93+
// cannot currently use shallow rendering to test refs
94+
props.templateProps = {
95+
templates: {header: '', range: '', footer: ''},
96+
templatesConfig: {},
97+
transformData: undefined,
98+
useCustomCompileOptions: {header: false, footer: false, range: false}
99+
};
100+
101+
let eventData = {preventDefault: sinon.spy()};
102+
let component = TestUtils.renderIntoDocument(<PriceRanges {...props} />);
103+
component.refs.from.value = 10;
104+
component.refs.to.value = 20;
105+
TestUtils.Simulate.change(component.refs.from);
106+
TestUtils.Simulate.change(component.refs.to);
107+
TestUtils.Simulate.submit(component.refs.form, eventData);
108+
expect(props.refine.firstCall.args).toEqual([10, 20]);
109+
expect(eventData.preventDefault.calledOnce).toBe(true);
110+
});
89111
});
90112
});

example/app.js

+8-26
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,6 @@ search.addWidget(
7878
})
7979
);
8080

81-
search.addWidget(
82-
instantsearch.widgets.refinementList({
83-
container: '#price-range',
84-
facetName: 'price_range',
85-
operator: 'and',
86-
limit: 10,
87-
cssClasses: {
88-
header: 'panel-heading',
89-
root: 'list-group'
90-
},
91-
templates: {
92-
header: 'Price ranges',
93-
item: require('./templates/and.html')
94-
},
95-
transformData: function(data) {
96-
data.name = data.name.replace(/(\d+) - (\d+)/, '$$$1 - $$$2').replace(/> (\d+)/, '> $$$1');
97-
return data;
98-
}
99-
})
100-
);
101-
10281
search.addWidget(
10382
instantsearch.widgets.toggle({
10483
container: '#free-shipping',
@@ -167,23 +146,26 @@ search.addWidget(
167146
})
168147
);
169148

170-
171149
search.once('render', function() {
172150
document.querySelector('.search').className = 'row search search--visible';
173151
});
174152

175153
search.addWidget(
176154
instantsearch.widgets.priceRanges({
177-
container: '#price_ranges',
155+
container: '#price-ranges',
178156
facetName: 'price',
157+
templates: {
158+
header: 'Price ranges'
159+
},
179160
cssClasses: {
180-
root: 'nav nav-stacked',
161+
header: 'panel-heading',
162+
body: 'nav nav-stacked',
181163
range: 'list-group-item',
182-
inputGroup: 'list-group-item form-inline',
164+
form: 'list-group-item form-inline',
183165
input: 'form-control input-sm fixed-input-sm',
184166
button: 'btn btn-default btn-sm'
185167
},
186-
template: require('./templates/price_range.html')
168+
template: require('./templates/price-ranges.html')
187169
})
188170
);
189171

example/index.html

+1-6
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,13 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
2525

2626
<div class="panel panel-default" id="brands"></div>
2727

28-
<div class="panel panel-default" id="price-range"></div>
29-
3028
<div class="panel panel-default" id="free-shipping"></div>
3129

3230
<div class="panel panel-default" id="price"></div>
3331

3432
<div class="panel panel-default" id="hierarchical-categories"></div>
3533

36-
<div class="panel panel-default">
37-
<div class="panel-heading">Price</div>
38-
<div id="price_ranges" class="list-group"></div>
39-
</div>
34+
<div class="panel panel-default" id="price-ranges"></div>
4035

4136
</div>
4237
<div class="col-md-9">

example/style.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,6 @@ body {
8181
display: block;
8282
}
8383

84-
.fixed-input-sm {
85-
width: 65px !important;
84+
.ais-price-ranges .ais-price-ranges--input{
85+
width: 65px;
8686
}

example/templates/price_range.html example/templates/price-ranges.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
${{to}}
1616
{{/to}}
1717
<span>{{count}}</span>
18-
</a>
18+
</a>

widgets-screenshots/price-ranges.png

16.5 KB
Loading

0 commit comments

Comments
 (0)