Skip to content

Commit a3e0f78

Browse files
committed
feat(hits-per-page-selector): New widget to change hitsPerPage
Did a refactoring using Selector instead of IndexSelector for both indexSelector and hitsPerPageSelector. Closes #331
1 parent 4837a4a commit a3e0f78

File tree

12 files changed

+347
-108
lines changed

12 files changed

+347
-108
lines changed

components/IndexSelector.js

-38
This file was deleted.

components/Selector.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
var React = require('react');
2+
3+
class Selector extends React.Component {
4+
handleChange(event) {
5+
this.props.setValue(event.target.value);
6+
}
7+
8+
render() {
9+
var {currentValue, options} = this.props;
10+
11+
var handleChange = this.handleChange.bind(this);
12+
13+
return (
14+
<select
15+
className={this.props.cssClasses.root}
16+
onChange={handleChange}
17+
value={currentValue}
18+
>
19+
{options.map((option) => {
20+
return <option className={this.props.cssClasses.item} key={option.value} value={option.value}>{option.label}</option>;
21+
})}
22+
</select>
23+
);
24+
}
25+
}
26+
27+
Selector.propTypes = {
28+
cssClasses: React.PropTypes.shape({
29+
root: React.PropTypes.oneOfType([
30+
React.PropTypes.string,
31+
React.PropTypes.arrayOf(React.PropTypes.string)
32+
]),
33+
item: React.PropTypes.oneOfType([
34+
React.PropTypes.string,
35+
React.PropTypes.arrayOf(React.PropTypes.string)
36+
])
37+
}),
38+
currentValue: React.PropTypes.oneOfType([
39+
React.PropTypes.string,
40+
React.PropTypes.number
41+
]).isRequired,
42+
options: React.PropTypes.arrayOf(
43+
React.PropTypes.shape({
44+
value: React.PropTypes.oneOfType([
45+
React.PropTypes.string,
46+
React.PropTypes.number
47+
]).isRequired,
48+
label: React.PropTypes.string.isRequired
49+
})
50+
).isRequired,
51+
setValue: React.PropTypes.func.isRequired
52+
};
53+
54+
module.exports = Selector;

components/__tests__/IndexSelector-test.js

-53
This file was deleted.

components/__tests__/Selector-test.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* eslint-env mocha */
2+
3+
import React from 'react';
4+
import expect from 'expect';
5+
import TestUtils from 'react-addons-test-utils';
6+
import Selector from '../Selector';
7+
8+
import expectJSX from 'expect-jsx';
9+
expect.extend(expectJSX);
10+
11+
describe('Selector', () => {
12+
var renderer;
13+
14+
beforeEach(() => {
15+
let {createRenderer} = TestUtils;
16+
renderer = createRenderer();
17+
});
18+
19+
20+
it('should render <Selector/> with strings', () => {
21+
var out = render({
22+
currentValue: 'index-a',
23+
cssClasses: {
24+
root: 'custom-root',
25+
item: 'custom-item'
26+
},
27+
options: [{value: 'index-a', label: 'Index A'}, {value: 'index-b', label: 'Index B'}]
28+
});
29+
expect(out).toEqualJSX(
30+
<select
31+
className="custom-root"
32+
onChange={() => {}}
33+
value="index-a"
34+
>
35+
<option className="custom-item" value="index-a">Index A</option>
36+
<option className="custom-item" value="index-b">Index B</option>
37+
</select>
38+
);
39+
});
40+
41+
it('should render <Selector/> with numbers', () => {
42+
var out = render({
43+
currentValue: 10,
44+
cssClasses: {
45+
root: 'custom-root',
46+
item: 'custom-item'
47+
},
48+
options: [{value: 10, label: '10 results per page'}, {value: 20, label: '20 results per page'}]
49+
});
50+
expect(out).toEqualJSX(
51+
<select
52+
className="custom-root"
53+
onChange={() => {}}
54+
value={10}
55+
>
56+
<option className="custom-item" value={10}>10 results per page</option>
57+
<option className="custom-item" value={20}>20 results per page</option>
58+
</select>
59+
);
60+
});
61+
62+
function render(props = {}) {
63+
renderer.render(<Selector {...props} />);
64+
return renderer.getRenderOutput();
65+
}
66+
});

example/app.js

+14
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ search.addWidget(
3838
})
3939
);
4040

41+
search.addWidget(
42+
instantsearch.widgets.hitsPerPageSelector({
43+
container: '#hits-per-page-selector',
44+
options: [
45+
{value: 6, label: '6 per page'},
46+
{value: 12, label: '12 per page'},
47+
{value: 24, label: '24 per page'}
48+
],
49+
cssClasses: {
50+
select: 'form-control'
51+
}
52+
})
53+
);
54+
4155
search.addWidget(
4256
instantsearch.widgets.hits({
4357
container: '#hits',

example/index.html

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,17 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
3434
<input id="search-box" class="form-control" />
3535
</div>
3636
<div class="row">
37-
<div class="col-md-8">
37+
<div class="col-md-4">
3838
<div id="stats"></div>
3939
</div>
40+
<div class="col-md-4 text-right">
41+
<div class="form-inline">
42+
<div class="form-group">
43+
<label for="hits-per-page-select">Show:</label>
44+
<span id="hits-per-page-selector"></span>
45+
</div>
46+
</div>
47+
</div>
4048
<div class="col-md-4 text-right">
4149
<div class="form-inline">
4250
<div class="form-group">

lib/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var instantsearch = toFactory(InstantSearch);
99
instantsearch.widgets = {
1010
hierarchicalMenu: require('../widgets/hierarchical-menu/hierarchical-menu.js'),
1111
hits: require('../widgets/hits/hits'),
12+
hitsPerPageSelector: require('../widgets/hits-per-page-selector/hits-per-page-selector'),
1213
indexSelector: require('../widgets/index-selector/index-selector'),
1314
menu: require('../widgets/menu/menu.js'),
1415
refinementList: require('../widgets/refinement-list/refinement-list.js'),

lib/url-sync.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class URLSync {
9595
this.originalConfig = null;
9696
this.timer = timerMaker(Date.now());
9797
this.threshold = options.threshold || 700;
98-
this.trackedParameters = options.trackedParameters || ['query', 'attribute:*', 'index', 'page'];
98+
this.trackedParameters = options.trackedParameters || ['query', 'attribute:*', 'index', 'page', 'hitsPerPage'];
9999
}
100100

101101
getConfiguration(currentConfiguration) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/* eslint-env mocha */
2+
3+
import React from 'react';
4+
import expect from 'expect';
5+
import sinon from 'sinon';
6+
import jsdom from 'mocha-jsdom';
7+
8+
import expectJSX from 'expect-jsx';
9+
expect.extend(expectJSX);
10+
11+
import hitsPerPageSelector from '../hits-per-page-selector';
12+
import Selector from '../../../components/Selector';
13+
14+
describe('hitsPerPageSelector()', () => {
15+
jsdom({useEach: true});
16+
17+
var ReactDOM;
18+
var container;
19+
var options;
20+
var cssClasses;
21+
var widget;
22+
var props;
23+
var helper;
24+
var results;
25+
var autoHideContainer;
26+
27+
beforeEach(() => {
28+
autoHideContainer = sinon.stub().returns(Selector);
29+
ReactDOM = {render: sinon.spy()};
30+
31+
hitsPerPageSelector.__Rewire__('ReactDOM', ReactDOM);
32+
hitsPerPageSelector.__Rewire__('autoHideContainer', autoHideContainer);
33+
34+
container = document.createElement('div');
35+
options = [
36+
{value: 10, label: '10 results'},
37+
{value: 20, label: '20 results'}
38+
];
39+
cssClasses = {
40+
root: 'custom-root',
41+
item: 'custom-item'
42+
};
43+
widget = hitsPerPageSelector({container, options, cssClasses});
44+
helper = {
45+
state: {
46+
hitsPerPage: 20
47+
},
48+
setQueryParameter: sinon.spy(),
49+
search: sinon.spy()
50+
};
51+
results = {
52+
hits: []
53+
};
54+
});
55+
56+
it('doesn\'t configure anything', () => {
57+
expect(widget.getConfiguration).toEqual(undefined);
58+
});
59+
60+
it('calls ReactDOM.render(<Selector props />, container)', () => {
61+
widget.render({helper, results, state: helper.state});
62+
props = {
63+
cssClasses: {
64+
root: 'ais-hits-per-page-selector custom-root',
65+
item: 'ais-hits-per-page-selector--item custom-item'
66+
},
67+
currentValue: 20,
68+
hasResults: false,
69+
hideContainerWhenNoResults: false,
70+
options: [
71+
{value: 10, label: '10 results'},
72+
{value: 20, label: '20 results'}
73+
],
74+
setValue: () => {}
75+
};
76+
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
77+
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Selector {...props} />);
78+
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
79+
});
80+
81+
it('sets the underlying hitsPerPage', () => {
82+
widget.setHitsPerPage(helper, helper.state, 10);
83+
expect(helper.setQueryParameter.calledOnce).toBe(true, 'setQueryParameter called once');
84+
expect(helper.search.calledOnce).toBe(true, 'search called once');
85+
});
86+
87+
it('should throw if there is no name attribute in a passed object', () => {
88+
options.length = 0;
89+
options.push({label: 'Label without a value'});
90+
expect(() => {
91+
widget.init(helper.state, helper);
92+
}).toThrow(/No option in `options` with `value: 20`/);
93+
});
94+
95+
it('must include the current hitsPerPage at initialization time', () => {
96+
helper.state.hitsPerPage = -1;
97+
expect(() => {
98+
widget.init(helper.state, helper);
99+
}).toThrow(/No option in `options` with `value: -1`/);
100+
});
101+
102+
afterEach(() => {
103+
hitsPerPageSelector.__ResetDependency__('ReactDOM');
104+
hitsPerPageSelector.__ResetDependency__('autoHideContainer');
105+
});
106+
});

0 commit comments

Comments
 (0)