Skip to content

Commit

Permalink
feat(hits): Add BEM styling to the hits widget
Browse files Browse the repository at this point in the history
- Add BEM classes to the hit list and each hit element
- Add one when the list is empty
- Renamed the key from `hit` to `item` in CSS class, template and
  transformData to stay consistent with other widgets
- Updated the doc accordingly, with styling examples
- Updated tests
- Updated the bemHelper to be able to use a modifier without an
  element (`ais-hits__empty`)

BREAKING CHANGE: The hit template and transform data key is renamed
from `hit` to `item` to stay consistent with the other widgets
  • Loading branch information
pixelastic committed Oct 21, 2015
1 parent 38ae082 commit 6681960
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 44 deletions.
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,14 +533,16 @@ search.addWidget(
/**
* Display the list of results (hits) from the current search
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {Object} [options.cssClasses] CSS classes to add
* @param {String} [options.cssClasses.root] CSS class to add to the wrapping element
* @param {String} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
* @param {String} [options.cssClasses.item] CSS class to add to each result
* @param {Object} [options.templates] Templates to use for the widget
* @param {String|Function} [options.templates.empty=''] Template to use when there are no results.
* Gets passed the `result` from the API call.
* @param {String|Function} [options.templates.hit=''] Template to use for each result.
* Gets passed the `hit` of the result.
* @param {String|Function} [options.templates.item=''] Template to use for each result.
* @param {Object} [options.transformData] Method to change the object passed to the templates
* @param {Function} [options.transformData.empty=''] Method used to change the object passed to the empty template
* @param {Function} [options.transformData.hit=''] Method used to change the object passed to the hit template
* @param {Function} [options.transformData.item=''] Method used to change the object passed to the item template
* @param {Number} [hitsPerPage=20] The number of hits to display per page
* @return {Object}
*/
Expand All @@ -558,10 +560,10 @@ search.addWidget(
container: '#hits',
templates: {
empty: 'No results'
hit: '<div><strong>{{name}}</strong> {{price}}</div>'
item: '<div><strong>{{name}}</strong> {{price}}</div>'
},
transformData: {
hit: function(data) {
item: function(data) {
data.price = data.price + '$';
return data;
}
Expand All @@ -571,6 +573,29 @@ search.addWidget(
);
```

### Styling

```html
<div class="ais-hits">
<div class="ais-hits--item">Hit content</div>
...
<div class="ais-hits--item">Hit content</div>
</div>
<!-- If no results -->
<div class="ais-hits ais-hits__empty">
No results
</div>
```

```css
.ais-hits {
}
.ais-hits--item {
}
.ais-hits__empty {
}
```

### toggle

![Example of the toggle widget][toggle]
Expand Down
21 changes: 12 additions & 9 deletions components/Hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ var Template = require('./Template');

class Hits extends React.Component {
renderWithResults() {
var renderedHits = map(this.props.results.hits, (hit) => {
var renderedHits = map(this.props.results.hits, hit => {
return (
<Template
data={hit}
key={hit.objectID}
templateKey="hit"
{...this.props.templateProps}
/>
<div className={this.props.cssClasses.item}>
<Template
data={hit}
key={hit.objectID}
templateKey="item"
{...this.props.templateProps}
/>
</div>
);
});

return <div>{renderedHits}</div>;
return <div className={this.props.cssClasses.root}>{renderedHits}</div>;
}

renderNoResults() {
var className = [this.props.cssClasses.root, this.props.cssClasses.empty].join(' ');
return (
<div>
<div className={className}>
<Template
data={this.props.results}
templateKey="empty"
Expand Down
48 changes: 34 additions & 14 deletions components/__tests__/Hits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,55 @@ describe('Hits', () => {
objectID: 'mom'
}]};

let props = {results, templateProps};
let props = {
results,
templateProps,
cssClasses: {
root: 'custom-root',
item: 'custom-item',
empty: 'custom-empty'
}
};
renderer.render(<Hits {...props} />);
let out = renderer.getRenderOutput();

expect(out).toEqualJSX(
<div>
<Template
data={results.hits[0]}
key={results.hits[0].objectID}
templateKey="hit"
/>
<Template
data={results.hits[1]}
key={results.hits[1].objectID}
templateKey="hit"
/>
<div className="custom-root">
<div className="custom-item">
<Template
data={results.hits[0]}
key={results.hits[0].objectID}
templateKey="item"
/>
</div>
<div className="custom-item">
<Template
data={results.hits[1]}
key={results.hits[1].objectID}
templateKey="item"
/>
</div>
</div>
);
});

it('renders a specific template when no results', () => {
results = {hits: []};

let props = {results, templateProps};
let props = {
results,
templateProps,
cssClasses: {
root: 'custom-root',
item: 'custom-item',
empty: 'custom-empty'
}
};
renderer.render(<Hits {...props} />);
let out = renderer.getRenderOutput();

expect(out).toEqualJSX(
<div>
<div className="custom-root custom-empty">
<Template
data={results}
templateKey="empty"
Expand Down
2 changes: 1 addition & 1 deletion example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ search.addWidget(
container: '#hits',
templates: {
empty: require('./templates/no-results.html'),
hit: require('./templates/hit.html')
item: require('./templates/item.html')
},
hitsPerPage: 6
})
Expand Down
File renamed without changes.
17 changes: 13 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,22 @@ function isSpecialClick(event) {

function bemHelper(block) {
return function(element, modifier) {
if (!element) {
// block
if (!element && !modifier) {
return block;
}
if (!modifier) {
return block + '--' + element;
// block--element
if (element && !modifier) {
return `${block}--${element}`;
}
// block--element__modifier
if (element && modifier) {
return `${block}--${element}__${modifier}`;
}
// block__modifier
if (!element && modifier) {
return `${block}__${modifier}`;
}
return block + '--' + element + '__' + modifier;
};
}

Expand Down
6 changes: 6 additions & 0 deletions themes/default/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
}

/* HITS */
.ais-hits {
}
.ais-hits--item {
}
.ais-hits__empty {
}

/* PAGINATION */
.ais-pagination {
Expand Down
6 changes: 3 additions & 3 deletions widgets/hits/__tests__/defaultTemplates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('hits defaultTemplates', () => {
expect(defaultTemplates.empty).toBe('No results');
});

it('has a `hit` default template', () => {
let hit = {
it('has a `item` default template', () => {
let item = {
hello: 'there,',
how: {
are: 'you?'
Expand All @@ -25,6 +25,6 @@ describe('hits defaultTemplates', () => {
}
}`;

expect(defaultTemplates.hit(hit)).toBe(expected);
expect(defaultTemplates.item(item)).toBe(expected);
});
});
11 changes: 10 additions & 1 deletion widgets/hits/__tests__/hits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ describe('hits()', () => {
});

function getProps() {
return {hits: results.hits, results, templateProps};
return {
hits: results.hits,
results,
templateProps,
cssClasses: {
root: 'ais-hits',
item: 'ais-hits--item',
empty: 'ais-hits__empty'
}
};
}
});
2 changes: 1 addition & 1 deletion widgets/hits/defaultTemplates.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
empty: 'No results',
hit: function(data) {
item: function(data) {
return JSON.stringify(data, null, 2);
}
};
22 changes: 17 additions & 5 deletions widgets/hits/hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,48 @@ var React = require('react');
var ReactDOM = require('react-dom');

var utils = require('../../lib/utils.js');
var bem = utils.bemHelper('ais-hits');
var cx = require('classnames/dedupe');

var Hits = require('../../components/Hits');
var defaultTemplates = require('./defaultTemplates');

/**
* Display the list of results (hits) from the current search
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {Object} [options.cssClasses] CSS classes to add
* @param {String} [options.cssClasses.root] CSS class to add to the wrapping element
* @param {String} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
* @param {String} [options.cssClasses.item] CSS class to add to each result
* @param {Object} [options.templates] Templates to use for the widget
* @param {String|Function} [options.templates.empty=''] Template to use when there are no results.
* Gets passed the `result` from the API call.
* @param {String|Function} [options.templates.hit=''] Template to use for each result.
* Gets passed the `hit` of the result.
* @param {String|Function} [options.templates.item=''] Template to use for each result.
* @param {Object} [options.transformData] Method to change the object passed to the templates
* @param {Function} [options.transformData.empty=''] Method used to change the object passed to the empty template
* @param {Function} [options.transformData.hit=''] Method used to change the object passed to the hit template
* @param {Function} [options.transformData.item=''] Method used to change the object passed to the item template
* @param {Number} [hitsPerPage=20] The number of hits to display per page
* @return {Object}
*/
function hits({
container,
cssClasses = {},
templates = defaultTemplates,
transformData,
hitsPerPage = 20
}) {
var containerNode = utils.getContainerNode(container);
var usage = 'Usage: hits({container, [templates.{empty,hit}, transformData.{empty,hit}, hitsPerPage])';
var usage = 'Usage: hits({container, [cssClasses.{root,empty,item}, templates.{empty,item}, transformData.{empty,item}, hitsPerPage])';

if (container === null) {
throw new Error(usage);
}

cssClasses = {
root: cx(bem(null), cssClasses.root),
item: cx(bem('item'), cssClasses.item),
empty: cx(bem(null, 'empty'), cssClasses.empty)
};

return {
getConfiguration: () => ({hitsPerPage}),
render: function({results, templatesConfig}) {
Expand All @@ -45,6 +56,7 @@ function hits({

ReactDOM.render(
<Hits
cssClasses={cssClasses}
hits={results.hits}
results={results}
templateProps={templateProps}
Expand Down

0 comments on commit 6681960

Please sign in to comment.