Skip to content

Commit

Permalink
feat(numericRefinementList): create numericRefinementList widget usin…
Browse files Browse the repository at this point in the history
…g refinementList component
  • Loading branch information
maxiloc committed Nov 4, 2015
1 parent 848eec1 commit a29e9c7
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 5 deletions.
12 changes: 10 additions & 2 deletions components/RefinementList/RefinementList.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ class RefinementList extends React.Component {
[this.props.cssClasses.active]: facetValue.isRefined
});

let key = facetValue[this.props.facetNameKey] + '/' + facetValue.isRefined + '/' + facetValue.count;
let key = facetValue[this.props.facetNameKey];
if (facetValue.isRefined !== undefined) {
key += '/' + facetValue.isRefined;
}

if (facetValue.count !== undefined) {
key += '/' + facetValue.count;
}
return (
<div
className={cssClassItem}
Expand Down Expand Up @@ -79,7 +86,8 @@ class RefinementList extends React.Component {
let parent = e.target;

while (parent !== e.currentTarget) {
if (parent.tagName === 'LABEL' && parent.querySelector('input[type="checkbox"]')) {
if (parent.tagName === 'LABEL' && (parent.querySelector('input[type="checkbox"]')
|| parent.querySelector('input[type="radio"]'))) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions components/RefinementList/__tests__/RefinementList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ describe('RefinementList', () => {
</div>
</div>
);
expect(out.props.children[0].key).toEqual('facet1/undefined/undefined');
expect(out.props.children[1].key).toEqual('facet2/undefined/undefined');
expect(out.props.children[0].key).toEqual('facet1');
expect(out.props.children[1].key).toEqual('facet2');
});

it('should render default list highlighted', () => {
Expand Down
42 changes: 42 additions & 0 deletions dev/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ search.addWidget(
})
);

search.addWidget(
instantsearch.widgets.numericRefinementList({
container: '#price-numeric-list',
attributeName: 'price',
operator: 'or',
options: [
{name: 'All'},
{end: 4, name: 'less than 4'},
{start: 4, end: 4, name: '4'},
{start: 5, end: 10, name: 'between 5 and 10'},
{start: 10, name: 'more than 10'}
],
cssClasses: {
header: 'facet-title',
link: 'facet-value',
count: 'facet-count pull-right',
active: 'facet-active'
},
templates: {
header: 'Price numeric list'
}
})
);

search.addWidget(
instantsearch.widgets.refinementList({
container: '#price-range',
Expand Down Expand Up @@ -187,4 +211,22 @@ search.once('render', function() {
document.querySelector('.search').className = 'row search search--visible';
});

search.addWidget(
instantsearch.widgets.priceRanges({
container: '#price-ranges',
attributeName: 'price',
templates: {
header: 'Price ranges'
},
cssClasses: {
header: 'facet-title',
body: 'nav nav-stacked',
range: 'facet-value',
form: '',
input: 'fixed-input-sm',
button: 'btn btn-default btn-sm'
}
})
);

search.start();
1 change: 1 addition & 0 deletions dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1><a href="./">Instant search demo</a> <small>using instantsearch.js</small></
<div class="facet" id="price"></div>
<div class="facet" id="categories"></div>
<div class="facet" id="price-ranges"></div>
<div class="facet" id="price-numeric-list"></div>
</div>
<div class="col-md-9">
<div class="form-group">
Expand Down
4 changes: 4 additions & 0 deletions dev/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ body {
.ais-price-ranges .ais-price-ranges--input{
width: 65px;
}

.ais-refinement-list--label input[type=radio]{
margin: 4px 8px 0 0;
}
22 changes: 22 additions & 0 deletions docs/_includes/widget-jsdoc/numericRefinementList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
| Param | Description |
| --- | --- |
| <span class='attr-required'>`options.container`</span> | CSS Selector or DOMElement to insert the widget |
| <span class='attr-required'>`options.attributeName`</span> | Name of the attribute for filtering |
| <span class='attr-optional'>`options.cssClasses`</span> | CSS classes to add to the wrapping elements: root, list, item |
| <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.list`</span> | CSS class to add to the list element |
| <span class='attr-optional'>`options.cssClasses.label`</span> | CSS class to add to each link element |
| <span class='attr-optional'>`options.cssClasses.item`</span> | CSS class to add to each item element |
| <span class='attr-optional'>`options.cssClasses.radio`</span> | CSS class to add to each radio element (when using the default template) |
| <span class='attr-optional'>`options.cssClasses.active`</span> | CSS class to add to each active 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.item`</span> | Item template, provided with `name`, `count`, `isRefined` |
| <span class='attr-optional'>`options.templates.footer`</span> | Footer template |
| <span class='attr-optional'>`options.transformData`</span> | Function to change the object passed to the item template |
| <span class='attr-optional'>`hideContainerWhenNoResults`</span> | Hide the container when there's no results |

<p class="attr-legend">* <span>Required</span></p>
47 changes: 47 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,53 @@ results that have either the value `a` or `b` will match.

<div id="brands" class="widget-container"></div>

#### numericRefinementList

<div class="code-box">
<div class="code-sample-snippet">
{% highlight javascript %}
search.addWidget(
instantsearch.widgets.numericRefinementList({
container: '#popularity',
attributeName: 'popularity',
options: [
{name: 'All'},
{end: 4, name: 'less than 4'},
{start: 4, end: 4, name: '4'},
{start: 5, end: 10, name: 'between 5 and 10'},
{start: 10, name: 'more than 10'}
],
templates: {
header: 'Price'
},
cssClasses: {
root: '',
header: '',
body: '',
footer: '',
list: '',
link: '',
active: ''
}
})
);
{% endhighlight %}
</div>
<div class="jsdoc" style='display:none'>
{% highlight javascript %}
instantsearch.widgets.numericRefinementList(options);
{% endhighlight %}

{% include widget-jsdoc/refinementList.md %}
</div>
</div>

<img class="widget-icon pull-left" src="../img/icon-widget-refinement.svg">
This filtering widget lets the user choose one value for a single numeric attribute. You can specify if you want it to be a equality or a range by giving a "start" and an "end" value
{:.description}

<div id="popularity" class="widget-container"></div>

#### toggle

<div class="code-box">
Expand Down
1 change: 1 addition & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ instantsearch.widgets = {
indexSelector: require('../widgets/index-selector/index-selector'),
menu: require('../widgets/menu/menu.js'),
refinementList: require('../widgets/refinement-list/refinement-list.js'),
numericRefinementList: require('../widgets/numeric-refinement-list/numeric-refinement-list.js'),
pagination: require('../widgets/pagination/pagination'),
priceRanges: require('../widgets/price-ranges/price-ranges.js'),
searchBox: require('../widgets/search-box/search-box'),
Expand Down
2 changes: 1 addition & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* 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);

describe('numericRefinementList()', () => {
jsdom({useEach: true});

let ReactDOM;
let container;
let widget;
let helper;

let autoHideContainer;
let headerFooter;
let RefinementList;
let numericRefinementList;
let options;

beforeEach(() => {
numericRefinementList = require('../numeric-refinement-list');
RefinementList = require('../../../components/RefinementList/RefinementList');
ReactDOM = {render: sinon.spy()};
numericRefinementList.__Rewire__('ReactDOM', ReactDOM);
autoHideContainer = sinon.stub().returns(RefinementList);
numericRefinementList.__Rewire__('autoHideContainer', autoHideContainer);
headerFooter = sinon.stub().returns(RefinementList);
numericRefinementList.__Rewire__('headerFooter', headerFooter);

options = [
{name: 'All'},
{end: 4, name: 'less than 4'},
{start: 4, end: 4, name: '4'},
{start: 5, end: 10, name: 'between 5 and 10'},
{start: 10, name: 'more than 10'}
];

container = document.createElement('div');
widget = numericRefinementList({container, attributeName: 'price', options: options});
helper = {
state: {
getNumericRefinements: sinon.stub().returns([])
},
addNumericRefinement: sinon.spy(),
search: sinon.spy(),
setState: sinon.spy()
};

helper.state.clearRefinements = sinon.stub().returns(helper.state);
helper.state.addNumericRefinement = sinon.stub().returns(helper.state);
});

it('calls twice ReactDOM.render(<RefinementList props />, container)', () => {
widget.render({helper});
widget.render({helper});

let props = {
cssClasses: {
active: 'ais-refinement-list--item__active',
body: 'ais-refinement-list--body',
footer: 'ais-refinement-list--footer',
header: 'ais-refinement-list--header',
item: 'ais-refinement-list--item',
label: 'ais-refinement-list--label',
list: 'ais-refinement-list--list',
radio: 'ais-refinement-list--radio',
root: 'ais-refinement-list'
},
facetValues: [
{attributeName: 'price', isRefined: true, name: 'All'},
{attributeName: 'price', end: 4, isRefined: false, name: 'less than 4'},
{attributeName: 'price', end: 4, isRefined: false, name: '4', start: 4},
{attributeName: 'price', end: 10, isRefined: false, name: 'between 5 and 10', start: 5},
{attributeName: 'price', isRefined: false, name: 'more than 10', start: 10}
],
createURL: () => {},
toggleRefinement: () => {},
shouldAutoHideContainer: false,
templateProps: {
templates: {footer: '', header: '', item: '<label class="{{cssClasses.label}}">\n <input type="radio" class="{{cssClasses.checkbox}}" name="{{attributeName}}" {{#isRefined}}checked{{/isRefined}} />{{name}}\n</label>'},
templatesConfig: undefined,
transformData: undefined,
useCustomCompileOptions: {
footer: false,
header: false,
item: false
}
}
};

expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<RefinementList {...props} />);
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<RefinementList {...props} />);
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
});

it('doesn\'t call the refinement functions if not refined', () => {
widget.render({helper});
expect(helper.state.clearRefinements.called).toBe(false, 'clearRefinements called one');
expect(helper.state.addNumericRefinement.called).toBe(false, 'addNumericRefinement never called');
expect(helper.search.called).toBe(false, 'search never called');
});

it('calls the refinement functions if refined with "4"', () => {
widget._toggleRefinement(helper, '4');
expect(helper.state.clearRefinements.calledOnce).toBe(true, 'clearRefinements called once');
expect(helper.state.addNumericRefinement.calledOnce).toBe(true, 'addNumericRefinement called once');
expect(helper.state.addNumericRefinement.getCall(0).args).toEqual(['price', '=', 4]);
expect(helper.search.calledOnce).toBe(true, 'search called once');
});

it('calls the refinement functions if refined with "between 5 and 10"', () => {
widget._toggleRefinement(helper, 'between 5 and 10');
expect(helper.state.clearRefinements.calledOnce).toBe(true, 'clearRefinements called once');
expect(helper.state.addNumericRefinement.calledTwice).toBe(true, 'addNumericRefinement called twice');
expect(helper.state.addNumericRefinement.getCall(0).args).toEqual(['price', '>=', 5]);
expect(helper.state.addNumericRefinement.getCall(1).args).toEqual(['price', '<=', 10]);
expect(helper.search.calledOnce).toBe(true, 'search called once');
});

it('calls two times the refinement functions if refined with "less than 4"', () => {
widget._toggleRefinement(helper, 'less than 4');
expect(helper.state.clearRefinements.calledOnce).toBe(true, 'clearRefinements called once');
expect(helper.state.addNumericRefinement.calledOnce).toBe(true, 'addNumericRefinement called once');
expect(helper.state.addNumericRefinement.getCall(0).args).toEqual(['price', '<=', 4]);
expect(helper.search.calledOnce).toBe(true, 'search called once');
});

it('calls two times the refinement functions if refined with "more than 10"', () => {
widget._toggleRefinement(helper, 'more than 10');
expect(helper.state.clearRefinements.calledOnce).toBe(true, 'clearRefinements called once');
expect(helper.state.addNumericRefinement.calledOnce).toBe(true, 'addNumericRefinement called once');
expect(helper.state.addNumericRefinement.getCall(0).args).toEqual(['price', '>=', 10]);
expect(helper.search.calledOnce).toBe(true, 'search called once');
});

afterEach(() => {
numericRefinementList.__ResetDependency__('ReactDOM');
numericRefinementList.__ResetDependency__('autoHideContainer');
numericRefinementList.__ResetDependency__('headerFooter');
});
});
7 changes: 7 additions & 0 deletions widgets/numeric-refinement-list/defaultTemplates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
header: '',
item: `<label class="{{cssClasses.label}}">
<input type="radio" class="{{cssClasses.checkbox}}" name="{{attributeName}}" {{#isRefined}}checked{{/isRefined}} />{{name}}
</label>`,
footer: ''
};
Loading

0 comments on commit a29e9c7

Please sign in to comment.