Skip to content

Commit 745ed89

Browse files
bobylitovvo
authored andcommitted
fix(infinite-hits): disable load more button when no more pages (#1973)
fixes #1971
1 parent 6e0d19d commit 745ed89

File tree

5 files changed

+159
-8
lines changed

5 files changed

+159
-8
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<h4 class="no-toc">Parameters</h4>
2+
<p class="attr-name">
3+
<span class='attr-required'>`options.container`<span class="show-description">…</span></span>
4+
<span class="attr-infos">(<code>string</code> &#124; <code>DOMElement</code>)</span>
5+
</p>
6+
<p class="attr-description important">CSS Selector or DOMElement to insert the widget</p>
7+
<p class="attr-name">
8+
<span class='attr-optional important'>`options.hitsPerPage`<span class="show-description">…</span></span>
9+
<span class="attr-infos">Default:<code class="attr-default">20</code>(<code>number</code>)</span>
10+
</p>
11+
<p class="attr-description important">The number of hits to display per page</p>
12+
<p class="attr-name">
13+
<span class='attr-optional'>`options.templates`<span class="show-description">…</span></span>
14+
<span class="attr-infos">(<code>Object</code>)</span>
15+
</p>
16+
<p class="attr-description">Templates to use for the widget</p>
17+
<p class="attr-name">
18+
<span class='attr-optional'>`options.templates.empty`<span class="show-description">…</span></span>
19+
<span class="attr-infos">Default:<code class="attr-default">&quot;&quot;</code>(<code>string</code> &#124; <code>function</code>)</span>
20+
</p>
21+
<p class="attr-description">Template to use when there are no results.</p>
22+
<p class="attr-name">
23+
<span class='attr-optional'>`options.templates.item`<span class="show-description">…</span></span>
24+
<span class="attr-infos">Default:<code class="attr-default">&quot;&quot;</code>(<code>string</code> &#124; <code>function</code>)</span>
25+
</p>
26+
<p class="attr-description">Template to use for each result. This template will receive an object containing a single record.</p>
27+
<p class="attr-name">
28+
<span class='attr-optional'>`options.showMoreLabel`<span class="show-description">…</span></span>
29+
<span class="attr-infos">Default:<code class="attr-default">&quot;Show more results&quot;</code>(<code>string</code>)</span>
30+
</p>
31+
<p class="attr-description">label used on the show more button</p>
32+
<p class="attr-name">
33+
<span class='attr-optional'>`options.transformData`<span class="show-description">…</span></span>
34+
<span class="attr-infos">(<code>Object</code>)</span>
35+
</p>
36+
<p class="attr-description">Method to change the object passed to the templates</p>
37+
<p class="attr-name">
38+
<span class='attr-optional'>`options.transformData.empty`<span class="show-description">…</span></span>
39+
<span class="attr-infos">(<code>function</code>)</span>
40+
</p>
41+
<p class="attr-description">Method used to change the object passed to the `empty` template</p>
42+
<p class="attr-name">
43+
<span class='attr-optional'>`options.transformData.item`<span class="show-description">…</span></span>
44+
<span class="attr-infos">(<code>function</code>)</span>
45+
</p>
46+
<p class="attr-description">Method used to change the object passed to the `item` template</p>
47+
<p class="attr-name">
48+
<span class='attr-optional'>`options.cssClasses`<span class="show-description">…</span></span>
49+
<span class="attr-infos">(<code>Object</code>)</span>
50+
</p>
51+
<p class="attr-description">CSS classes to add</p>
52+
<p class="attr-name">
53+
<span class='attr-optional'>`options.cssClasses.root`<span class="show-description">…</span></span>
54+
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
55+
</p>
56+
<p class="attr-description">CSS class to add to the wrapping element</p>
57+
<p class="attr-name">
58+
<span class='attr-optional'>`options.cssClasses.empty`<span class="show-description">…</span></span>
59+
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
60+
</p>
61+
<p class="attr-description">CSS class to add to the wrapping element when no results</p>
62+
<p class="attr-name">
63+
<span class='attr-optional'>`options.cssClasses.item`<span class="show-description">…</span></span>
64+
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
65+
</p>
66+
<p class="attr-description">CSS class to add to each result</p>
67+
68+
<p class="attr-legend">* <span>Required</span></p>

docs/documentation.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,55 @@ and
423423

424424
<div id="hits-container" class="widget-container"></div>
425425

426+
#### Infinite hits
427+
428+
<div class="codebox-combo">
429+
430+
<img class="widget-icon pull-left" src="{% asset_path icon-widget-results.svg %}">
431+
The infinite hits widget is like the hits widget but instead of relying on an external pagination widget, it displays a button to load more results. <br/>
432+
It will display the results as one continuous long page. Its usage is particularly recommended in a mobile context. Even though it is not incompatible, it does not go well with the pagination widget. <br/>
433+
It accepts a [Mustache](https://mustache.github.io/) template string or a function returning a string. See the [templates](#templates) section.
434+
{:.description}
435+
436+
<div class="code-box">
437+
<div class="code-sample-snippet js-toggle-snippet">
438+
{% highlight javascript %}
439+
{% raw %}
440+
search.addWidget(
441+
instantsearch.widgets.infiniteHits({
442+
container: '#infinite-hits-container',
443+
templates: {
444+
empty: 'No results',
445+
item: '<strong>Hit {{objectID}}</strong>: {{{_highlightResult.name.value}}}'
446+
},
447+
hitsPerPage: 3
448+
})
449+
);
450+
{% endraw %}
451+
{% endhighlight %}
452+
</div>
453+
<div class="jsdoc js-toggle-jsdoc">
454+
{% highlight javascript %}
455+
{% raw %}
456+
instantsearch.widgets.infiniteHits(options);
457+
{% endraw %}
458+
{% endhighlight %}
459+
460+
{% include widget-jsdoc/infiniteHits.md %}
461+
462+
</div>
463+
<div class="requirements js-toggle-requirements">
464+
For better control over what kind of data is returned, we suggest you configure the
465+
[attributeToRetrieve](https://www.algolia.com/doc/rest#param-attributesToRetrieve)
466+
and
467+
[attributeToHighlight](https://www.algolia.com/doc/rest#param-attributesToHighlight) of your index.
468+
</div>
469+
</div>
470+
471+
</div>
472+
473+
<div id="infinite-hits-container" class="widget-container"></div>
474+
426475

427476
#### hitsPerPageSelector
428477

src/components/InfiniteHits.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class InfiniteHits extends React.Component {
55

66
render() {
77
const {cssClasses, hits, results, showMore, showMoreLabel, templateProps} = this.props;
8+
const btn = this.props.isLastPage ?
9+
<button disabled>{showMoreLabel}</button> :
10+
<button onClick={showMore}>{showMoreLabel}</button>;
11+
812
return (
913
<div>
1014
<Hits
@@ -13,7 +17,9 @@ class InfiniteHits extends React.Component {
1317
results={results}
1418
templateProps={templateProps}
1519
/>
16-
<div className={cssClasses.showmore}><button onClick={showMore}>{showMoreLabel}</button></div>
20+
<div className={cssClasses.showmore}>
21+
{btn}
22+
</div>
1723
</div>
1824
);
1925
}
@@ -32,6 +38,7 @@ InfiniteHits.propTypes = {
3238
showMore: React.PropTypes.function,
3339
showMoreLabel: React.PropTypes.string,
3440
templateProps: React.PropTypes.object.isRequired,
41+
isLastPage: React.PropTypes.bool.isRequired,
3542
};
3643

3744
export default InfiniteHits;

src/widgets/infinite-hits/__tests__/infinite-hits-test.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ describe('infiniteHits()', () => {
6767
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
6868
});
6969

70+
it('if it is the last page, then the props should contain isLastPage true', () => {
71+
props = getProps();
72+
const state = {page: 0};
73+
widget.render({
74+
results: {...results, page: 0, nbPages: 2},
75+
state,
76+
});
77+
widget.render({
78+
results: {...results, page: 1, nbPages: 2},
79+
state,
80+
});
81+
82+
expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
83+
const propsWithIsLastPageFalse = {...(getProps({...results, page: 0, nbPages: 2})), isLastPage: false};
84+
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<InfiniteHits {...propsWithIsLastPageFalse} />);
85+
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
86+
const propsWithIsLastPageTrue = {...(getProps({...results, page: 1, nbPages: 2})), isLastPage: true};
87+
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<InfiniteHits {...propsWithIsLastPageTrue} />);
88+
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
89+
});
90+
7091
it('does not accept both item and allItems templates', () => {
7192
expect(infiniteHits.bind({container, templates: {item: '', allItems: ''}})).toThrow();
7293
});
@@ -85,10 +106,10 @@ describe('infiniteHits()', () => {
85106
infiniteHits.__ResetDependency__('defaultTemplates');
86107
});
87108

88-
function getProps() {
109+
function getProps(otherResults) {
89110
return {
90-
hits: results.hits,
91-
results,
111+
hits: (otherResults || results).hits,
112+
results: otherResults || results,
92113
templateProps,
93114
cssClasses: {
94115
root: 'ais-infinite-hits root cx',
@@ -98,6 +119,7 @@ describe('infiniteHits()', () => {
98119
},
99120
showMore: () => {},
100121
showMoreLabel: 'Show more results',
122+
isLastPage: false,
101123
};
102124
}
103125
});

src/widgets/infinite-hits/infinite-hits.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ const bem = bemHelper('ais-infinite-hits');
1313

1414
/**
1515
* Display the list of results (hits) from the current search
16-
* @function hits
16+
* @function infiniteHits
1717
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
1818
* @param {number} [options.hitsPerPage=20] The number of hits to display per page [*]
1919
* @param {Object} [options.templates] Templates to use for the widget
20-
* @param {string|Function} [options.templates.empty=''] Template to use when there are no results.
21-
* @param {string|Function} [options.templates.item=''] Template to use for each result. This template will receive an object containing a single record.
20+
* @param {string|Function} [options.templates.empty=""] Template to use when there are no results.
21+
* @param {string|Function} [options.templates.item=""] Template to use for each result. This template will receive an object containing a single record.
22+
* @param {string} [options.showMoreLabel="Show more results"] label used on the show more button
2223
* @param {Object} [options.transformData] Method to change the object passed to the templates
2324
* @param {Function} [options.transformData.empty] Method used to change the object passed to the `empty` template
2425
* @param {Function} [options.transformData.item] Method used to change the object passed to the `item` template
2526
* @param {Object} [options.cssClasses] CSS classes to add
2627
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the wrapping element
2728
* @param {string|string[]} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
28-
* @param {string|string[]} [options.7cssClasses.item] CSS class to add to each result
29+
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each result
2930
* @return {Object}
3031
*/
3132
const usage = `
@@ -34,6 +35,7 @@ infiniteHits({
3435
container,
3536
[ cssClasses.{root,empty,item}={} ],
3637
[ templates.{empty,item} | templates.{empty} ],
38+
[ showMoreLabel="Show more results" ]
3739
[ transformData.{empty,item} | transformData.{empty} ],
3840
[ hitsPerPage=20 ]
3941
})`;
@@ -80,13 +82,16 @@ function infiniteHits({
8082

8183
hitsCache = [...hitsCache, ...results.hits];
8284

85+
const isLastPage = results.nbPages <= results.page + 1;
86+
8387
ReactDOM.render(
8488
<InfiniteHits
8589
cssClasses={cssClasses}
8690
hits={hitsCache}
8791
results={results}
8892
showMore={this.showMore}
8993
showMoreLabel={showMoreLabel}
94+
isLastPage={isLastPage}
9095
templateProps={this._templateProps}
9196
/>,
9297
containerNode

0 commit comments

Comments
 (0)