Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clearAll): New widget #427

Merged
merged 1 commit into from
Nov 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions components/ClearAll/ClearAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
let React = require('react');

let Template = require('../Template.js');

let {isSpecialClick} = require('../../lib/utils.js');

class ClearAll extends React.Component {
handleClick(e) {
if (isSpecialClick(e)) {
// do not alter the default browser behavior
// if one special key is down
return;
}
e.preventDefault();
this.props.clearAll();
}

render() {
const className = this.props.cssClasses.link;
const data = {
hasRefinements: this.props.hasRefinements
};

return (
<a
className={className}
href={this.props.url}
onClick={this.handleClick.bind(this)}
>
<Template
data={data}
templateKey="link"
{...this.props.templateProps}
/>
</a>);
}
}

ClearAll.propTypes = {
clearAll: React.PropTypes.func.isRequired,
cssClasses: React.PropTypes.shape({
link: React.PropTypes.string
}),
hasRefinements: React.PropTypes.bool.isRequired,
templateProps: React.PropTypes.object.isRequired,
url: React.PropTypes.string.isRequired
};

module.exports = ClearAll;
73 changes: 73 additions & 0 deletions components/ClearAll/__tests__/ClearAll-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-env mocha */

import React from 'react';
import expect from 'expect';
import sinon from 'sinon';
import TestUtils from 'react-addons-test-utils';
import ClearAll from '../ClearAll.js';
import Template from '../../Template.js';

import expectJSX from 'expect-jsx';
expect.extend(expectJSX);

let {createRenderer} = TestUtils;

describe('ClearAll', () => {
let renderer;
let defaultProps = {
clearAll: () => {},
cssClasses: {
link: 'custom-link'
},
hasRefinements: false,
templateProps: {},
url: '#all-cleared!'
};

beforeEach(() => {
renderer = createRenderer();
});

it('should render <ClearAll />', () => {
let out = render();
expect(out).toEqualJSX(
<a
className="custom-link"
href="#all-cleared!"
onClick={() => {}}
>
<Template
data={{hasRefinements: false}}
templateKey="link"
/>
</a>);
});

it('should handle clicks (and special clicks)', () => {
let props = {
clearAll: sinon.spy()
};
let preventDefault = sinon.spy();
let component = new ClearAll(props);
['ctrlKey', 'shiftKey', 'altKey', 'metaKey'].forEach((e) => {
let event = {preventDefault};
event[e] = true;
component.handleClick(event);
expect(props.clearAll.called).toBe(false, 'clearAll never called');
expect(preventDefault.called).toBe(false, 'preventDefault never called');
});
component.handleClick({preventDefault});
expect(props.clearAll.calledOnce).toBe(true, 'clearAll called once');
expect(preventDefault.calledOnce).toBe(true, 'preventDefault called once');
});


function render(extraProps = {}) {
let props = {
...defaultProps,
...extraProps
};
renderer.render(<ClearAll {...props} />);
return renderer.getRenderOutput();
}
});
20 changes: 20 additions & 0 deletions css/default/_clear-all.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import "../base";
@import "variables";

@include block(clear-all) {
@include element(header) {
/* widget header */
}

@include element(body) {
/* widget body */
}

@include element(link) {
/* widget link */
}

@include element(footer) {
/* widget footer */
}
}
1 change: 1 addition & 0 deletions css/instantsearch.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
@import "default/hierarchical-menu";
@import "default/range-slider";
@import "default/price-ranges" ;
@import "default/clear-all";

7 changes: 7 additions & 0 deletions dev/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ search.addWidget(
})
);

search.addWidget(
instantsearch.widgets.clearAll({
container: '#clear-all',
autoHideContainer: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go for a more explicit option here (public facing API). Something like hideIfNoRefinements, while still keeping and internal call to the autoHideContainer decorator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you discuss this in #407 ? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, in the end, I agree that autoHideContainer is better :)

})
);

search.addWidget(
instantsearch.widgets.refinementList({
container: '#brands',
Expand Down
1 change: 1 addition & 0 deletions dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ <h1><a href="./">Instant search demo</a> <small>using instantsearch.js</small></

<div class="row search search--hidden">
<div class="col-md-3">
<div id="clear-all"></div>
<div class="facet" id="hierarchical-categories"></div>
<div class="facet" id="brands"></div>
<div class="facet" id="price-range"></div>
Expand Down
16 changes: 16 additions & 0 deletions docs/_includes/widget-jsdoc/clearAll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
| Param | Description |
| --- | --- |
| <span class='attr-required'>`options.container`</span> | CSS Selector or DOMElement to insert the widget |
| <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 |
| <span class='attr-optional'>`options.cssClasses.body`</span> | CSS class to add to the body element |
| <span class='attr-optional'>`options.cssClasses.footer`</span> | CSS class to add to the footer element |
| <span class='attr-optional'>`options.cssClasses.link`</span> | CSS class to add to the link element |
| <span class='attr-optional'>`options.templates`</span> | Templates to use for the widget |
| <span class='attr-optional'>`options.templates.header`</span> | Header template |
| <span class='attr-optional'>`options.templates.link`</span> | Link template |
| <span class='attr-optional'>`options.templates.footer`</span> | Footer template |
| <span class='attr-optional'>`options.autoHideContainer`</span> | Hide the container when there's no refinement to clear |

<p class="attr-legend">* <span>Required</span></p>
41 changes: 41 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,47 @@ instantsearch.widgets.priceRanges(options);

<div id="price-ranges" class="widget-container"></div>

#### clearAll

<div class="codebox-combo">

<img class="widget-icon pull-left" src="../img/icon-widget-clearall.svg">
This filtering widget lets the user choose between ranges of price. Those ranges are dynamically computed based on the returned results.
{:.description}

<div class="code-box">
<div class="code-sample-snippet">
{% highlight javascript %}
search.addWidget(
instantsearch.widgets.clearAll({
container: '#clear-all',
templates: {
link: 'Reset everything'
},
cssClasses: {
root: '',
header: '',
body: '',
footer: '',
link: '',
},
autoHideContainer: false
})
);
{% endhighlight %}
</div>
<div class="jsdoc" style='display:none'>
{% highlight javascript %}
instantsearch.widgets.clearAll(options);
{% endhighlight %}
{% include widget-jsdoc/clearAll.md %}
</div>
</div>

</div>

<div id="clear-all" class="widget-container"></div>

### Sort

#### indexSelector
Expand Down
1 change: 1 addition & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
130 changes: 130 additions & 0 deletions widgets/clear-all/__tests__/clear-all-test.js
Original file line number Diff line number Diff line change
@@ -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(<ClearAll props />, 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(<ClearAll {...getProps()} />);
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<ClearAll {...getProps()} />);
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(<ClearAll props />, 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(<ClearAll {...getProps()} />);
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<ClearAll {...getProps()} />);
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
});
});

afterEach(() => {
clearAll.__ResetDependency__('ReactDOM');
clearAll.__ResetDependency__('defaultTemplates');
});

function getProps(extraProps = {}) {
return {
...props,
...extraProps
};
}
});
Loading