Skip to content

Commit

Permalink
feat(priceRanges): new Amazon-style price ranges widget
Browse files Browse the repository at this point in the history
  • Loading branch information
redox authored and vvo committed Oct 21, 2015
1 parent 873f503 commit e5fe344
Show file tree
Hide file tree
Showing 12 changed files with 575 additions and 0 deletions.
81 changes: 81 additions & 0 deletions components/PriceRanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
var React = require('react');

var Template = require('./Template');
var cx = require('classnames');

class PriceRange extends React.Component {
refine(from, to, event) {
event.preventDefault();
this.refs.from.value = this.refs.to.value = '';
this.props.refine(from, to);
}

render() {
return (
<div className={this.props.cssClasses.root}>
{this.props.facetValues.map(facetValue => {
var key = facetValue.from + '_' + facetValue.to;
return (
<a
className={cx(this.props.cssClasses.range, {active: facetValue.isRefined})}
href="#"
key={key}
onClick={this.refine.bind(this, facetValue.from, facetValue.to)}
>
<Template data={facetValue} templateKey="range" {...this.props.templateProps} />
</a>
);
})}
<div className={this.props.cssClasses.inputGroup}>
<label>
{this.props.labels.currency}{' '}
<input className={this.props.cssClasses.input} ref="from" type="number" />
</label>
{' '}{this.props.labels.to}{' '}
<label>
{this.props.labels.currency}{' '}
<input className={this.props.cssClasses.input} ref="to" type="number" />
</label>
{' '}
<button
className={this.props.cssClasses.button}
onClick={(e) => {
this.refine(+this.refs.from.value || undefined, +this.refs.to.value || undefined, e);
}}
>{this.props.labels.button}</button>
</div>
</div>
);
}
}

PriceRange.propTypes = {
cssClasses: React.PropTypes.shape({
root: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
range: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
input: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
button: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
])
}),
facetValues: React.PropTypes.array,
labels: React.PropTypes.shape({
button: React.PropTypes.string,
currency: React.PropTypes.string,
to: React.PropTypes.string
}),
refine: React.PropTypes.func.isRequired,
templateProps: React.PropTypes.object.isRequired
};

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

import React from 'react';
import expect from 'expect';
import TestUtils from 'react-addons-test-utils';
import PriceRanges from '../PriceRanges';
import generateRanges from '../../widgets/price-ranges/generate-ranges.js';

describe('PriceRanges', () => {
var renderer;

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

context('with stats', () => {
var out;
var facetValues;

beforeEach(() => {
facetValues = generateRanges({
min: 1.99,
max: 4999.98,
avg: 243.349,
sum: 2433490.0
});

var props = {
templateProps: {},
facetValues,
cssClasses: {
root: 'root-class',
range: 'range-class',
inputGroup: 'input-group-class',
button: 'button-class',
input: 'input-class'
},
labels: {
currency: 'USD',
to: 'to',
button: 'Go'
},
refine: () => {}
};

renderer.render(<PriceRanges {...props} />);
out = renderer.getRenderOutput();
});

it('should add the root class', () => {
expect(out.type).toBe('div');
expect(out.props.className).toEqual('root-class');
});

it('should have the right number of children', () => {
expect(out.props.children.length).toEqual(2);
expect(out.props.children[0].length).toEqual(facetValues.length);
});

it('should have the range class', () => {
out.props.children[0].forEach((c) => {
expect(c.props.className).toEqual('range-class');
});
});

it('should have the input group class', () => {
expect(out.props.children.length).toEqual(2);
expect(out.props.children[1].props.className).toEqual('input-group-class');
});

it('should display the inputs with the associated class & labels', () => {
expect(out.props.children.length).toEqual(2);
var click = out.props.children[1].props.children[6].props.onClick;
expect(out.props.children[1]).toEqual(
<div className="input-group-class">
<label>
USD{' '}<input className="input-class" ref="from" type="number" />
</label>
{' '}to{' '}
<label>
USD{' '}<input className="input-class" ref="to" type="number" />
</label>
{' '}
<button className="button-class" onClick={click}>Go</button>
</div>
);
});
});
});
16 changes: 16 additions & 0 deletions example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,24 @@ search.addWidget(
})
);


search.once('render', function() {
document.querySelector('.search').className = 'row search search--visible';
});

search.addWidget(
instantsearch.widgets.priceRanges({
container: '#price_ranges',
facetName: 'price',
cssClasses: {
root: 'nav nav-stacked',
range: 'list-group-item',
inputGroup: 'list-group-item form-inline',
input: 'form-control input-sm fixed-input-sm',
button: 'btn btn-default btn-sm'
},
template: require('./templates/price_range.html')
})
);

search.start();
5 changes: 5 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>

<div class="panel panel-default" id="hierarchical-categories"></div>

<div class="panel panel-default">
<div class="panel-heading">Price</div>
<div id="price_ranges" class="list-group"></div>
</div>

</div>
<div class="col-md-9">
<div class="form-group">
Expand Down
4 changes: 4 additions & 0 deletions example/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ body {
.ais-toggle--label {
display: block;
}

.fixed-input-sm {
width: 65px !important;
}
18 changes: 18 additions & 0 deletions example/templates/price_range.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<a href="#" class="list-group-item{{#isRefined}} active{{/isRefined}}">
{{#from}}
{{^to}}
&gt;
{{/to}}
${{from}}
{{/from}}
{{#to}}
{{#from}}
-
{{/from}}
{{^from}}
&lt;
{{/from}}
${{to}}
{{/to}}
<span>{{count}}</span>
</a>
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ instantsearch.widgets = {
menu: require('./widgets/menu/menu.js'),
refinementList: require('./widgets/refinement-list/refinement-list.js'),
pagination: require('./widgets/pagination/pagination'),
priceRanges: require('./widgets/price-ranges/price-ranges.js'),
searchBox: require('./widgets/search-box'),
rangeSlider: require('./widgets/range-slider/range-slider'),
stats: require('./widgets/stats/stats'),
Expand Down
47 changes: 47 additions & 0 deletions widgets/price-ranges/__tests__/generate-ranges-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-env mocha */

import React from 'react';
import expect from 'expect';

import generateRanges from '../generate-ranges';

describe('generateRanges()', () => {
it('should generate ranges', () => {
var stats = {
min: 1.99,
max: 4999.98,
avg: 243.349,
sum: 2433490.0
};
var expected = [
{to: 1},
{from: 1, to: 80},
{from: 80, to: 160},
{from: 160, to: 240},
{from: 240, to: 1820},
{from: 1820, to: 3400},
{from: 3400, to: 4980},
{from: 4980}
];
expect(generateRanges(stats)).toEqual(expected);
});

it('should generate small ranges', () => {
var stats = {min: 20, max: 50, avg: 35, sum: 70};
var expected = [
{to: 20},
{from: 20, to: 25},
{from: 25, to: 30},
{from: 30, to: 35},
{from: 35, to: 40},
{from: 40, to: 45},
{from: 45}
];
expect(generateRanges(stats)).toEqual(expected);
});

it('should not generate ranges', () => {
var stats = {min: 20, max: 20, avg: 20, sum: 20};
expect(generateRanges(stats)).toEqual([]);
});
});
Loading

0 comments on commit e5fe344

Please sign in to comment.