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(priceRanges): Add BEM classes and tests #390

Merged
merged 1 commit into from
Nov 2, 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
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}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not directly call the <Form> instead of calling a function? Not sure this is a common pattern in React community.

The <Form> call is only 5 LOC and it would be ovious to read the render() then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal was to be able to:

  • Unit test the getForm method separatly
  • Mock the getForm when testing render

This lets me focus on one thing in each test. In the end I think I did not unit test getForm (my bad, I might have forgotten).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add the missing test and rebase

Copy link
Contributor

Choose a reason for hiding this comment

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

The goal was to be able to:

Unit test the getForm method separatly
Mock the getForm when testing render
This lets me focus on one thing in each test. In the end I think I did not unit test getForm (my bad, I might have forgotten).

I think we should try to avoid mocking when we can, you can always store the expected <Form> in a variable in your test rather than modifying the implementation to make it easily testable.

The least mocking we have in testing render() the better?

Copy link
Contributor

Choose a reason for hiding this comment

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

Small detail in the end, if you test getForm() that's ok

Copy link
Contributor

Choose a reason for hiding this comment

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

This is maybe due to the fact that most components we always only test the render() method even if that's some boilerplate to add. This way we can just refactor the whole component really easily.

In the end testing a React component output is just asserting some JSX on render(), the underlying implementation should not have to be tested when we can avoid it (sometimes for handlers we need to test it).

</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>
Copy link
Contributor

Choose a reason for hiding this comment

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

The only issue with that code is that there won't be any space between the toInput and the button, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, but why do you need one? This should be handled by CSS

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm happy with that; it's the same for the checkboxes in the refinementList widget anyway.

</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