Skip to content

Commit 9b6c2bf

Browse files
committed
feat(toggle): Allow custom on/off values
Fixes #409 Adds the `values` option to the `toggle` widget. Defaults to `{on: true, off: undefined}`. - It does not break previous API, new default value is retro-compatible - If `off` set to `undefined`, it will simply remove all filtering on this facet - If `off` set to anything else, it will allow toggling between `on` and `off` value when checking/unchecking the checkbox - If an `off` value is set and the results are not currently filtered on the `on` value at startup, then we add filtering on `off`. This helps in not creating an undefined state on first load. To test if a refinement was currently set on a specific value, I had to use `helper.state.isFacetRefined`, I did not found any method directly on the helper. Maybe I missed something.
1 parent 3f8eb9e commit 9b6c2bf

File tree

4 files changed

+175
-16
lines changed

4 files changed

+175
-16
lines changed

docs/_includes/widget-jsdoc/toggle.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
| <span class='attr-required'>`options.container`</span> | CSS Selector or DOMElement to insert the widget |
44
| <span class='attr-required'>`options.facetName`</span> | Name of the attribute for faceting (eg. "free_shipping") |
55
| <span class='attr-required'>`options.label`</span> | Human-readable name of the filter (eg. "Free Shipping") |
6+
| <span class='attr-optional'>`options.values`</span> | Lets you define the values to filter on when toggling |
7+
| <span class='attr-optional'>`options.values.on`</span> | Value to filter on when checked |
8+
| <span class='attr-optional'>`options.values.off`</span> | Value to filter on when unchecked |
69
| <span class='attr-optional'>`options.cssClasses`</span> | CSS classes to add |
710
| <span class='attr-optional'>`options.cssClasses.root`</span> | CSS class to add to the root element |
811
| <span class='attr-optional'>`options.cssClasses.header`</span> | CSS class to add to the header element |

docs/documentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@ search.addWidget(
589589
container: '#free-shipping',
590590
facetName: 'free_shipping',
591591
label: 'Free Shipping',
592+
values: {
593+
on: true,
594+
off: false
595+
},
592596
templates: {
593597
header: 'Shipping'
594598
},

widgets/toggle/__tests__/toggle-test.js

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ describe('toggle()', () => {
8383
transformData: undefined
8484
};
8585
helper = {
86-
hasRefinements: sinon.stub().returns(false),
86+
state: {
87+
isFacetRefined: sinon.stub().returns(false)
88+
},
8789
removeFacetRefinement: sinon.spy(),
8890
addFacetRefinement: sinon.spy(),
8991
search: sinon.spy()
@@ -163,7 +165,9 @@ describe('toggle()', () => {
163165

164166
it('when refined', () => {
165167
helper = {
166-
hasRefinements: sinon.stub().returns(true)
168+
state: {
169+
isFacetRefined: sinon.stub().returns(true)
170+
}
167171
};
168172
results = {
169173
hits: [{Hello: ', world!'}],
@@ -198,12 +202,128 @@ describe('toggle()', () => {
198202
expect(helper.addFacetRefinement.calledOnce).toBe(true);
199203
expect(helper.addFacetRefinement.calledWithExactly(facetName, true));
200204
helper.hasRefinements = sinon.stub().returns(true);
201-
ReactDOM.render.reset();
202-
widget.render({results, helper});
203-
toggleRefinement = ReactDOM.render.firstCall.args[0].props.toggleRefinement;
204-
toggleRefinement();
205-
expect(helper.removeFacetRefinement.calledOnce).toBe(true);
206-
expect(helper.removeFacetRefinement.calledWithExactly(facetName, true));
205+
});
206+
});
207+
208+
context('toggleRefinement', () => {
209+
let helper;
210+
let values;
211+
212+
function toggleOn() {
213+
widget.toggleRefinement(helper, false);
214+
}
215+
function toggleOff() {
216+
widget.toggleRefinement(helper, true);
217+
}
218+
219+
beforeEach(() => {
220+
helper = {
221+
removeFacetRefinement: sinon.spy(),
222+
addFacetRefinement: sinon.spy(),
223+
search: sinon.spy()
224+
};
225+
});
226+
227+
context('default values', () => {
228+
it('toggle on should add filter to true', () => {
229+
// Given
230+
widget = toggle({container, facetName, label});
231+
232+
// When
233+
toggleOn();
234+
235+
// Then
236+
expect(helper.addFacetRefinement.calledWith(facetName, true)).toBe(true);
237+
expect(helper.removeFacetRefinement.called).toBe(false);
238+
});
239+
it('toggle off should remove all filters', () => {
240+
// Given
241+
widget = toggle({container, facetName, label});
242+
243+
// When
244+
toggleOff();
245+
246+
// Then
247+
expect(helper.removeFacetRefinement.calledWith(facetName, true)).toBe(true);
248+
expect(helper.addFacetRefinement.called).toBe(false);
249+
});
250+
});
251+
context('specific values', () => {
252+
it('toggle on should change the refined value', () => {
253+
// Given
254+
values = {on: 'on', off: 'off'};
255+
widget = toggle({container, facetName, label, values});
256+
257+
// When
258+
toggleOn();
259+
260+
// Then
261+
expect(helper.removeFacetRefinement.calledWith(facetName, 'off')).toBe(true);
262+
expect(helper.addFacetRefinement.calledWith(facetName, 'on')).toBe(true);
263+
});
264+
it('toggle off should change the refined value', () => {
265+
// Given
266+
values = {on: 'on', off: 'off'};
267+
widget = toggle({container, facetName, label, values});
268+
269+
// When
270+
toggleOff();
271+
272+
// Then
273+
expect(helper.removeFacetRefinement.calledWith(facetName, 'on')).toBe(true);
274+
expect(helper.addFacetRefinement.calledWith(facetName, 'off')).toBe(true);
275+
});
276+
});
277+
});
278+
279+
context('custom off value', () => {
280+
it('should add a refinement for custom off value on init', () => {
281+
// Given
282+
let values = {on: 'on', off: 'off'};
283+
widget = toggle({container, facetName, label, values});
284+
let state = {
285+
isFacetRefined: sinon.stub().returns(false)
286+
};
287+
let helper = {
288+
addFacetRefinement: sinon.spy()
289+
};
290+
291+
// When
292+
widget.init(state, helper);
293+
294+
// Then
295+
expect(helper.addFacetRefinement.calledWith(facetName, 'off')).toBe(true);
296+
});
297+
it('should not add a refinement for custom off value on init if already checked', () => {
298+
// Given
299+
let values = {on: 'on', off: 'off'};
300+
widget = toggle({container, facetName, label, values});
301+
let state = {
302+
isFacetRefined: sinon.stub().returns(true)
303+
};
304+
let helper = {
305+
addFacetRefinement: sinon.spy()
306+
};
307+
308+
// When
309+
widget.init(state, helper);
310+
311+
// Then
312+
expect(helper.addFacetRefinement.called).toBe(false);
313+
});
314+
it('should not add a refinement for no custom off value on init', () => {
315+
// Given
316+
widget = toggle({container, facetName, label});
317+
let state = {};
318+
let helper = {
319+
addFacetRefinement: sinon.spy()
320+
};
321+
322+
// When
323+
widget.init(state, helper);
324+
325+
// Then
326+
expect(helper.addFacetRefinement.called).toBe(false);
207327
});
208328
});
209329

widgets/toggle/toggle.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ let defaultTemplates = require('./defaultTemplates');
1818
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
1919
* @param {string} options.facetName Name of the attribute for faceting (eg. "free_shipping")
2020
* @param {string} options.label Human-readable name of the filter (eg. "Free Shipping")
21+
* @param {Object} [options.values] Lets you define the values to filter on when toggling
22+
* @param {*} [options.values.on] Value to filter on when checked
23+
* @param {*} [options.values.off] Value to filter on when unchecked
2124
* @param {Object} [options.cssClasses] CSS classes to add
2225
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
2326
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
@@ -41,6 +44,7 @@ function toggle({
4144
container,
4245
facetName,
4346
label,
47+
values: userValues = {on: true, off: undefined},
4448
templates = defaultTemplates,
4549
cssClasses: userCssClasses = {},
4650
transformData,
@@ -58,12 +62,44 @@ function toggle({
5862
throw new Error(usage);
5963
}
6064

65+
let hasAnOffValue = (userValues.off !== undefined);
66+
6167
return {
6268
getConfiguration: () => ({
6369
facets: [facetName]
6470
}),
71+
init: (state, helper) => {
72+
if (userValues.off === undefined) {
73+
return;
74+
}
75+
// Add filtering on the 'off' value if set
76+
let isRefined = state.isFacetRefined(facetName, userValues.on);
77+
if (!isRefined) {
78+
helper.addFacetRefinement(facetName, userValues.off);
79+
}
80+
},
81+
toggleRefinement: (helper, isRefined) => {
82+
let on = userValues.on;
83+
let off = userValues.off;
84+
85+
// Checking
86+
if (!isRefined) {
87+
if (hasAnOffValue) {
88+
helper.removeFacetRefinement(facetName, off);
89+
}
90+
helper.addFacetRefinement(facetName, on);
91+
} else {
92+
// Unchecking
93+
helper.removeFacetRefinement(facetName, on);
94+
if (hasAnOffValue) {
95+
helper.addFacetRefinement(facetName, off);
96+
}
97+
}
98+
99+
helper.search();
100+
},
65101
render: function({helper, results, templatesConfig, state, createURL}) {
66-
let isRefined = helper.hasRefinements(facetName);
102+
let isRefined = helper.state.isFacetRefined(facetName, userValues.on);
67103
let values = find(results.getFacetValues(facetName), {name: isRefined.toString()});
68104
let hasNoResults = results.nbHits === 0;
69105

@@ -93,26 +129,22 @@ function toggle({
93129
count: cx(bem('count'), userCssClasses.count)
94130
};
95131

132+
let toggleRefinement = this.toggleRefinement.bind(this, helper, isRefined);
133+
96134
ReactDOM.render(
97135
<RefinementList
98136
createURL={() => createURL(state.toggleRefinement(facetName, facetValue.isRefined))}
99137
cssClasses={cssClasses}
100138
facetValues={[facetValue]}
101139
shouldAutoHideContainer={hasNoResults}
102140
templateProps={templateProps}
103-
toggleRefinement={toggleRefinement.bind(null, helper, facetName, facetValue.isRefined)}
141+
toggleRefinement={toggleRefinement}
104142
/>,
105143
containerNode
106144
);
107145
}
108146
};
109147
}
110148

111-
function toggleRefinement(helper, facetName, isRefined) {
112-
let action = isRefined ? 'remove' : 'add';
113-
114-
helper[action + 'FacetRefinement'](facetName, true);
115-
helper.search();
116-
}
117149

118150
module.exports = toggle;

0 commit comments

Comments
 (0)