Skip to content

Commit f727949

Browse files
author
Alexandre Stanislawski
committed
feat(connector): Add hierarchical menu connector
1 parent 02f7d3e commit f727949

File tree

6 files changed

+212
-167
lines changed

6 files changed

+212
-167
lines changed

src/components/RefinementList/RefinementList.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import isEqual from 'lodash/isEqual';
88

99
import SearchBox from '../SearchBox';
1010

11-
class RefinementList extends React.Component {
11+
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
12+
import headerFooterHOC from '../../decorators/headerFooter.js';
13+
14+
export class RawRefinementList extends React.Component {
1215
constructor(props) {
1316
super(props);
1417
this.state = {
@@ -33,7 +36,7 @@ class RefinementList extends React.Component {
3336
let subItems;
3437
const hasChildren = facetValue.data && facetValue.data.length > 0;
3538
if (hasChildren) {
36-
subItems = <RefinementList
39+
subItems = <RawRefinementList
3740
{...this.props}
3841
depth={this.props.depth + 1}
3942
facetValues={facetValue.data}
@@ -185,7 +188,7 @@ class RefinementList extends React.Component {
185188
}
186189
}
187190

188-
RefinementList.propTypes = {
191+
RawRefinementList.propTypes = {
189192
Template: React.PropTypes.func,
190193
attributeNameKey: React.PropTypes.string,
191194
createURL: React.PropTypes.func,
@@ -207,10 +210,10 @@ RefinementList.propTypes = {
207210
isFromSearch: React.PropTypes.bool,
208211
};
209212

210-
RefinementList.defaultProps = {
213+
RawRefinementList.defaultProps = {
211214
cssClasses: {},
212215
depth: 0,
213216
attributeNameKey: 'name',
214217
};
215218

216-
export default RefinementList;
219+
export default autoHideContainerHOC(headerFooterHOC(RawRefinementList));

src/components/RefinementList/__tests__/RefinementList-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {shallow} from 'enzyme';
55
import expect from 'expect';
66
import sinon from 'sinon';
77

8-
import RefinementList from '../RefinementList';
8+
import {RawRefinementList as RefinementList} from '../RefinementList';
99
import RefinementListItem from '../RefinementListItem';
1010

1111
import expectJSX from 'expect-jsx';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import {
2+
bemHelper,
3+
prepareTemplateProps,
4+
getContainerNode,
5+
} from '../../lib/utils.js';
6+
import cx from 'classnames';
7+
import defaultTemplates from './defaultTemplates.js';
8+
9+
const bem = bemHelper('ais-hierarchical-menu');
10+
/**
11+
* Create a hierarchical menu using multiple attributes
12+
* @function hierarchicalMenu
13+
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
14+
* @param {string[]} options.attributes Array of attributes to use to generate the hierarchy of the menu.
15+
* See the example for the convention to follow.
16+
* @param {number} [options.limit=10] How much facet values to get [*]
17+
* @param {string} [options.separator=">"] Separator used in the attributes to separate level values. [*]
18+
* @param {string} [options.rootPath] Prefix path to use if the first level is not the root level.
19+
* @param {string} [options.showParentLevel=false] Show the parent level of the current refined value
20+
* @param {string[]|Function} [options.sortBy=['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
21+
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
22+
* @param {Object} [options.templates] Templates to use for the widget
23+
* @param {string|Function} [options.templates.header=''] Header template (root level only)
24+
* @param {string|Function} [options.templates.item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties
25+
* @param {string|Function} [options.templates.footer=''] Footer template (root level only)
26+
* @param {Function} [options.transformData.item] Method to change the object passed to the `item` template
27+
* @param {boolean} [options.autoHideContainer=true] Hide the container when there are no items in the menu
28+
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements
29+
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
30+
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
31+
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
32+
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
33+
* @param {string|string[]} [options.cssClasses.list] CSS class to add to the list element
34+
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each item element
35+
* @param {string|string[]} [options.cssClasses.depth] CSS class to add to each item element to denote its depth. The actual level will be appended to the given class name (ie. if `depth` is given, the widget will add `depth0`, `depth1`, ... according to the level of each item).
36+
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
37+
* @param {string|string[]} [options.cssClasses.link] CSS class to add to each link (when using the default template)
38+
* @param {string|string[]} [options.cssClasses.count] CSS class to add to each count element (when using the default template)
39+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
40+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
41+
* @return {Object}
42+
*/
43+
const usage = `Usage:
44+
hierarchicalMenu({
45+
container,
46+
attributes,
47+
[ separator=' > ' ],
48+
[ rootPath ],
49+
[ showParentLevel=true ],
50+
[ limit=10 ],
51+
[ sortBy=['name:asc'] ],
52+
[ cssClasses.{root , header, body, footer, list, depth, item, active, link}={} ],
53+
[ templates.{header, item, footer} ],
54+
[ transformData.{item} ],
55+
[ autoHideContainer=true ],
56+
[ collapsible=false ]
57+
})`;
58+
const connectHierarchicalMenu = renderHierarchicalMenu => ({
59+
container,
60+
attributes,
61+
separator = ' > ',
62+
rootPath = null,
63+
showParentLevel = true,
64+
limit = 10,
65+
sortBy = ['name:asc'],
66+
cssClasses: userCssClasses = {},
67+
autoHideContainer = true,
68+
templates = defaultTemplates,
69+
collapsible = false,
70+
transformData,
71+
} = {}) => {
72+
if (!container || !attributes || !attributes.length) {
73+
throw new Error(usage);
74+
}
75+
76+
const containerNode = getContainerNode(container);
77+
78+
// we need to provide a hierarchicalFacet name for the search state
79+
// so that we can always map $hierarchicalFacetName => real attributes
80+
// we use the first attribute name
81+
const hierarchicalFacetName = attributes[0];
82+
83+
const cssClasses = {
84+
root: cx(bem(null), userCssClasses.root),
85+
header: cx(bem('header'), userCssClasses.header),
86+
body: cx(bem('body'), userCssClasses.body),
87+
footer: cx(bem('footer'), userCssClasses.footer),
88+
list: cx(bem('list'), userCssClasses.list),
89+
depth: bem('list', 'lvl'),
90+
item: cx(bem('item'), userCssClasses.item),
91+
active: cx(bem('item', 'active'), userCssClasses.active),
92+
link: cx(bem('link'), userCssClasses.link),
93+
count: cx(bem('count'), userCssClasses.count),
94+
};
95+
96+
return {
97+
getConfiguration: currentConfiguration => ({
98+
hierarchicalFacets: [{
99+
name: hierarchicalFacetName,
100+
attributes,
101+
separator,
102+
rootPath,
103+
showParentLevel,
104+
}],
105+
maxValuesPerFacet: currentConfiguration.maxValuesPerFacet !== undefined ?
106+
Math.max(currentConfiguration.maxValuesPerFacet, limit) :
107+
limit,
108+
}),
109+
init({helper, templatesConfig, createURL}) {
110+
this._toggleRefinement = facetValue => helper
111+
.toggleRefinement(hierarchicalFacetName, facetValue)
112+
.search();
113+
114+
this._templateProps = prepareTemplateProps({
115+
transformData,
116+
defaultTemplates,
117+
templatesConfig,
118+
templates,
119+
});
120+
121+
// Bind createURL to this specific attribute
122+
function _createURL(facetValue) {
123+
return createURL(helper.state.toggleRefinement(hierarchicalFacetName, facetValue));
124+
}
125+
126+
renderHierarchicalMenu({
127+
attributeNameKey: 'path',
128+
collapsible,
129+
createURL: _createURL,
130+
cssClasses,
131+
facetValues: undefined,
132+
shouldAutoHideContainer: autoHideContainer,
133+
templateProps: this._templateProps,
134+
toggleRefinement: this._toggleRefinement,
135+
containerNode,
136+
}, true);
137+
},
138+
_prepareFacetValues(facetValues, state) {
139+
return facetValues
140+
.slice(0, limit)
141+
.map(subValue => {
142+
if (Array.isArray(subValue.data)) {
143+
subValue.data = this._prepareFacetValues(subValue.data, state);
144+
}
145+
146+
return subValue;
147+
});
148+
},
149+
render({results, state, createURL}) {
150+
let facetValues = results.getFacetValues(hierarchicalFacetName, {sortBy}).data || [];
151+
facetValues = this._prepareFacetValues(facetValues, state);
152+
153+
// Bind createURL to this specific attribute
154+
function _createURL(facetValue) {
155+
return createURL(state.toggleRefinement(hierarchicalFacetName, facetValue));
156+
}
157+
158+
renderHierarchicalMenu({
159+
attributeNameKey: 'path',
160+
collapsible,
161+
createURL: _createURL,
162+
cssClasses,
163+
facetValues,
164+
shouldAutoHideContainer: autoHideContainer && facetValues.length === 0,
165+
templateProps: this._templateProps,
166+
toggleRefinement: this._toggleRefinement,
167+
containerNode,
168+
}, false);
169+
},
170+
};
171+
};
172+
173+
export default connectHierarchicalMenu;
File renamed without changes.

src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import expectJSX from 'expect-jsx';
77
expect.extend(expectJSX);
88
import hierarchicalMenu from '../hierarchical-menu';
99
import RefinementList from '../../../components/RefinementList/RefinementList';
10+
import defaultTemplates from '../../../connectors/hierarchical-menu/defaultTemplates.js';
1011

1112
describe('hierarchicalMenu()', () => {
12-
let autoHideContainer;
1313
let container;
1414
let attributes;
15-
let headerFooter;
1615
let options;
1716
let widget;
1817
let ReactDOM;
@@ -23,10 +22,6 @@ describe('hierarchicalMenu()', () => {
2322
options = {};
2423
ReactDOM = {render: sinon.spy()};
2524
hierarchicalMenu.__Rewire__('ReactDOM', ReactDOM);
26-
autoHideContainer = sinon.stub().returnsArg(0);
27-
hierarchicalMenu.__Rewire__('autoHideContainerHOC', autoHideContainer);
28-
headerFooter = sinon.stub().returnsArg(0);
29-
hierarchicalMenu.__Rewire__('headerFooterHOC', headerFooter);
3025
});
3126

3227
context('instantiated with wrong parameters', () => {
@@ -46,26 +41,6 @@ describe('hierarchicalMenu()', () => {
4641
});
4742
});
4843

49-
context('autoHideContainer', () => {
50-
beforeEach(() => { options = {container, attributes}; });
51-
52-
it('should be called if autoHideContainer set to true', () => {
53-
hierarchicalMenu({...options, autoHideContainer: true});
54-
expect(autoHideContainer.calledOnce).toBe(true);
55-
});
56-
57-
it('should not be called if autoHideContainer set to false', () => {
58-
hierarchicalMenu({container, attributes, autoHideContainer: false});
59-
expect(autoHideContainer.called).toBe(false);
60-
});
61-
});
62-
63-
it('uses headerFooter', () => {
64-
options = {container, attributes};
65-
hierarchicalMenu(options);
66-
expect(headerFooter.calledOnce).toBe(true);
67-
});
68-
6944
context('getConfiguration', () => {
7045
beforeEach(() => { options = {container, attributes}; });
7146

@@ -160,18 +135,12 @@ describe('hierarchicalMenu()', () => {
160135
let results;
161136
let data;
162137
let cssClasses;
163-
const defaultTemplates = {
164-
header: 'header',
165-
item: 'item',
166-
footer: 'footer',
167-
};
168138
let templateProps;
169139
let helper;
170140
let state;
171141
let createURL;
172142

173143
beforeEach(() => {
174-
hierarchicalMenu.__Rewire__('defaultTemplates', defaultTemplates);
175144
templateProps = {
176145
transformData: undefined,
177146
templatesConfig: undefined,

0 commit comments

Comments
 (0)