Skip to content

Commit bf9a9c0

Browse files
author
Alexandre Stanislawski
committed
feat(connector): toggle connector
This one is special. It contains a compatibility layer that we might want to remove for good. The tests are disabled on that part.
1 parent 680743b commit bf9a9c0

File tree

9 files changed

+412
-118
lines changed

9 files changed

+412
-118
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {
2+
bemHelper,
3+
getContainerNode,
4+
} from '../../lib/utils.js';
5+
import defaultTemplates from './defaultTemplates.js';
6+
import cx from 'classnames';
7+
import connectCurrent from './implementations/current.js';
8+
import connectLegacy from './implementations/legacy.js';
9+
10+
const bem = bemHelper('ais-toggle');
11+
12+
// we cannot use helper. because the facet is not yet declared in the helper
13+
const hasFacetsRefinementsFor = (attributeName, searchParameters) =>
14+
searchParameters &&
15+
searchParameters.facetsRefinements &&
16+
searchParameters.facetsRefinements[attributeName] !== undefined;
17+
18+
/**
19+
* Instantiate the toggling of a boolean facet filter on and off.
20+
* @function toggle
21+
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
22+
* @param {string} options.attributeName Name of the attribute for faceting (eg. "free_shipping")
23+
* @param {string} options.label Human-readable name of the filter (eg. "Free Shipping")
24+
* @param {Object} [options.values] Lets you define the values to filter on when toggling
25+
* @param {string|number|boolean} [options.values.on=true] Value to filter on when checked
26+
* @param {string|number|boolean} [options.values.off=undefined] Value to filter on when unchecked
27+
* element (when using the default template). By default when switching to `off`, no refinement will be asked. So you
28+
* will get both `true` and `false` results. If you set the off value to `false` then you will get only objects
29+
* having `false` has a value for the selected attribute.
30+
* @param {Object} [options.templates] Templates to use for the widget
31+
* @param {string|Function} [options.templates.header] Header template
32+
* @param {string|Function} [options.templates.item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties
33+
* count is always the number of hits that would be shown if you toggle the widget. We also provide
34+
* `onFacetValue` and `offFacetValue` objects with according counts.
35+
* @param {string|Function} [options.templates.footer] Footer template
36+
* @param {Function} [options.transformData.item] Function to change the object passed to the `item` template
37+
* @param {boolean} [options.autoHideContainer=true] Hide the container when there are no results
38+
* @param {Object} [options.cssClasses] CSS classes to add
39+
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
40+
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
41+
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
42+
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
43+
* @param {string|string[]} [options.cssClasses.list] CSS class to add to the list element
44+
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each item element
45+
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
46+
* @param {string|string[]} [options.cssClasses.label] CSS class to add to each
47+
* label element (when using the default template)
48+
* @param {string|string[]} [options.cssClasses.checkbox] CSS class to add to each
49+
* checkbox element (when using the default template)
50+
* @param {string|string[]} [options.cssClasses.count] CSS class to add to each count
51+
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
52+
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
53+
* @return {Object}
54+
*/
55+
const usage = `Usage:
56+
toggle({
57+
container,
58+
attributeName,
59+
label,
60+
[ values={on: true, off: undefined} ],
61+
[ cssClasses.{root,header,body,footer,list,item,active,label,checkbox,count} ],
62+
[ templates.{header,item,footer} ],
63+
[ transformData.{item} ],
64+
[ autoHideContainer=true ],
65+
[ collapsible=false ]
66+
})`;
67+
function connectToggle(toggleRendering) {
68+
const legacyToggle = connectLegacy(toggleRendering);
69+
const currentToggle = connectCurrent(toggleRendering);
70+
71+
return ({
72+
container,
73+
attributeName,
74+
label,
75+
values: userValues = {on: true, off: undefined},
76+
templates = defaultTemplates,
77+
collapsible = false,
78+
cssClasses: userCssClasses = {},
79+
transformData,
80+
autoHideContainer = true,
81+
} = {}) => {
82+
const containerNode = getContainerNode(container);
83+
84+
if (!container || !attributeName || !label) {
85+
throw new Error(usage);
86+
}
87+
88+
const hasAnOffValue = userValues.off !== undefined;
89+
90+
const cssClasses = {
91+
root: cx(bem(null), userCssClasses.root),
92+
header: cx(bem('header'), userCssClasses.header),
93+
body: cx(bem('body'), userCssClasses.body),
94+
footer: cx(bem('footer'), userCssClasses.footer),
95+
list: cx(bem('list'), userCssClasses.list),
96+
item: cx(bem('item'), userCssClasses.item),
97+
active: cx(bem('item', 'active'), userCssClasses.active),
98+
label: cx(bem('label'), userCssClasses.label),
99+
checkbox: cx(bem('checkbox'), userCssClasses.checkbox),
100+
count: cx(bem('count'), userCssClasses.count),
101+
};
102+
103+
// store the computed options for usage in the two toggle implementations
104+
const implemOptions = {
105+
attributeName,
106+
label,
107+
userValues,
108+
templates,
109+
collapsible,
110+
transformData,
111+
hasAnOffValue,
112+
containerNode,
113+
cssClasses,
114+
autoHideContainer,
115+
};
116+
117+
return {
118+
getConfiguration(currentSearchParameters, searchParametersFromUrl) {
119+
const useLegacyToggle =
120+
hasFacetsRefinementsFor(attributeName, currentSearchParameters) ||
121+
hasFacetsRefinementsFor(attributeName, searchParametersFromUrl);
122+
123+
const toggleImplementation = useLegacyToggle ?
124+
legacyToggle(implemOptions) :
125+
currentToggle(implemOptions);
126+
127+
this.init = toggleImplementation.init.bind(toggleImplementation);
128+
this.render = toggleImplementation.render.bind(toggleImplementation);
129+
return toggleImplementation.getConfiguration(currentSearchParameters, searchParametersFromUrl);
130+
},
131+
init() {},
132+
render() {},
133+
};
134+
};
135+
}
136+
137+
export default connectToggle;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import find from 'lodash/find';
2+
import defaultTemplates from '../defaultTemplates.js';
3+
import {
4+
prepareTemplateProps,
5+
escapeRefinement,
6+
unescapeRefinement,
7+
} from '../../../lib/utils.js';
8+
9+
const connectToggle = toggleRendering => ({
10+
attributeName,
11+
label,
12+
userValues,
13+
templates,
14+
collapsible,
15+
transformData,
16+
hasAnOffValue,
17+
containerNode,
18+
cssClasses,
19+
autoHideContainer,
20+
} = {}) => {
21+
const on = userValues ? escapeRefinement(userValues.on) : undefined;
22+
const off = userValues ? escapeRefinement(userValues.off) : undefined;
23+
24+
return {
25+
getConfiguration() {
26+
return {
27+
disjunctiveFacets: [attributeName],
28+
};
29+
},
30+
toggleRefinement(helper, facetValue, isRefined) {
31+
// Checking
32+
if (!isRefined) {
33+
if (hasAnOffValue) {
34+
helper.removeDisjunctiveFacetRefinement(attributeName, off);
35+
}
36+
helper.addDisjunctiveFacetRefinement(attributeName, on);
37+
} else {
38+
// Unchecking
39+
helper.removeDisjunctiveFacetRefinement(attributeName, on);
40+
if (hasAnOffValue) {
41+
helper.addDisjunctiveFacetRefinement(attributeName, off);
42+
}
43+
}
44+
45+
helper.search();
46+
},
47+
init({state, helper, templatesConfig}) {
48+
this._templateProps = prepareTemplateProps({
49+
transformData,
50+
defaultTemplates,
51+
templatesConfig,
52+
templates,
53+
});
54+
55+
this.toggleRefinement = this.toggleRefinement.bind(this, helper);
56+
57+
// no need to refine anything at init if no custom off values
58+
if (!hasAnOffValue) {
59+
return;
60+
}
61+
62+
// Add filtering on the 'off' value if set
63+
const isRefined = state.isDisjunctiveFacetRefined(attributeName, on);
64+
if (!isRefined) {
65+
helper.addDisjunctiveFacetRefinement(attributeName, off);
66+
}
67+
68+
toggleRendering({
69+
collapsible,
70+
createURL: () => '',
71+
cssClasses,
72+
facetValues: [],
73+
shouldAutoHideContainer: autoHideContainer,
74+
templateProps: this._templateProps,
75+
toggleRefinement: this.toggleRefinement,
76+
containerNode,
77+
}, false);
78+
},
79+
render({helper, results, state, createURL}) {
80+
const isRefined = helper.state.isDisjunctiveFacetRefined(attributeName, on);
81+
const offValue = off === undefined ? false : off;
82+
const allFacetValues = results.getFacetValues(attributeName);
83+
const onData = find(allFacetValues, {name: unescapeRefinement(on)});
84+
const onFacetValue = {
85+
name: label,
86+
isRefined: onData !== undefined ? onData.isRefined : false,
87+
count: onData === undefined ? null : onData.count,
88+
};
89+
const offData = hasAnOffValue ? find(allFacetValues, {name: unescapeRefinement(offValue)}) : undefined;
90+
const offFacetValue = {
91+
name: label,
92+
isRefined: offData !== undefined ? offData.isRefined : false,
93+
count: offData === undefined ? results.nbHits : offData.count,
94+
};
95+
96+
// what will we show by default,
97+
// if checkbox is not checked, show: [ ] free shipping (countWhenChecked)
98+
// if checkbox is checked, show: [x] free shipping (countWhenNotChecked)
99+
const nextRefinement = isRefined ? offFacetValue : onFacetValue;
100+
101+
const facetValue = {
102+
name: label,
103+
isRefined,
104+
count: nextRefinement === undefined ? null : nextRefinement.count,
105+
onFacetValue,
106+
offFacetValue,
107+
};
108+
109+
// Bind createURL to this specific attribute
110+
function _createURL() {
111+
return createURL(
112+
state
113+
.removeDisjunctiveFacetRefinement(attributeName, isRefined ? on : off)
114+
.addDisjunctiveFacetRefinement(attributeName, isRefined ? off : on)
115+
);
116+
}
117+
118+
toggleRendering({
119+
collapsible,
120+
createURL: _createURL,
121+
cssClasses,
122+
facetValues: [facetValue],
123+
shouldAutoHideContainer: autoHideContainer && (facetValue.count === 0 || facetValue.count === null),
124+
templateProps: this._templateProps,
125+
toggleRefinement: this.toggleRefinement,
126+
containerNode,
127+
}, false);
128+
},
129+
};
130+
};
131+
132+
export default connectToggle;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import find from 'lodash/find';
2+
import defaultTemplates from '../defaultTemplates.js';
3+
import {
4+
prepareTemplateProps,
5+
} from '../../../lib/utils.js';
6+
7+
const connectToggle = toggleRendering => ({
8+
attributeName,
9+
label,
10+
userValues,
11+
templates,
12+
collapsible,
13+
transformData,
14+
hasAnOffValue,
15+
autoHideContainer,
16+
cssClasses,
17+
containerNode,
18+
} = {}) => { //eslint-disable-line
19+
return {
20+
getConfiguration() {
21+
return {
22+
facets: [attributeName],
23+
};
24+
},
25+
toggleRefinement(helper, facetValue, isRefined) {
26+
const on = userValues.on;
27+
const off = userValues.off;
28+
29+
// Checking
30+
if (!isRefined) {
31+
if (hasAnOffValue) {
32+
helper.removeFacetRefinement(attributeName, off);
33+
}
34+
helper.addFacetRefinement(attributeName, on);
35+
} else {
36+
// Unchecking
37+
helper.removeFacetRefinement(attributeName, on);
38+
if (hasAnOffValue) {
39+
helper.addFacetRefinement(attributeName, off);
40+
}
41+
}
42+
43+
helper.search();
44+
},
45+
init({state, helper, templatesConfig}) {
46+
this._templateProps = prepareTemplateProps({
47+
transformData,
48+
defaultTemplates,
49+
templatesConfig,
50+
templates,
51+
});
52+
this.toggleRefinement = this.toggleRefinement.bind(this, helper);
53+
54+
// no need to refine anything at init if no custom off values
55+
if (!hasAnOffValue) {
56+
return;
57+
}
58+
// Add filtering on the 'off' value if set
59+
const isRefined = state.isFacetRefined(attributeName, userValues.on);
60+
if (!isRefined) {
61+
helper.addFacetRefinement(attributeName, userValues.off);
62+
}
63+
64+
toggleRendering({
65+
collapsible,
66+
createURL: () => '',
67+
cssClasses,
68+
facetValues: [],
69+
shouldAutoHideContainer: autoHideContainer,
70+
templateProps: this._templateProps,
71+
toggleRefinement: this.toggleRefinement,
72+
containerNode,
73+
}, true);
74+
},
75+
render({helper, results, state, createURL}) {
76+
const isRefined = helper.state.isFacetRefined(attributeName, userValues.on);
77+
const currentRefinement = isRefined ? userValues.on : userValues.off;
78+
let count;
79+
if (typeof currentRefinement === 'number') {
80+
count = results.getFacetStats(attributeName).sum;
81+
} else {
82+
const facetData = find(results.getFacetValues(attributeName), {name: isRefined.toString()});
83+
count = facetData !== undefined ? facetData.count : null;
84+
}
85+
86+
const facetValue = {
87+
name: label,
88+
isRefined,
89+
count,
90+
};
91+
92+
// Bind createURL to this specific attribute
93+
function _createURL() {
94+
return createURL(state.toggleRefinement(attributeName, isRefined));
95+
}
96+
97+
toggleRendering({
98+
collapsible,
99+
createURL: _createURL,
100+
cssClasses,
101+
facetValues: [facetValue],
102+
shouldAutoHideContainer: autoHideContainer && results.nbHits === 0,
103+
templateProps: this._templateProps,
104+
toggleRefinement: this.toggleRefinement,
105+
containerNode,
106+
}, false);
107+
},
108+
};
109+
};
110+
111+
export default connectToggle;

0 commit comments

Comments
 (0)