Skip to content

Commit

Permalink
feat(clearAll): New widget
Browse files Browse the repository at this point in the history
- ClearAll: Component + tests
- clearAll: Widget + tests
- Documentation
- Default style
  • Loading branch information
Jerska committed Nov 12, 2015
1 parent ce2ae74 commit 9e61a14
Show file tree
Hide file tree
Showing 12 changed files with 437 additions and 0 deletions.
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
})
);

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

0 comments on commit 9e61a14

Please sign in to comment.