Skip to content

Commit 6681960

Browse files
committed
feat(hits): Add BEM styling to the hits widget
- 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
1 parent 38ae082 commit 6681960

File tree

11 files changed

+128
-44
lines changed

11 files changed

+128
-44
lines changed

README.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,16 @@ search.addWidget(
533533
/**
534534
* Display the list of results (hits) from the current search
535535
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
536+
* @param {Object} [options.cssClasses] CSS classes to add
537+
* @param {String} [options.cssClasses.root] CSS class to add to the wrapping element
538+
* @param {String} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
539+
* @param {String} [options.cssClasses.item] CSS class to add to each result
536540
* @param {Object} [options.templates] Templates to use for the widget
537541
* @param {String|Function} [options.templates.empty=''] Template to use when there are no results.
538-
* Gets passed the `result` from the API call.
539-
* @param {String|Function} [options.templates.hit=''] Template to use for each result.
540-
* Gets passed the `hit` of the result.
542+
* @param {String|Function} [options.templates.item=''] Template to use for each result.
541543
* @param {Object} [options.transformData] Method to change the object passed to the templates
542544
* @param {Function} [options.transformData.empty=''] Method used to change the object passed to the empty template
543-
* @param {Function} [options.transformData.hit=''] Method used to change the object passed to the hit template
545+
* @param {Function} [options.transformData.item=''] Method used to change the object passed to the item template
544546
* @param {Number} [hitsPerPage=20] The number of hits to display per page
545547
* @return {Object}
546548
*/
@@ -558,10 +560,10 @@ search.addWidget(
558560
container: '#hits',
559561
templates: {
560562
empty: 'No results'
561-
hit: '<div><strong>{{name}}</strong> {{price}}</div>'
563+
item: '<div><strong>{{name}}</strong> {{price}}</div>'
562564
},
563565
transformData: {
564-
hit: function(data) {
566+
item: function(data) {
565567
data.price = data.price + '$';
566568
return data;
567569
}
@@ -571,6 +573,29 @@ search.addWidget(
571573
);
572574
```
573575

576+
### Styling
577+
578+
```html
579+
<div class="ais-hits">
580+
<div class="ais-hits--item">Hit content</div>
581+
...
582+
<div class="ais-hits--item">Hit content</div>
583+
</div>
584+
<!-- If no results -->
585+
<div class="ais-hits ais-hits__empty">
586+
No results
587+
</div>
588+
```
589+
590+
```css
591+
.ais-hits {
592+
}
593+
.ais-hits--item {
594+
}
595+
.ais-hits__empty {
596+
}
597+
```
598+
574599
### toggle
575600

576601
![Example of the toggle widget][toggle]

components/Hits.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@ var Template = require('./Template');
55

66
class Hits extends React.Component {
77
renderWithResults() {
8-
var renderedHits = map(this.props.results.hits, (hit) => {
8+
var renderedHits = map(this.props.results.hits, hit => {
99
return (
10-
<Template
11-
data={hit}
12-
key={hit.objectID}
13-
templateKey="hit"
14-
{...this.props.templateProps}
15-
/>
10+
<div className={this.props.cssClasses.item}>
11+
<Template
12+
data={hit}
13+
key={hit.objectID}
14+
templateKey="item"
15+
{...this.props.templateProps}
16+
/>
17+
</div>
1618
);
1719
});
1820

19-
return <div>{renderedHits}</div>;
21+
return <div className={this.props.cssClasses.root}>{renderedHits}</div>;
2022
}
2123

2224
renderNoResults() {
25+
var className = [this.props.cssClasses.root, this.props.cssClasses.empty].join(' ');
2326
return (
24-
<div>
27+
<div className={className}>
2528
<Template
2629
data={this.props.results}
2730
templateKey="empty"

components/__tests__/Hits-test.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,55 @@ describe('Hits', () => {
2828
objectID: 'mom'
2929
}]};
3030

31-
let props = {results, templateProps};
31+
let props = {
32+
results,
33+
templateProps,
34+
cssClasses: {
35+
root: 'custom-root',
36+
item: 'custom-item',
37+
empty: 'custom-empty'
38+
}
39+
};
3240
renderer.render(<Hits {...props} />);
3341
let out = renderer.getRenderOutput();
3442

3543
expect(out).toEqualJSX(
36-
<div>
37-
<Template
38-
data={results.hits[0]}
39-
key={results.hits[0].objectID}
40-
templateKey="hit"
41-
/>
42-
<Template
43-
data={results.hits[1]}
44-
key={results.hits[1].objectID}
45-
templateKey="hit"
46-
/>
44+
<div className="custom-root">
45+
<div className="custom-item">
46+
<Template
47+
data={results.hits[0]}
48+
key={results.hits[0].objectID}
49+
templateKey="item"
50+
/>
51+
</div>
52+
<div className="custom-item">
53+
<Template
54+
data={results.hits[1]}
55+
key={results.hits[1].objectID}
56+
templateKey="item"
57+
/>
58+
</div>
4759
</div>
4860
);
4961
});
5062

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

54-
let props = {results, templateProps};
66+
let props = {
67+
results,
68+
templateProps,
69+
cssClasses: {
70+
root: 'custom-root',
71+
item: 'custom-item',
72+
empty: 'custom-empty'
73+
}
74+
};
5575
renderer.render(<Hits {...props} />);
5676
let out = renderer.getRenderOutput();
5777

5878
expect(out).toEqualJSX(
59-
<div>
79+
<div className="custom-root custom-empty">
6080
<Template
6181
data={results}
6282
templateKey="empty"

example/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ search.addWidget(
4343
container: '#hits',
4444
templates: {
4545
empty: require('./templates/no-results.html'),
46-
hit: require('./templates/hit.html')
46+
item: require('./templates/item.html')
4747
},
4848
hitsPerPage: 6
4949
})
File renamed without changes.

lib/utils.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,22 @@ function isSpecialClick(event) {
3535

3636
function bemHelper(block) {
3737
return function(element, modifier) {
38-
if (!element) {
38+
// block
39+
if (!element && !modifier) {
3940
return block;
4041
}
41-
if (!modifier) {
42-
return block + '--' + element;
42+
// block--element
43+
if (element && !modifier) {
44+
return `${block}--${element}`;
45+
}
46+
// block--element__modifier
47+
if (element && modifier) {
48+
return `${block}--${element}__${modifier}`;
49+
}
50+
// block__modifier
51+
if (!element && modifier) {
52+
return `${block}__${modifier}`;
4353
}
44-
return block + '--' + element + '__' + modifier;
4554
};
4655
}
4756

themes/default/default.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
}
4545

4646
/* HITS */
47+
.ais-hits {
48+
}
49+
.ais-hits--item {
50+
}
51+
.ais-hits__empty {
52+
}
4753

4854
/* PAGINATION */
4955
.ais-pagination {

widgets/hits/__tests__/defaultTemplates-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ describe('hits defaultTemplates', () => {
99
expect(defaultTemplates.empty).toBe('No results');
1010
});
1111

12-
it('has a `hit` default template', () => {
13-
let hit = {
12+
it('has a `item` default template', () => {
13+
let item = {
1414
hello: 'there,',
1515
how: {
1616
are: 'you?'
@@ -25,6 +25,6 @@ describe('hits defaultTemplates', () => {
2525
}
2626
}`;
2727

28-
expect(defaultTemplates.hit(hit)).toBe(expected);
28+
expect(defaultTemplates.item(item)).toBe(expected);
2929
});
3030
});

widgets/hits/__tests__/hits-test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ describe('hits()', () => {
6060
});
6161

6262
function getProps() {
63-
return {hits: results.hits, results, templateProps};
63+
return {
64+
hits: results.hits,
65+
results,
66+
templateProps,
67+
cssClasses: {
68+
root: 'ais-hits',
69+
item: 'ais-hits--item',
70+
empty: 'ais-hits__empty'
71+
}
72+
};
6473
}
6574
});

widgets/hits/defaultTemplates.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
empty: 'No results',
3-
hit: function(data) {
3+
item: function(data) {
44
return JSON.stringify(data, null, 2);
55
}
66
};

widgets/hits/hits.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,48 @@ var React = require('react');
22
var ReactDOM = require('react-dom');
33

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

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

911
/**
1012
* Display the list of results (hits) from the current search
1113
* @param {String|DOMElement} options.container CSS Selector or DOMElement to insert the widget
14+
* @param {Object} [options.cssClasses] CSS classes to add
15+
* @param {String} [options.cssClasses.root] CSS class to add to the wrapping element
16+
* @param {String} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
17+
* @param {String} [options.cssClasses.item] CSS class to add to each result
1218
* @param {Object} [options.templates] Templates to use for the widget
1319
* @param {String|Function} [options.templates.empty=''] Template to use when there are no results.
14-
* Gets passed the `result` from the API call.
15-
* @param {String|Function} [options.templates.hit=''] Template to use for each result.
16-
* Gets passed the `hit` of the result.
20+
* @param {String|Function} [options.templates.item=''] Template to use for each result.
1721
* @param {Object} [options.transformData] Method to change the object passed to the templates
1822
* @param {Function} [options.transformData.empty=''] Method used to change the object passed to the empty template
19-
* @param {Function} [options.transformData.hit=''] Method used to change the object passed to the hit template
23+
* @param {Function} [options.transformData.item=''] Method used to change the object passed to the item template
2024
* @param {Number} [hitsPerPage=20] The number of hits to display per page
2125
* @return {Object}
2226
*/
2327
function hits({
2428
container,
29+
cssClasses = {},
2530
templates = defaultTemplates,
2631
transformData,
2732
hitsPerPage = 20
2833
}) {
2934
var containerNode = utils.getContainerNode(container);
30-
var usage = 'Usage: hits({container, [templates.{empty,hit}, transformData.{empty,hit}, hitsPerPage])';
35+
var usage = 'Usage: hits({container, [cssClasses.{root,empty,item}, templates.{empty,item}, transformData.{empty,item}, hitsPerPage])';
3136

3237
if (container === null) {
3338
throw new Error(usage);
3439
}
3540

41+
cssClasses = {
42+
root: cx(bem(null), cssClasses.root),
43+
item: cx(bem('item'), cssClasses.item),
44+
empty: cx(bem(null, 'empty'), cssClasses.empty)
45+
};
46+
3647
return {
3748
getConfiguration: () => ({hitsPerPage}),
3849
render: function({results, templatesConfig}) {
@@ -45,6 +56,7 @@ function hits({
4556

4657
ReactDOM.render(
4758
<Hits
59+
cssClasses={cssClasses}
4860
hits={results.hits}
4961
results={results}
5062
templateProps={templateProps}

0 commit comments

Comments
 (0)