Skip to content

Commit 1facd9d

Browse files
author
vvo
committed
feat: hierarchicalWidget
Add hierarchicalWidget. + add limit prop to RefinementList (will then PR other refinement widget to use it) + add `Requirements` section to documentation. We should have it for every widget
1 parent 2f247ad commit 1facd9d

File tree

9 files changed

+209
-6
lines changed

9 files changed

+209
-6
lines changed

README.md

+57
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ npm run test:watch # developer mode, test only
191191
[hits]: ./widgets-screenshots/hits.png
192192
[toggle]: ./widgets-screenshots/toggle.png
193193
[refinementList]: ./widgets-screenshots/refinement-list.png
194+
[hierarchicalMenu]: ./widgets-screenshots/hierarchicalMenu.png
194195
[menu]: ./widgets-screenshots/menu.png
195196
[rangeSlider]: ./widgets-screenshots/range-slider.png
196197
[urlSync]: ./widgets-screenshots/url-sync.gif
@@ -662,6 +663,62 @@ search.addWidget(
662663
);
663664
```
664665

666+
### hierarchicalMenu
667+
668+
![Example of the hierarchicalMenu widget][hierarchicalMenu]
669+
670+
#### API
671+
672+
```js
673+
/**
674+
* Create a hierarchical menu using multiple attributes
675+
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
676+
* @param {String[]} options.attributes Array of attributes to use to generate the hierarchy of the menu.
677+
* You need to follow some conventions:
678+
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
679+
* @param {Number} [options.limit=100] How much facet values to get
680+
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements: root, list, item
681+
* @param {String|String[]} [options.cssClasses.root]
682+
* @param {String|String[]} [options.cssClasses.list]
683+
* @param {String|String[]} [options.cssClasses.item]
684+
* @param {Object} [options.templates] Templates to use for the widget
685+
* @param {String|Function} [options.templates.header=''] Header template (root level only)
686+
* @param {String|Function} [options.templates.item='<a href="{{href}}">{{name}}</a> {{count}}'] Item template, provided with `name`, `count`, `isRefined`, `path`
687+
* @param {String|Function} [options.templates.footer=''] Footer template (root level only)
688+
* @param {Function} [options.transformData] Method to change the object passed to the item template
689+
* @param {boolean} [hideWhenNoResults=true] Hide the container when there's no results
690+
* @return {Object}
691+
*/
692+
```
693+
694+
#### Algolia requirements
695+
696+
All the `attributes` should be added to `attributesForFaceting` in your index settings.
697+
698+
Your index's objects must be formatted in a way that is expected by the `hierarchicalMenu` widget:
699+
700+
```json
701+
{
702+
"objectID": "123",
703+
"name": "orange",
704+
"categories": {
705+
"lvl0": "fruits",
706+
"lvl1": "fruits > citrus"
707+
}
708+
}
709+
```
710+
711+
#### Usage
712+
713+
```js
714+
search.addWidget(
715+
instantsearch.widgets.hierarchicalMenu({
716+
container: '#products',
717+
attributes: ['categories.lvl0', 'categories.lvl1', 'categories.lvl2']
718+
})
719+
);
720+
```
721+
665722
## Browser support
666723

667724
We natively support IE10+ and all other modern browsers without any dependency need

components/RefinementList.js

+19-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ class RefinementList extends React.Component {
1919
// has a checkbox inside, we ignore the first click event because we will get another one.
2020
handleClick(value, e) {
2121
if (e.target.tagName === 'A' && e.target.href) {
22+
// do not trigger any url change by the href
2223
e.preventDefault();
24+
// do not bubble (so that hierarchical lists are not triggering refine twice)
25+
e.stopPropagation();
2326
}
2427

2528
if (e.target.tagName === 'INPUT') {
@@ -43,14 +46,21 @@ class RefinementList extends React.Component {
4346
render() {
4447
return (
4548
<div className={cx(this.props.cssClasses.list)}>
46-
{this.props.facetValues.map(facetValue => {
49+
{this.props.facetValues.slice(0, this.props.limit).map(facetValue => {
50+
var hasChildren = facetValue.data && facetValue.data.length > 0;
51+
52+
var subList = hasChildren ?
53+
<RefinementList {...this.props} facetValues={facetValue.data} /> :
54+
null;
55+
4756
return (
4857
<div
4958
className={cx(this.props.cssClasses.item)}
50-
key={facetValue.name}
51-
onClick={this.handleClick.bind(this, facetValue.name)}
59+
key={facetValue[this.props.facetNameKey]}
60+
onClick={this.handleClick.bind(this, facetValue[this.props.facetNameKey])}
5261
>
5362
<this.props.Template data={facetValue} templateKey="item" />
63+
{subList}
5464
</div>
5565
);
5666
})}
@@ -70,16 +80,20 @@ RefinementList.propTypes = {
7080
React.PropTypes.arrayOf(React.PropTypes.string)
7181
])
7282
}),
83+
limit: React.PropTypes.number,
7384
facetValues: React.PropTypes.array,
7485
Template: React.PropTypes.func,
75-
toggleRefinement: React.PropTypes.func.isRequired
86+
toggleRefinement: React.PropTypes.func.isRequired,
87+
facetNameKey: React.PropTypes.string
7688
};
7789

7890
RefinementList.defaultProps = {
7991
cssClasses: {
8092
item: null,
8193
list: null
82-
}
94+
},
95+
limit: 1000,
96+
facetNameKey: 'name'
8397
};
8498

8599
module.exports = RefinementList;

example/app.js

+15
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,19 @@ search.addWidget(
139139
})
140140
);
141141

142+
search.addWidget(
143+
instantsearch.widgets.hierarchicalMenu({
144+
container: '#hierarchical-categories',
145+
attributes: ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1', 'hierarchicalCategories.lvl2'],
146+
cssClasses: {
147+
root: 'list-group',
148+
list: 'hierarchical-categories-list'
149+
},
150+
templates: {
151+
header: '<div class="panel-heading">Hierarchical categories</div>',
152+
item: require('./templates/category.html')
153+
}
154+
})
155+
);
156+
142157
search.start();

example/index.html

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
3535

3636
<div class="panel panel-default" id="price"></div>
3737

38+
<div class="panel panel-default" id="hierarchical-categories"></div>
39+
3840
</div>
3941
<div class="col-md-9">
4042
<div class="row">

example/style.css

+4
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ body {
4444
background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);
4545
border-color: #ddd;
4646
}
47+
48+
.hierarchical-categories-list .hierarchical-categories-list {
49+
padding-left: 20px;
50+
}

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var InstantSearch = require('./lib/InstantSearch');
44
var instantsearch = toFactory(InstantSearch);
55

66
instantsearch.widgets = {
7+
hierarchicalMenu: require('./widgets/hierarchicalMenu'),
78
hits: require('./widgets/hits'),
89
indexSelector: require('./widgets/index-selector'),
910
menu: require('./widgets/menu'),
29.4 KB
Loading

widgets/hierarchicalMenu.js

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
var React = require('react');
2+
3+
var utils = require('../lib/utils.js');
4+
var autoHide = require('../decorators/autoHide');
5+
var bindProps = require('../decorators/bindProps');
6+
var headerFooter = require('../decorators/headerFooter');
7+
var RefinementList = autoHide(headerFooter(require('../components/RefinementList')));
8+
var Template = require('../components/Template');
9+
10+
var hierarchicalCounter = 0;
11+
var defaultTemplates = {
12+
header: '',
13+
item: '<a href="{{href}}">{{name}}</a> {{count}}',
14+
footer: ''
15+
};
16+
17+
/**
18+
* Create a hierarchical menu using multiple attributes
19+
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
20+
* @param {String[]} options.attributes Array of attributes to use to generate the hierarchy of the menu.
21+
* You need to follow some conventions:
22+
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
23+
* @param {Number} [options.limit=100] How much facet values to get
24+
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements: root, list, item
25+
* @param {String|String[]} [options.cssClasses.root]
26+
* @param {String|String[]} [options.cssClasses.list]
27+
* @param {String|String[]} [options.cssClasses.item]
28+
* @param {Object} [options.templates] Templates to use for the widget
29+
* @param {String|Function} [options.templates.header=''] Header template (root level only)
30+
* @param {String|Function} [options.templates.item='<a href="{{href}}">{{name}}</a> {{count}}'] Item template, provided with `name`, `count`, `isRefined`, `path`
31+
* @param {String|Function} [options.templates.footer=''] Footer template (root level only)
32+
* @param {Function} [options.transformData] Method to change the object passed to the item template
33+
* @param {boolean} [hideWhenNoResults=true] Hide the container when there's no results
34+
* @return {Object}
35+
*/
36+
function hierarchicalMenu({
37+
container = null,
38+
attributes = [],
39+
separator,
40+
limit = 100,
41+
sortBy = ['name:asc'],
42+
cssClasses = {
43+
root: null,
44+
list: null,
45+
item: null
46+
},
47+
hideWhenNoResults = true,
48+
templates = defaultTemplates,
49+
transformData
50+
}) {
51+
hierarchicalCounter++;
52+
53+
var containerNode = utils.getContainerNode(container);
54+
var usage = 'Usage: hierarchicalMenu({container, attributes, [separator, sortBy, limit, cssClasses.{root, list, item}, templates.{header, item, footer}, transformData]})';
55+
56+
if (!container || !attributes || !attributes.length) {
57+
throw new Error(usage);
58+
}
59+
60+
var hierarchicalFacetName = 'instantsearch.js-hierarchicalMenu' + hierarchicalCounter;
61+
62+
return {
63+
getConfiguration: () => ({
64+
hierarchicalFacets: [{
65+
name: hierarchicalFacetName,
66+
attributes,
67+
separator
68+
}]
69+
}),
70+
render: function({results, helper, templatesConfig}) {
71+
var facetValues = getFacetValues(results, hierarchicalFacetName, sortBy);
72+
73+
var templateProps = utils.prepareTemplateProps({
74+
transformData,
75+
defaultTemplates,
76+
templatesConfig,
77+
templates
78+
});
79+
80+
React.render(
81+
<RefinementList
82+
cssClasses={cssClasses}
83+
facetValues={facetValues}
84+
limit={limit}
85+
Template={bindProps(Template, templateProps)}
86+
hideWhenNoResults={hideWhenNoResults}
87+
hasResults={facetValues.length > 0}
88+
facetNameKey="path"
89+
toggleRefinement={toggleRefinement.bind(null, helper, hierarchicalFacetName)}
90+
/>,
91+
containerNode
92+
);
93+
}
94+
};
95+
}
96+
97+
function toggleRefinement(helper, facetName, facetValue) {
98+
helper
99+
.toggleRefinement(facetName, facetValue)
100+
.search();
101+
}
102+
103+
function getFacetValues(results, hierarchicalFacetName, sortBy) {
104+
var values = results
105+
.getFacetValues(hierarchicalFacetName, {sortBy: sortBy});
106+
107+
return values.data || [];
108+
}
109+
110+
module.exports = hierarchicalMenu;

widgets/menu.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function menu({
5555
throw new Error(usage);
5656
}
5757

58-
var hierarchicalFacetName = 'instantsearch.js' + hierarchicalCounter;
58+
var hierarchicalFacetName = 'instantsearch.js-menu' + hierarchicalCounter;
5959

6060
return {
6161
getConfiguration: () => ({

0 commit comments

Comments
 (0)