Skip to content

Commit a888143

Browse files
author
vvo
committed
feat(menu): first widget version
1 parent 5cbf451 commit a888143

File tree

9 files changed

+182
-24
lines changed

9 files changed

+182
-24
lines changed

README.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,17 @@ search.addWidget(
171171

172172
```js
173173
/**
174-
* Instantiate a list of refinement based on a facet
174+
* Instantiate a list of refinements based on a facet
175175
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
176176
* @param {String} options.facetName Name of the attribute for faceting
177177
* @param {String} options.operator How to apply refinements. Possible values: `or`, `and`
178178
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
179179
* @param {String} [options.limit=100] How much facet values to get.
180-
* @param {String|String[]} [options.cssClass=null] CSS class(es) for the main `<ul>` wrapper.
180+
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
181+
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
181182
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
183+
* @param {String|Function} [options.singleRefine=true] Are multiple refinements allowed or only one at the same time. You can use this
184+
* to build radio based refinement lists for example.
182185
* @return {Object}
183186
*/
184187
```
@@ -199,3 +202,37 @@ search.addWidget(
199202
})
200203
);
201204
```
205+
206+
### menu
207+
208+
#### API
209+
210+
```js
211+
/**
212+
* Create a menu out of a facet
213+
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
214+
* @param {String} options.facetName Name of the attribute for faceting
215+
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
216+
* @param {String} [options.limit=100] How much facet values to get.
217+
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
218+
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
219+
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
220+
* @return {Object}
221+
*/
222+
```
223+
224+
225+
#### Usage
226+
227+
```html
228+
<div id="categories"></div>
229+
```
230+
231+
```js
232+
search.addWidget(
233+
instantsearch.widgets.menu({
234+
container: '#categories',
235+
facetName: 'categories'
236+
})
237+
);
238+
```

components/MultipleChoiceList.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ var Template = require('./Template');
55
class MultipleChoiceList extends React.Component {
66
refine(value) {
77
this.props.toggleRefine(value);
8-
this.props.search();
98
}
109

1110
// Click events on DOM tree like LABEL > INPUT will result in two click events
@@ -46,10 +45,10 @@ class MultipleChoiceList extends React.Component {
4645
var template = this.props.template;
4746

4847
return (
49-
<ul className={this.props.cssClass}>
48+
<ul className={this.props.rootClass}>
5049
{facetValues.map(facetValue => {
5150
return (
52-
<li key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
51+
<li className={this.props.itemClass} key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
5352
<Template data={facetValue} template={template} />
5453
</li>
5554
);
@@ -60,12 +59,15 @@ class MultipleChoiceList extends React.Component {
6059
}
6160

6261
MultipleChoiceList.propTypes = {
63-
cssClass: React.PropTypes.oneOfType([
62+
rootClass: React.PropTypes.oneOfType([
6463
React.PropTypes.string,
65-
React.PropTypes.array
64+
React.PropTypes.arrayOf(React.PropTypes.string)
65+
]),
66+
itemClass: React.PropTypes.oneOfType([
67+
React.PropTypes.string,
68+
React.PropTypes.arrayOf(React.PropTypes.string)
6669
]),
6770
facetValues: React.PropTypes.array,
68-
search: React.PropTypes.func.isRequired,
6971
template: React.PropTypes.oneOfType([
7072
React.PropTypes.string,
7173
React.PropTypes.func

example/app.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,19 @@ search.addWidget(
4747
facetName: 'brand',
4848
operator: 'or',
4949
limit: 10,
50-
cssClass: 'nav nav-stacked',
50+
rootClass: 'nav nav-stacked',
5151
template: require('./templates/or.html')
5252
})
5353
);
5454

55+
search.addWidget(
56+
instantsearch.widgets.menu({
57+
container: '#categories',
58+
facetName: 'categories',
59+
limit: 10,
60+
rootClass: 'list-unstyled',
61+
template: require('./templates/category.html')
62+
})
63+
);
64+
5565
search.start();

example/index.html

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
1414

1515
<div class="row">
1616
<div class="col-md-3">
17-
<div id="search-box"></div>
17+
<div class="panel">
18+
<div class="panel-body" id="search-box"></div>
19+
</div>
1820

19-
<h2>Brands</h1>
20-
<div id="brands"></div>
21+
<div class="panel panel-default">
22+
<div class="panel-heading">Categories</div>
23+
<div id="categories" class="list-group"></div>
24+
</div>
25+
26+
<div class="panel panel-default">
27+
<div class="panel-heading">Brands</div>
28+
<div class="panel-body" id="brands"></div>
29+
</div>
2130
</div>
2231
<div class="col-md-9">
2332
<div id="stats"></div>

example/templates/category.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<a href="#" class="list-group-item{{#isRefined}} active{{/isRefined}}">{{name}} <span class="badge">{{count}}</span></a>

example/templates/or.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
<label class="checkbox">
2-
<input type="checkbox" value="{{name}}" {{#isRefined}}checked{{/isRefined}} />{{name}} <span class="badge pull-right">{{count}}</span>
3-
</label>
1+
<div class="checkbox">
2+
<label>
3+
<input type="checkbox" value="{{name}}" {{#isRefined}}checked{{/isRefined}} />{{name}}
4+
</label>
5+
<span class="badge pull-right">{{count}}</span>
6+
</div>

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
InstantSearch: require('./lib/InstantSearch'),
33
widgets: {
44
hits: require('./widgets/hits'),
5+
menu: require('./widgets/menu'),
56
multipleChoiceList: require('./widgets/multiple-choice-list'),
67
pagination: require('./widgets/pagination'),
78
searchBox: require('./widgets/search-box'),

widgets/menu.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
var React = require('react');
2+
var cx = require('classnames');
3+
4+
var utils = require('../lib/widget-utils.js');
5+
6+
var defaultTemplate = `<a href="{{href}}">{{name}}</a> {{count}}`;
7+
8+
var hierarchicalCounter = 0;
9+
10+
/**
11+
* Instantiate a list of refinements based on a facet
12+
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
13+
* @param {String} options.facetName Name of the attribute for faceting
14+
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
15+
* @param {String} [options.limit=100] How much facet values to get.
16+
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
17+
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
18+
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
19+
* @return {Object}
20+
*/
21+
function menu({
22+
container = null,
23+
facetName = null,
24+
sortBy = ['count:desc'],
25+
limit = 100,
26+
rootClass = null,
27+
itemClass = null,
28+
template = defaultTemplate
29+
}) {
30+
hierarchicalCounter++;
31+
32+
var MultipleChoiceList = require('../components/MultipleChoiceList');
33+
34+
var containerNode = utils.getContainerNode(container);
35+
var usage = 'Usage: menu({container, facetName, [sortBy, limit, rootClass, itemClass, template]})';
36+
37+
if (container === null || facetName === null) {
38+
throw new Error(usage);
39+
}
40+
41+
var hierarchicalFacetName = 'instantsearch.js' + hierarchicalCounter;
42+
43+
return {
44+
getConfiguration: () => ({
45+
hierarchicalFacets: [{
46+
name: hierarchicalFacetName,
47+
attributes: [facetName]
48+
}]
49+
}),
50+
render: function(results, state, helper) {
51+
React.render(
52+
<MultipleChoiceList
53+
rootClass={cx(rootClass)}
54+
itemClass={cx(itemClass)}
55+
facetValues={getFacetValues(results, hierarchicalFacetName, sortBy, limit)}
56+
template={template}
57+
toggleRefine={toggleRefine.bind(null, helper, hierarchicalFacetName)}
58+
/>,
59+
containerNode
60+
);
61+
}
62+
};
63+
}
64+
65+
function toggleRefine(helper, facetName, facetValue) {
66+
helper
67+
.toggleRefine(facetName, facetValue)
68+
.search();
69+
}
70+
71+
function getFacetValues(results, hierarchicalFacetName, sortBy, limit) {
72+
return results
73+
.getFacetValues(hierarchicalFacetName, {sortBy: sortBy})
74+
.data.slice(0, limit);
75+
}
76+
77+
module.exports = menu;

widgets/multiple-choice-list.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ var defaultTemplate = `<label>
88
</label>`;
99

1010
/**
11-
* Instantiate a list of refinement based on a facet
11+
* Instantiate a list of refinements based on a facet
1212
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
1313
* @param {String} options.facetName Name of the attribute for faceting
1414
* @param {String} options.operator How to apply refinements. Possible values: `or`, `and`
1515
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
1616
* @param {String} [options.limit=100] How much facet values to get.
17-
* @param {String|String[]} [options.cssClass=null] CSS class(es) for the main `<ul>` wrapper.
17+
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
18+
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
1819
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
20+
* @param {String|Function} [options.singleRefine=true] Are multiple refinements allowed or only one at the same time. You can use this
21+
* to build radio based refinement lists for example.
1922
* @return {Object}
2023
*/
2124
function multipleChoiceList({
@@ -24,17 +27,22 @@ function multipleChoiceList({
2427
operator = null,
2528
sortBy = ['count:desc'],
2629
limit = 100,
27-
cssClass = null,
28-
template = defaultTemplate
30+
rootClass = null,
31+
itemClass = null,
32+
template = defaultTemplate,
33+
singleRefine = false
2934
}) {
3035
var MultipleChoiceList = require('../components/MultipleChoiceList');
3136

3237
var containerNode = utils.getContainerNode(container);
33-
var usage = 'Usage: multipleChoiceList({container, facetName, operator[sortBy, limit, cssClass, template]})';
38+
var usage = 'Usage: multipleChoiceList({container, facetName, operator[sortBy, limit, rootClass, itemClass, template]})';
3439

3540
if (container === null ||
3641
facetName === null ||
37-
operator === null) {
42+
// operator is mandatory when multiple refines allowed
43+
(operator === null && singleRefine === false) ||
44+
// operator is not allowed when single refine enabled
45+
(operator !== null && singleRefine === true)) {
3846
throw new Error(usage);
3947
}
4048

@@ -50,16 +58,26 @@ function multipleChoiceList({
5058
render: function(results, state, helper) {
5159
React.render(
5260
<MultipleChoiceList
53-
cssClass={cx(cssClass)}
61+
rootClass={cx(rootClass)}
62+
itemClass={cx(itemClass)}
5463
facetValues={results.getFacetValues(facetName, {sortBy: sortBy}).slice(0, limit)}
55-
search={helper.search.bind(helper)}
5664
template={template}
57-
toggleRefine={helper.toggleRefine.bind(helper, facetName)}
65+
toggleRefine={toggleRefine.bind(null, helper, singleRefine, facetName)}
5866
/>,
5967
containerNode
6068
);
6169
}
6270
};
6371
}
6472

73+
function toggleRefine(helper, singleRefine, facetName, facetValue) {
74+
if (singleRefine) {
75+
helper.clearRefinement(facetName);
76+
}
77+
78+
helper
79+
.toggleRefine(facetName, facetValue)
80+
.search();
81+
}
82+
6583
module.exports = multipleChoiceList;

0 commit comments

Comments
 (0)