+
diff --git a/docs/_includes/widget-jsdoc/clearAll.md b/docs/_includes/widget-jsdoc/clearAll.md
new file mode 100644
index 0000000000..31f032fc26
--- /dev/null
+++ b/docs/_includes/widget-jsdoc/clearAll.md
@@ -0,0 +1,16 @@
+| Param | Description |
+| --- | --- |
+|
`options.container` | CSS Selector or DOMElement to insert the widget |
+|
`options.cssClasses` | CSS classes to add |
+|
`options.cssClasses.root` | CSS class to add to the root element |
+|
`options.cssClasses.header` | CSS class to add to the header element |
+|
`options.cssClasses.body` | CSS class to add to the body element |
+|
`options.cssClasses.footer` | CSS class to add to the footer element |
+|
`options.cssClasses.link` | CSS class to add to the link element |
+|
`options.templates` | Templates to use for the widget |
+|
`options.templates.header` | Header template |
+|
`options.templates.link` | Link template |
+|
`options.templates.footer` | Footer template |
+|
`options.autoHideContainer` | Hide the container when there's no refinement to clear |
+
+
* Required
diff --git a/docs/documentation.md b/docs/documentation.md
index 9702771114..54de173d9c 100644
--- a/docs/documentation.md
+++ b/docs/documentation.md
@@ -752,6 +752,47 @@ instantsearch.widgets.priceRanges(options);
+#### clearAll
+
+
+
+
+This filtering widget lets the user choose between ranges of price. Those ranges are dynamically computed based on the returned results.
+{:.description}
+
+
+
+{% highlight javascript %}
+search.addWidget(
+ instantsearch.widgets.clearAll({
+ container: '#clear-all',
+ templates: {
+ link: 'Reset everything'
+ },
+ cssClasses: {
+ root: '',
+ header: '',
+ body: '',
+ footer: '',
+ link: '',
+ },
+ autoHideContainer: false
+ })
+);
+{% endhighlight %}
+
+
+{% highlight javascript %}
+instantsearch.widgets.clearAll(options);
+{% endhighlight %}
+{% include widget-jsdoc/clearAll.md %}
+
+
+
+
+
+
+
### Sort
#### indexSelector
diff --git a/lib/main.js b/lib/main.js
index 305c3b2633..44606d2028 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -8,6 +8,7 @@ let instantsearch = toFactory(InstantSearch);
let algoliasearchHelper = require('algoliasearch-helper');
instantsearch.widgets = {
+ clearAll: require('../widgets/clear-all/clear-all.js'),
hierarchicalMenu: require('../widgets/hierarchical-menu/hierarchical-menu.js'),
hits: require('../widgets/hits/hits'),
hitsPerPageSelector: require('../widgets/hits-per-page-selector/hits-per-page-selector'),
diff --git a/widgets/clear-all/__tests__/clear-all-test.js b/widgets/clear-all/__tests__/clear-all-test.js
new file mode 100644
index 0000000000..40ace0ff20
--- /dev/null
+++ b/widgets/clear-all/__tests__/clear-all-test.js
@@ -0,0 +1,130 @@
+/* eslint-env mocha */
+
+import React from 'react';
+
+import expect from 'expect';
+import sinon from 'sinon';
+import jsdom from 'mocha-jsdom';
+
+import expectJSX from 'expect-jsx';
+expect.extend(expectJSX);
+
+import clearAll from '../clear-all';
+import ClearAll from '../../../components/ClearAll/ClearAll';
+
+describe('clearAll()', () => {
+ jsdom({useEach: true});
+
+ let ReactDOM;
+ let container;
+ let widget;
+ let props;
+ let results;
+ let helper;
+ let autoHideContainerHOC;
+ let headerFooterHOC;
+ let createURL;
+
+ beforeEach(() => {
+ ReactDOM = {render: sinon.spy()};
+ autoHideContainerHOC = sinon.stub().returns(ClearAll);
+ headerFooterHOC = sinon.stub().returns(ClearAll);
+ createURL = sinon.stub().returns('#all-cleared');
+
+ clearAll.__Rewire__('ReactDOM', ReactDOM);
+ clearAll.__Rewire__('autoHideContainerHOC', autoHideContainerHOC);
+ clearAll.__Rewire__('headerFooterHOC', headerFooterHOC);
+
+ container = document.createElement('div');
+ widget = clearAll({container, autoHideContainer: true});
+
+ results = {};
+ helper = {
+ state: {
+ clearRefinements: sinon.spy(),
+ clearTags: sinon.spy()
+ },
+ search: sinon.spy()
+ };
+
+ props = {
+ clearAll: sinon.spy(),
+ cssClasses: {
+ root: 'ais-clear-all',
+ header: 'ais-clear-all--header',
+ body: 'ais-clear-all--body',
+ footer: 'ais-clear-all--footer',
+ link: 'ais-clear-all--link'
+ },
+ hasRefinements: false,
+ shouldAutoHideContainer: true,
+ templateProps: {
+ templates: require('../defaultTemplates'),
+ templatesConfig: undefined,
+ transformData: undefined,
+ useCustomCompileOptions: {header: false, footer: false, link: false}
+ },
+ url: '#all-cleared'
+ };
+ });
+
+ it('configures nothing', () => {
+ expect(widget.getConfiguration).toEqual(undefined);
+ });
+
+ it('calls the decorators', () => {
+ widget.render({results, helper, state: helper.state, createURL});
+ expect(headerFooterHOC.calledOnce).toBe(true);
+ expect(autoHideContainerHOC.calledOnce).toBe(true);
+ });
+
+ context('without refinements', () => {
+ beforeEach(() => {
+ helper.state.facetsRefinements = {};
+ props.hasRefinements = false;
+ props.shouldAutoHideContainer = true;
+ });
+
+ it('calls twice ReactDOM.render(
, container)', () => {
+ widget.render({results, helper, state: helper.state, createURL});
+ widget.render({results, helper, state: helper.state, createURL});
+
+ expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
+ expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(
);
+ expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
+ expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(
);
+ expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ });
+ });
+
+ context('with refinements', () => {
+ beforeEach(() => {
+ helper.state.facetsRefinements = ['something'];
+ props.hasRefinements = true;
+ props.shouldAutoHideContainer = false;
+ });
+
+ it('calls twice ReactDOM.render(
, container)', () => {
+ widget.render({results, helper, state: helper.state, createURL});
+ widget.render({results, helper, state: helper.state, createURL});
+
+ expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
+ expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(
);
+ expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
+ expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(
);
+ expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ });
+ });
+
+ afterEach(() => {
+ clearAll.__ResetDependency__('ReactDOM');
+ clearAll.__ResetDependency__('defaultTemplates');
+ });
+
+ function getProps(extraProps = {}) {
+ return {
+ ...props,
+ ...extraProps
+ };
+ }
+});
diff --git a/widgets/clear-all/clear-all.js b/widgets/clear-all/clear-all.js
new file mode 100644
index 0000000000..7817c7424d
--- /dev/null
+++ b/widgets/clear-all/clear-all.js
@@ -0,0 +1,93 @@
+let React = require('react');
+let ReactDOM = require('react-dom');
+
+let {bemHelper, getContainerNode, prepareTemplateProps, getRefinements} = require('../../lib/utils.js');
+let bem = bemHelper('ais-clear-all');
+let cx = require('classnames');
+
+let autoHideContainerHOC = require('../../decorators/autoHideContainer');
+let headerFooterHOC = require('../../decorators/headerFooter');
+
+let defaultTemplates = require('./defaultTemplates');
+
+/**
+ * Allows to clear all refinements at once
+ * @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
+ * @param {Object} [options.cssClasses] CSS classes to be added
+ * @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.link] CSS class to add to the link element
+ * @param {Object} [options.templates] Templates to use for the widget
+ * @param {string|Function} [options.templates.header=''] Header template
+ * @param {string|Function} [options.templates.link] Link template
+ * @param {string|Function} [options.templates.footer=''] Footer template
+ * @param {boolean} [options.autoHideContainer=true] Hide the container when there's no refinement to clear
+ * @return {Object}
+ */
+const usage = `Usage:
+toggle({
+ container,
+ [cssClasses.{root,header,body,footer,link}={}],
+ [templates.{header,link,footer}={header: '', link: 'Clear all', footer: ''}],
+ [autoHideContainer=true]
+})`;
+function clearAll({
+ container,
+ templates = defaultTemplates,
+ cssClasses: userCssClasses = {},
+ autoHideContainer = true
+ } = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ let containerNode = getContainerNode(container);
+ let ClearAll = headerFooterHOC(require('../../components/ClearAll/ClearAll.js'));
+ if (autoHideContainer === true) {
+ ClearAll = autoHideContainerHOC(ClearAll);
+ }
+
+ return {
+ _clearAll: function(helper) {
+ helper.clearTags().clearRefinements().search();
+ },
+
+ render: function({results, helper, state, templatesConfig, createURL}) {
+ let hasRefinements = getRefinements(results, state).length !== 0;
+
+ let 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),
+ link: cx(bem('link'), userCssClasses.link)
+ };
+
+ let url = createURL(state.clearRefinements());
+
+ let handleClick = this._clearAll.bind(null, helper);
+
+ let templateProps = prepareTemplateProps({
+ defaultTemplates,
+ templatesConfig,
+ templates
+ });
+
+ ReactDOM.render(
+
,
+ containerNode
+ );
+ }
+ };
+}
+
+module.exports = clearAll;
diff --git a/widgets/clear-all/defaultTemplates.js b/widgets/clear-all/defaultTemplates.js
new file mode 100644
index 0000000000..a0f7448bc7
--- /dev/null
+++ b/widgets/clear-all/defaultTemplates.js
@@ -0,0 +1,5 @@
+module.exports = {
+ header: '',
+ link: 'Clear all',
+ footer: ''
+};