Skip to content

Commit

Permalink
feat(priceRanges): Add BEM classes and tests
Browse files Browse the repository at this point in the history
Fixes #387

BREAKING CHANGE: `ais-price-ranges--range` are now named
`ais-price-ranges--item` and are wrapped in
a `ais-price-ranges--list`.

I've moved the bottom form into it's own PriceRangesForm component,
along with its own tests. I've fixed a minor typo where the component
was internally named PriceRange (without the final __s__).

I factorize some logic form the render in individual methods and
manage to individually test them. This was not an easy task. I had to
mock the default `render` (so it does nothing) before instanciating
the component. Then, I was able to call each inner method
individually. This requires to stub prototype methods in beforeEach,
then restore them in afterEach. I've added a few helper methods, this
can surely be simplified again but this gives nice granularity in
testing.

I've renamed the `range` items to `item` and wrapped them in a `list`.
I've also added classes to all elements we add (`label`, `separator`,
etc). I've removed the empty `span`s.
  • Loading branch information
pixelastic committed Nov 2, 2015
1 parent 09e8f7f commit ad58d7a
Show file tree
Hide file tree
Showing 12 changed files with 650 additions and 273 deletions.
82 changes: 70 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1047,20 +1047,27 @@ search.addWidget(
* Instantiate a price ranges on a numerical facet
* @param {string|DOMElement} options.container Valid CSS Selector as a string or DOMElement
* @param {string} options.facetName Name of the attribute for faceting
* @param {Object} [options.cssClasses] CSS classes to add to the wrapping elements: root, range
* @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.range] CSS class to add to the range element
* @param {string|string[]} [options.cssClasses.input] CSS class to add to the min/max input elements
* @param {string|string[]} [options.cssClasses.button] CSS class to add to the button element
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string} [options.cssClasses.root] CSS class to add to the root element
* @param {string} [options.cssClasses.header] CSS class to add to the header element
* @param {string} [options.cssClasses.body] CSS class to add to the body element
* @param {string} [options.cssClasses.list] CSS class to add to the wrapping list element
* @param {string} [options.cssClasses.item] CSS class to add to each item element
* @param {string} [options.cssClasses.active] CSS class to add to the active item element
* @param {string} [options.cssClasses.link] CSS class to add to each link element
* @param {string} [options.cssClasses.form] CSS class to add to the form element
* @param {string} [options.cssClasses.label] CSS class to add to each wrapping label of the form
* @param {string} [options.cssClasses.input] CSS class to add to each input of the form
* @param {string} [options.cssClasses.currency] CSS class to add to each currency element of the form
* @param {string} [options.cssClasses.separator] CSS class to add to the separator of the form
* @param {string} [options.cssClasses.button] CSS class to add to the submit button of the form
* @param {string} [options.cssClasses.footer] CSS class to add to the footer element
* @param {Object} [options.templates] Templates to use for the widget
* @param {string|Function} [options.templates.range] Range template
* @param {string|Function} [options.templates.item] Item template
* @param {Object} [options.labels] Labels to use for the widget
* @param {string|Function} [options.labels.button] Button label
* @param {string|Function} [options.labels.currency] Currency label
* @param {string|Function} [options.labels.to] To label
* @param {string|Function} [options.labels.separator] Separator labe, between min and max
* @param {string|Function} [options.labels.button] Button label
* @param {boolean} [hideContainerWhenNoResults=true] Hide the container when no results match
* @return {Object}
*/
Expand All @@ -1080,10 +1087,61 @@ search.addWidget(
#### Styling

```html

<div class="ais-price-ranges">
<div class="ais-price-ranges--header ais-header">Header</div>
<div class="ais-price-ranges--body">
<div class="ais-price-ranges--item">
<a class="ais-price-ranges--range" href="...">$3 - $13</a>
</div>
<div class="ais-price-ranges--item ais-price-ranges--item__active">
<a class="ais-price-ranges--range" href="...">$13 - $40</a>
</div>
<form class="ais-price-ranges--form">
<label class="ais-price-ranges--label">
<span class="ais-price-ranges--currency>$ </span>
<input class="ais-price-ranges--input" />
</label>
<span class="ais-price-ranges--separator"> to </span>
<label class="ais-price-ranges--label">
<span class="ais-price-ranges--currency>$ </span>
<input class="ais-price-ranges--input" />
</label>
<button class="ais-price-ranges--button">Go</button>
</form>
</div>
<div class="ais-price-ranges--footer ais-footer">Footer</div>
</div>
```

```css
.ais-price-ranges {
}
.ais-price-ranges--header {
}
.ais-price-ranges--body {
}
.ais-price-ranges--list {
}
.ais-price-ranges--item {
}
.ais-price-ranges--item__active {
}
.ais-price-ranges--link {
}
.ais-price-ranges--form {
}
.ais-price-ranges--label {
}
.ais-price-ranges--currency {
}
.ais-price-ranges--input {
}
.ais-price-ranges--separator {
}
.ais-price-ranges--button {
}
.ais-price-ranges--footer {
}

```

Expand Down
83 changes: 0 additions & 83 deletions components/PriceRanges.js

This file was deleted.

97 changes: 97 additions & 0 deletions components/PriceRanges/PriceRanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
var React = require('react');

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

class PriceRanges extends React.Component {
getForm() {
return (
<PriceRangesForm
cssClasses={this.props.cssClasses}
labels={this.props.labels}
refine={this.refine.bind(this)}
/>
);
}

getURLFromFacetValue(facetValue) {
if (!this.props.createURL) {
return '#';
}
return this.props.createURL(facetValue.from, facetValue.to, facetValue.isRefined);
}

getItemFromFacetValue(facetValue) {
let cssClassItem = cx(
this.props.cssClasses.item,
{[this.props.cssClasses.active]: facetValue.isRefined}
);
let url = this.getURLFromFacetValue(facetValue);
let key = facetValue.from + '_' + facetValue.to;
let handleClick = this.refine.bind(this, facetValue.from, facetValue.to);
return (
<div className={cssClassItem} key={key}>
<a
className={this.props.cssClasses.link}
href={url}
onClick={handleClick}
>
<Template data={facetValue} templateKey="item" {...this.props.templateProps} />
</a>
</div>
);
}

refine(from, to, event) {
event.preventDefault();
this.setState({
formFromValue: null,
formToValue: null
});
this.props.refine(from, to);
}

render() {
let form = this.getForm();
return (
<div>
<div className={this.props.cssClasses.list}>
{this.props.facetValues.map(facetValue => {
return this.getItemFromFacetValue(facetValue);
})}
</div>
{form}
</div>
);
}
}

PriceRanges.propTypes = {
createURL: React.PropTypes.func.isRequired,
cssClasses: React.PropTypes.shape({
active: React.PropTypes.string,
button: React.PropTypes.string,
form: React.PropTypes.string,
input: React.PropTypes.string,
item: React.PropTypes.string,
label: React.PropTypes.string,
link: React.PropTypes.string,
list: React.PropTypes.string,
separator: 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
};

PriceRanges.defaultProps = {
cssClasses: {}
};

module.exports = PriceRanges;
59 changes: 59 additions & 0 deletions components/PriceRanges/PriceRangesForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
var React = require('react');

class PriceRangesForm extends React.Component {
getInput(type) {
return (
<label className={this.props.cssClasses.label}>
<span className={this.props.cssClasses.currency}>{this.props.labels.currency} </span>
<input className={this.props.cssClasses.input} ref={type} type="number" />
</label>
);
}

handleSubmit(event) {
let from = +this.refs.from.value || undefined;
let to = +this.refs.to.value || undefined;
this.props.refine(from, to, event);
}

render() {
let fromInput = this.getInput('from');
let toInput = this.getInput('to');
let onSubmit = this.handleSubmit.bind(this);
return (
<form className={this.props.cssClasses.form} onSubmit={onSubmit} ref="form">
{fromInput}
<span className={this.props.cssClasses.separator}> {this.props.labels.separator} </span>
{toInput}
<button className={this.props.cssClasses.button} type="submit">{this.props.labels.button}</button>
</form>
);
}
}

PriceRangesForm.propTypes = {
cssClasses: React.PropTypes.shape({
button: React.PropTypes.string,
currency: React.PropTypes.string,
form: React.PropTypes.string,
input: React.PropTypes.string,
label: React.PropTypes.string,
separator: React.PropTypes.string
}),
labels: React.PropTypes.shape({
button: React.PropTypes.string,
currency: React.PropTypes.string,
separator: React.PropTypes.string
}),
refine: React.PropTypes.func.isRequired
};


PriceRangesForm.defaultProps = {
cssClasses: {},
labels: {}
};


module.exports = PriceRangesForm;

Loading

0 comments on commit ad58d7a

Please sign in to comment.