Skip to content

Commit 9996b4d

Browse files
author
Alexandre Stanislawski
committed
feat(connector): star rating connector
1 parent b9847cf commit 9996b4d

File tree

5 files changed

+215
-165
lines changed

5 files changed

+215
-165
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
import defaultLabels from './defaultLabels.js';
9+
10+
const bem = bemHelper('ais-star-rating');
11+
12+
/**
13+
* Instantiate a list of refinements based on a rating attribute
14+
* The ratings must be integer values. You can still keep the precise float value in another attribute
15+
* to be used in the custom ranking configuration. So that the actual hits ranking is precise.
16+
* @function starRating
17+
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
18+
* @param {string} options.attributeName Name of the attribute for filtering
19+
* @param {number} [options.max] The maximum rating value
20+
* @param {Object} [options.labels] Labels used by the default template
21+
* @param {string} [options.labels.andUp] The label suffixed after each line
22+
* @param {Object} [options.templates] Templates to use for the widget
23+
* @param {string|Function} [options.templates.header] Header template
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
26+
* @param {Function} [options.transformData.item] Function to change the object passed to the `item` template
27+
* @param {boolean} [options.autoHideContainer=true] Hide the container when no results match
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.link] CSS class to add to each link element
36+
* @param {string|string[]} [options.cssClasses.disabledLink] CSS class to add to each disabled link (when using the default template)
37+
* @param {string|string[]} [options.cssClasses.star] CSS class to add to each star element (when using the default template)
38+
* @param {string|string[]} [options.cssClasses.emptyStar] CSS class to add to each empty star element (when using the default template)
39+
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
40+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
41+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
42+
* @return {Object}
43+
*/
44+
const usage = `Usage:
45+
starRating({
46+
container,
47+
attributeName,
48+
[ max=5 ],
49+
[ cssClasses.{root,header,body,footer,list,item,active,link,disabledLink,star,emptyStar,count} ],
50+
[ templates.{header,item,footer} ],
51+
[ transformData.{item} ],
52+
[ labels.{andUp} ],
53+
[ autoHideContainer=true ],
54+
[ collapsible=false ]
55+
})`;
56+
const connectStarRating = starRatingRendering => ({
57+
container,
58+
attributeName,
59+
max = 5,
60+
cssClasses: userCssClasses = {},
61+
labels = defaultLabels,
62+
templates = defaultTemplates,
63+
collapsible = false,
64+
transformData,
65+
autoHideContainer = true,
66+
}) => {
67+
const containerNode = getContainerNode(container);
68+
69+
if (!container || !attributeName) {
70+
throw new Error(usage);
71+
}
72+
73+
const cssClasses = {
74+
root: cx(bem(null), userCssClasses.root),
75+
header: cx(bem('header'), userCssClasses.header),
76+
body: cx(bem('body'), userCssClasses.body),
77+
footer: cx(bem('footer'), userCssClasses.footer),
78+
list: cx(bem('list'), userCssClasses.list),
79+
item: cx(bem('item'), userCssClasses.item),
80+
link: cx(bem('link'), userCssClasses.link),
81+
disabledLink: cx(bem('link', 'disabled'), userCssClasses.disabledLink),
82+
count: cx(bem('count'), userCssClasses.count),
83+
star: cx(bem('star'), userCssClasses.star),
84+
emptyStar: cx(bem('star', 'empty'), userCssClasses.emptyStar),
85+
active: cx(bem('item', 'active'), userCssClasses.active),
86+
};
87+
88+
return {
89+
getConfiguration: () => ({disjunctiveFacets: [attributeName]}),
90+
91+
init({templatesConfig, helper, createURL}) {
92+
this._templateProps = prepareTemplateProps({
93+
transformData,
94+
defaultTemplates,
95+
templatesConfig,
96+
templates,
97+
});
98+
this._toggleRefinement = this._toggleRefinement.bind(this, helper);
99+
this._createURL = state => facetValue => createURL(state.toggleRefinement(attributeName, facetValue));
100+
101+
starRatingRendering({
102+
collapsible,
103+
createURL: this._createURL(helper.state),
104+
cssClasses,
105+
facetValues: [],
106+
shouldAutoHideContainer: autoHideContainer,
107+
templateProps: this._templateProps,
108+
toggleRefinement: this._toggleRefinement,
109+
containerNode,
110+
}, true);
111+
},
112+
113+
render({helper, results, state}) {
114+
const facetValues = [];
115+
const allValues = {};
116+
for (let v = max - 1; v >= 0; --v) {
117+
allValues[v] = 0;
118+
}
119+
results.getFacetValues(attributeName).forEach(facet => {
120+
const val = Math.round(facet.name);
121+
if (!val || val > max - 1) {
122+
return;
123+
}
124+
for (let v = val; v >= 1; --v) {
125+
allValues[v] += facet.count;
126+
}
127+
});
128+
const refinedStar = this._getRefinedStar(helper);
129+
for (let star = max - 1; star >= 1; --star) {
130+
const count = allValues[star];
131+
if (refinedStar && star !== refinedStar && count === 0) {
132+
// skip count==0 when at least 1 refinement is enabled
133+
// eslint-disable-next-line no-continue
134+
continue;
135+
}
136+
const stars = [];
137+
for (let i = 1; i <= max; ++i) {
138+
stars.push(i <= star);
139+
}
140+
facetValues.push({
141+
stars,
142+
name: String(star),
143+
count,
144+
isRefined: refinedStar === star,
145+
labels,
146+
});
147+
}
148+
149+
starRatingRendering({
150+
collapsible,
151+
createURL: this._createURL(state),
152+
cssClasses,
153+
facetValues,
154+
shouldAutoHideContainer: autoHideContainer && results.nbHits === 0,
155+
templateProps: this._templateProps,
156+
toggleRefinement: this._toggleRefinement,
157+
containerNode,
158+
}, false);
159+
},
160+
161+
_toggleRefinement(helper, facetValue) {
162+
const isRefined = this._getRefinedStar(helper) === Number(facetValue);
163+
helper.clearRefinements(attributeName);
164+
if (!isRefined) {
165+
for (let val = Number(facetValue); val <= max; ++val) {
166+
helper.addDisjunctiveFacetRefinement(attributeName, val);
167+
}
168+
}
169+
helper.search();
170+
},
171+
172+
_getRefinedStar(helper) {
173+
let refinedStar = undefined;
174+
const refinements = helper.getRefinements(attributeName);
175+
refinements.forEach(r => {
176+
if (!refinedStar || Number(r.value) < refinedStar) {
177+
refinedStar = Number(r.value);
178+
}
179+
});
180+
return refinedStar;
181+
},
182+
};
183+
};
184+
185+
export default connectStarRating;
File renamed without changes.
File renamed without changes.

src/widgets/star-rating/__tests__/star-rating-test.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import sinon from 'sinon';
77
import expectJSX from 'expect-jsx';
88
expect.extend(expectJSX);
99

10-
import defaultTemplates from '../defaultTemplates.js';
11-
import defaultLabels from '../defaultLabels.js';
10+
import defaultTemplates from '../../../connectors/star-rating/defaultTemplates.js';
11+
import defaultLabels from '../../../connectors/star-rating/defaultLabels.js';
1212
import starRating from '../star-rating.js';
1313
import RefinementList from '../../../components/RefinementList/RefinementList.js';
1414

@@ -20,17 +20,11 @@ describe('starRating()', () => {
2020
let state;
2121
let createURL;
2222

23-
let autoHideContainer;
24-
let headerFooter;
2523
let results;
2624

2725
beforeEach(() => {
2826
ReactDOM = {render: sinon.spy()};
2927
starRating.__Rewire__('ReactDOM', ReactDOM);
30-
autoHideContainer = sinon.stub().returns(RefinementList);
31-
starRating.__Rewire__('autoHideContainerHOC', autoHideContainer);
32-
headerFooter = sinon.stub().returns(RefinementList);
33-
starRating.__Rewire__('headerFooterHOC', headerFooter);
3428

3529
container = document.createElement('div');
3630
widget = starRating({container, attributeName: 'anAttrName', cssClasses: {body: ['body', 'cx']}});
@@ -97,9 +91,7 @@ describe('starRating()', () => {
9791
},
9892
};
9993

100-
expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
101-
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
102-
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
94+
expect(ReactDOM.render.callCount).toBe(2);
10395
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<RefinementList {...props} />);
10496
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
10597
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<RefinementList {...props} />);
@@ -110,7 +102,7 @@ describe('starRating()', () => {
110102
helper.getRefinements = sinon.stub().returns([{value: '1'}]);
111103
results.getFacetValues = sinon.stub().returns([{name: '1', count: 42}]);
112104
widget.render({state, helper, results, createURL});
113-
expect(ReactDOM.render.calledOnce).toBe(true, 'ReactDOM.render called once');
105+
expect(ReactDOM.render.callCount).toBe(1);
114106
expect(ReactDOM.render.firstCall.args[0].props.facetValues).toEqual([
115107
{
116108
count: 42,

0 commit comments

Comments
 (0)