Skip to content

Commit

Permalink
feat(toggle): Allow custom on/off values
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pixelastic committed Nov 6, 2015
1 parent 3f8eb9e commit 9b6c2bf
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 16 deletions.
3 changes: 3 additions & 0 deletions docs/_includes/widget-jsdoc/toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
| <span class='attr-required'>`options.container`</span> | CSS Selector or DOMElement to insert the widget |
| <span class='attr-required'>`options.facetName`</span> | Name of the attribute for faceting (eg. "free_shipping") |
| <span class='attr-required'>`options.label`</span> | Human-readable name of the filter (eg. "Free Shipping") |
| <span class='attr-optional'>`options.values`</span> | Lets you define the values to filter on when toggling |
| <span class='attr-optional'>`options.values.on`</span> | Value to filter on when checked |
| <span class='attr-optional'>`options.values.off`</span> | Value to filter on when unchecked |
| <span class='attr-optional'>`options.cssClasses`</span> | CSS classes to add |
| <span class='attr-optional'>`options.cssClasses.root`</span> | CSS class to add to the root element |
| <span class='attr-optional'>`options.cssClasses.header`</span> | CSS class to add to the header element |
Expand Down
4 changes: 4 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,10 @@ search.addWidget(
container: '#free-shipping',
facetName: 'free_shipping',
label: 'Free Shipping',
values: {
on: true,
off: false
},
templates: {
header: 'Shipping'
},
Expand Down
136 changes: 128 additions & 8 deletions widgets/toggle/__tests__/toggle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ describe('toggle()', () => {
transformData: undefined
};
helper = {
hasRefinements: sinon.stub().returns(false),
state: {
isFacetRefined: sinon.stub().returns(false)
},
removeFacetRefinement: sinon.spy(),
addFacetRefinement: sinon.spy(),
search: sinon.spy()
Expand Down Expand Up @@ -163,7 +165,9 @@ describe('toggle()', () => {

it('when refined', () => {
helper = {
hasRefinements: sinon.stub().returns(true)
state: {
isFacetRefined: sinon.stub().returns(true)
}
};
results = {
hits: [{Hello: ', world!'}],
Expand Down Expand Up @@ -198,12 +202,128 @@ describe('toggle()', () => {
expect(helper.addFacetRefinement.calledOnce).toBe(true);
expect(helper.addFacetRefinement.calledWithExactly(facetName, true));
helper.hasRefinements = sinon.stub().returns(true);
ReactDOM.render.reset();
widget.render({results, helper});
toggleRefinement = ReactDOM.render.firstCall.args[0].props.toggleRefinement;
toggleRefinement();
expect(helper.removeFacetRefinement.calledOnce).toBe(true);
expect(helper.removeFacetRefinement.calledWithExactly(facetName, true));
});
});

context('toggleRefinement', () => {
let helper;
let values;

function toggleOn() {
widget.toggleRefinement(helper, false);
}
function toggleOff() {
widget.toggleRefinement(helper, true);
}

beforeEach(() => {
helper = {
removeFacetRefinement: sinon.spy(),
addFacetRefinement: sinon.spy(),
search: sinon.spy()
};
});

context('default values', () => {
it('toggle on should add filter to true', () => {
// Given
widget = toggle({container, facetName, label});

// When
toggleOn();

// Then
expect(helper.addFacetRefinement.calledWith(facetName, true)).toBe(true);
expect(helper.removeFacetRefinement.called).toBe(false);
});
it('toggle off should remove all filters', () => {
// Given
widget = toggle({container, facetName, label});

// When
toggleOff();

// Then
expect(helper.removeFacetRefinement.calledWith(facetName, true)).toBe(true);
expect(helper.addFacetRefinement.called).toBe(false);
});
});
context('specific values', () => {
it('toggle on should change the refined value', () => {
// Given
values = {on: 'on', off: 'off'};
widget = toggle({container, facetName, label, values});

// When
toggleOn();

// Then
expect(helper.removeFacetRefinement.calledWith(facetName, 'off')).toBe(true);
expect(helper.addFacetRefinement.calledWith(facetName, 'on')).toBe(true);
});
it('toggle off should change the refined value', () => {
// Given
values = {on: 'on', off: 'off'};
widget = toggle({container, facetName, label, values});

// When
toggleOff();

// Then
expect(helper.removeFacetRefinement.calledWith(facetName, 'on')).toBe(true);
expect(helper.addFacetRefinement.calledWith(facetName, 'off')).toBe(true);
});
});
});

context('custom off value', () => {
it('should add a refinement for custom off value on init', () => {
// Given
let values = {on: 'on', off: 'off'};
widget = toggle({container, facetName, label, values});
let state = {
isFacetRefined: sinon.stub().returns(false)
};
let helper = {
addFacetRefinement: sinon.spy()
};

// When
widget.init(state, helper);

// Then
expect(helper.addFacetRefinement.calledWith(facetName, 'off')).toBe(true);
});
it('should not add a refinement for custom off value on init if already checked', () => {
// Given
let values = {on: 'on', off: 'off'};
widget = toggle({container, facetName, label, values});
let state = {
isFacetRefined: sinon.stub().returns(true)
};
let helper = {
addFacetRefinement: sinon.spy()
};

// When
widget.init(state, helper);

// Then
expect(helper.addFacetRefinement.called).toBe(false);
});
it('should not add a refinement for no custom off value on init', () => {
// Given
widget = toggle({container, facetName, label});
let state = {};
let helper = {
addFacetRefinement: sinon.spy()
};

// When
widget.init(state, helper);

// Then
expect(helper.addFacetRefinement.called).toBe(false);
});
});

Expand Down
48 changes: 40 additions & 8 deletions widgets/toggle/toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ let defaultTemplates = require('./defaultTemplates');
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} options.facetName 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 {*} [options.values.on] Value to filter on when checked
* @param {*} [options.values.off] Value to filter on when unchecked
* @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
Expand All @@ -41,6 +44,7 @@ function toggle({
container,
facetName,
label,
values: userValues = {on: true, off: undefined},
templates = defaultTemplates,
cssClasses: userCssClasses = {},
transformData,
Expand All @@ -58,12 +62,44 @@ function toggle({
throw new Error(usage);
}

let hasAnOffValue = (userValues.off !== undefined);

return {
getConfiguration: () => ({
facets: [facetName]
}),
init: (state, helper) => {
if (userValues.off === undefined) {
return;
}
// Add filtering on the 'off' value if set
let isRefined = state.isFacetRefined(facetName, userValues.on);
if (!isRefined) {
helper.addFacetRefinement(facetName, userValues.off);
}
},
toggleRefinement: (helper, isRefined) => {
let on = userValues.on;
let off = userValues.off;

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

helper.search();
},
render: function({helper, results, templatesConfig, state, createURL}) {
let isRefined = helper.hasRefinements(facetName);
let isRefined = helper.state.isFacetRefined(facetName, userValues.on);
let values = find(results.getFacetValues(facetName), {name: isRefined.toString()});
let hasNoResults = results.nbHits === 0;

Expand Down Expand Up @@ -93,26 +129,22 @@ function toggle({
count: cx(bem('count'), userCssClasses.count)
};

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

ReactDOM.render(
<RefinementList
createURL={() => createURL(state.toggleRefinement(facetName, facetValue.isRefined))}
cssClasses={cssClasses}
facetValues={[facetValue]}
shouldAutoHideContainer={hasNoResults}
templateProps={templateProps}
toggleRefinement={toggleRefinement.bind(null, helper, facetName, facetValue.isRefined)}
toggleRefinement={toggleRefinement}
/>,
containerNode
);
}
};
}

function toggleRefinement(helper, facetName, isRefined) {
let action = isRefined ? 'remove' : 'add';

helper[action + 'FacetRefinement'](facetName, true);
helper.search();
}

module.exports = toggle;

0 comments on commit 9b6c2bf

Please sign in to comment.