Skip to content

Commit

Permalink
feat(connector): toggle connector
Browse files Browse the repository at this point in the history
This one is special. It contains a compatibility layer that we might
want to remove for good. The tests are disabled on that part.
  • Loading branch information
Alexandre Stanislawski committed Feb 15, 2017
1 parent 680743b commit bf9a9c0
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 118 deletions.
137 changes: 137 additions & 0 deletions src/connectors/toggle/connectToggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
bemHelper,
getContainerNode,
} from '../../lib/utils.js';
import defaultTemplates from './defaultTemplates.js';
import cx from 'classnames';
import connectCurrent from './implementations/current.js';
import connectLegacy from './implementations/legacy.js';

const bem = bemHelper('ais-toggle');

// we cannot use helper. because the facet is not yet declared in the helper
const hasFacetsRefinementsFor = (attributeName, searchParameters) =>
searchParameters &&
searchParameters.facetsRefinements &&
searchParameters.facetsRefinements[attributeName] !== undefined;

/**
* Instantiate the toggling of a boolean facet filter on and off.
* @function toggle
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} options.attributeName Name of the attribute for faceting (eg. "free_shipping")
* @param {string} options.label Human-readable name of the filter (eg. "Free Shipping")
* @param {Object} [options.values] Lets you define the values to filter on when toggling
* @param {string|number|boolean} [options.values.on=true] Value to filter on when checked
* @param {string|number|boolean} [options.values.off=undefined] Value to filter on when unchecked
* element (when using the default template). By default when switching to `off`, no refinement will be asked. So you
* will get both `true` and `false` results. If you set the off value to `false` then you will get only objects
* having `false` has a value for the selected attribute.
* @param {Object} [options.templates] Templates to use for the widget
* @param {string|Function} [options.templates.header] Header template
* @param {string|Function} [options.templates.item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties
* count is always the number of hits that would be shown if you toggle the widget. We also provide
* `onFacetValue` and `offFacetValue` objects with according counts.
* @param {string|Function} [options.templates.footer] Footer template
* @param {Function} [options.transformData.item] Function to change the object passed to the `item` template
* @param {boolean} [options.autoHideContainer=true] Hide the container when there are no results
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
* @param {string|string[]} [options.cssClasses.list] CSS class to add to the list element
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each item element
* @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element
* @param {string|string[]} [options.cssClasses.label] CSS class to add to each
* label element (when using the default template)
* @param {string|string[]} [options.cssClasses.checkbox] CSS class to add to each
* checkbox element (when using the default template)
* @param {string|string[]} [options.cssClasses.count] CSS class to add to each count
* @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header
* @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget
* @return {Object}
*/
const usage = `Usage:
toggle({
container,
attributeName,
label,
[ values={on: true, off: undefined} ],
[ cssClasses.{root,header,body,footer,list,item,active,label,checkbox,count} ],
[ templates.{header,item,footer} ],
[ transformData.{item} ],
[ autoHideContainer=true ],
[ collapsible=false ]
})`;
function connectToggle(toggleRendering) {
const legacyToggle = connectLegacy(toggleRendering);
const currentToggle = connectCurrent(toggleRendering);

return ({
container,
attributeName,
label,
values: userValues = {on: true, off: undefined},
templates = defaultTemplates,
collapsible = false,
cssClasses: userCssClasses = {},
transformData,
autoHideContainer = true,
} = {}) => {
const containerNode = getContainerNode(container);

if (!container || !attributeName || !label) {
throw new Error(usage);
}

const hasAnOffValue = userValues.off !== undefined;

const cssClasses = {
root: cx(bem(null), userCssClasses.root),
header: cx(bem('header'), userCssClasses.header),
body: cx(bem('body'), userCssClasses.body),
footer: cx(bem('footer'), userCssClasses.footer),
list: cx(bem('list'), userCssClasses.list),
item: cx(bem('item'), userCssClasses.item),
active: cx(bem('item', 'active'), userCssClasses.active),
label: cx(bem('label'), userCssClasses.label),
checkbox: cx(bem('checkbox'), userCssClasses.checkbox),
count: cx(bem('count'), userCssClasses.count),
};

// store the computed options for usage in the two toggle implementations
const implemOptions = {
attributeName,
label,
userValues,
templates,
collapsible,
transformData,
hasAnOffValue,
containerNode,
cssClasses,
autoHideContainer,
};

return {
getConfiguration(currentSearchParameters, searchParametersFromUrl) {
const useLegacyToggle =
hasFacetsRefinementsFor(attributeName, currentSearchParameters) ||
hasFacetsRefinementsFor(attributeName, searchParametersFromUrl);

const toggleImplementation = useLegacyToggle ?
legacyToggle(implemOptions) :
currentToggle(implemOptions);

this.init = toggleImplementation.init.bind(toggleImplementation);
this.render = toggleImplementation.render.bind(toggleImplementation);
return toggleImplementation.getConfiguration(currentSearchParameters, searchParametersFromUrl);
},
init() {},
render() {},
};
};
}

export default connectToggle;
File renamed without changes.
132 changes: 132 additions & 0 deletions src/connectors/toggle/implementations/current.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import find from 'lodash/find';
import defaultTemplates from '../defaultTemplates.js';
import {
prepareTemplateProps,
escapeRefinement,
unescapeRefinement,
} from '../../../lib/utils.js';

const connectToggle = toggleRendering => ({
attributeName,
label,
userValues,
templates,
collapsible,
transformData,
hasAnOffValue,
containerNode,
cssClasses,
autoHideContainer,
} = {}) => {
const on = userValues ? escapeRefinement(userValues.on) : undefined;
const off = userValues ? escapeRefinement(userValues.off) : undefined;

return {
getConfiguration() {
return {
disjunctiveFacets: [attributeName],
};
},
toggleRefinement(helper, facetValue, isRefined) {
// Checking
if (!isRefined) {
if (hasAnOffValue) {
helper.removeDisjunctiveFacetRefinement(attributeName, off);
}
helper.addDisjunctiveFacetRefinement(attributeName, on);
} else {
// Unchecking
helper.removeDisjunctiveFacetRefinement(attributeName, on);
if (hasAnOffValue) {
helper.addDisjunctiveFacetRefinement(attributeName, off);
}
}

helper.search();
},
init({state, helper, templatesConfig}) {
this._templateProps = prepareTemplateProps({
transformData,
defaultTemplates,
templatesConfig,
templates,
});

this.toggleRefinement = this.toggleRefinement.bind(this, helper);

// no need to refine anything at init if no custom off values
if (!hasAnOffValue) {
return;
}

// Add filtering on the 'off' value if set
const isRefined = state.isDisjunctiveFacetRefined(attributeName, on);
if (!isRefined) {
helper.addDisjunctiveFacetRefinement(attributeName, off);
}

toggleRendering({
collapsible,
createURL: () => '',
cssClasses,
facetValues: [],
shouldAutoHideContainer: autoHideContainer,
templateProps: this._templateProps,
toggleRefinement: this.toggleRefinement,
containerNode,
}, false);
},
render({helper, results, state, createURL}) {
const isRefined = helper.state.isDisjunctiveFacetRefined(attributeName, on);
const offValue = off === undefined ? false : off;
const allFacetValues = results.getFacetValues(attributeName);
const onData = find(allFacetValues, {name: unescapeRefinement(on)});
const onFacetValue = {
name: label,
isRefined: onData !== undefined ? onData.isRefined : false,
count: onData === undefined ? null : onData.count,
};
const offData = hasAnOffValue ? find(allFacetValues, {name: unescapeRefinement(offValue)}) : undefined;
const offFacetValue = {
name: label,
isRefined: offData !== undefined ? offData.isRefined : false,
count: offData === undefined ? results.nbHits : offData.count,
};

// what will we show by default,
// if checkbox is not checked, show: [ ] free shipping (countWhenChecked)
// if checkbox is checked, show: [x] free shipping (countWhenNotChecked)
const nextRefinement = isRefined ? offFacetValue : onFacetValue;

const facetValue = {
name: label,
isRefined,
count: nextRefinement === undefined ? null : nextRefinement.count,
onFacetValue,
offFacetValue,
};

// Bind createURL to this specific attribute
function _createURL() {
return createURL(
state
.removeDisjunctiveFacetRefinement(attributeName, isRefined ? on : off)
.addDisjunctiveFacetRefinement(attributeName, isRefined ? off : on)
);
}

toggleRendering({
collapsible,
createURL: _createURL,
cssClasses,
facetValues: [facetValue],
shouldAutoHideContainer: autoHideContainer && (facetValue.count === 0 || facetValue.count === null),
templateProps: this._templateProps,
toggleRefinement: this.toggleRefinement,
containerNode,
}, false);
},
};
};

export default connectToggle;
111 changes: 111 additions & 0 deletions src/connectors/toggle/implementations/legacy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import find from 'lodash/find';
import defaultTemplates from '../defaultTemplates.js';
import {
prepareTemplateProps,
} from '../../../lib/utils.js';

const connectToggle = toggleRendering => ({
attributeName,
label,
userValues,
templates,
collapsible,
transformData,
hasAnOffValue,
autoHideContainer,
cssClasses,
containerNode,
} = {}) => { //eslint-disable-line
return {
getConfiguration() {
return {
facets: [attributeName],
};
},
toggleRefinement(helper, facetValue, isRefined) {
const on = userValues.on;
const off = userValues.off;

// Checking
if (!isRefined) {
if (hasAnOffValue) {
helper.removeFacetRefinement(attributeName, off);
}
helper.addFacetRefinement(attributeName, on);
} else {
// Unchecking
helper.removeFacetRefinement(attributeName, on);
if (hasAnOffValue) {
helper.addFacetRefinement(attributeName, off);
}
}

helper.search();
},
init({state, helper, templatesConfig}) {
this._templateProps = prepareTemplateProps({
transformData,
defaultTemplates,
templatesConfig,
templates,
});
this.toggleRefinement = this.toggleRefinement.bind(this, helper);

// no need to refine anything at init if no custom off values
if (!hasAnOffValue) {
return;
}
// Add filtering on the 'off' value if set
const isRefined = state.isFacetRefined(attributeName, userValues.on);
if (!isRefined) {
helper.addFacetRefinement(attributeName, userValues.off);
}

toggleRendering({
collapsible,
createURL: () => '',
cssClasses,
facetValues: [],
shouldAutoHideContainer: autoHideContainer,
templateProps: this._templateProps,
toggleRefinement: this.toggleRefinement,
containerNode,
}, true);
},
render({helper, results, state, createURL}) {
const isRefined = helper.state.isFacetRefined(attributeName, userValues.on);
const currentRefinement = isRefined ? userValues.on : userValues.off;
let count;
if (typeof currentRefinement === 'number') {
count = results.getFacetStats(attributeName).sum;
} else {
const facetData = find(results.getFacetValues(attributeName), {name: isRefined.toString()});
count = facetData !== undefined ? facetData.count : null;
}

const facetValue = {
name: label,
isRefined,
count,
};

// Bind createURL to this specific attribute
function _createURL() {
return createURL(state.toggleRefinement(attributeName, isRefined));
}

toggleRendering({
collapsible,
createURL: _createURL,
cssClasses,
facetValues: [facetValue],
shouldAutoHideContainer: autoHideContainer && results.nbHits === 0,
templateProps: this._templateProps,
toggleRefinement: this.toggleRefinement,
containerNode,
}, false);
},
};
};

export default connectToggle;
Loading

0 comments on commit bf9a9c0

Please sign in to comment.