Skip to content

Commit

Permalink
fix(infinite-hits): disable load more button when no more pages (#1973)
Browse files Browse the repository at this point in the history
fixes #1971
  • Loading branch information
bobylito authored and vvo committed Feb 14, 2017
1 parent 6e0d19d commit 745ed89
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 8 deletions.
68 changes: 68 additions & 0 deletions docs/_includes/widget-jsdoc/infiniteHits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<h4 class="no-toc">Parameters</h4>
<p class="attr-name">
<span class='attr-required'>`options.container`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>string</code> &#124; <code>DOMElement</code>)</span>
</p>
<p class="attr-description important">CSS Selector or DOMElement to insert the widget</p>
<p class="attr-name">
<span class='attr-optional important'>`options.hitsPerPage`<span class="show-description">…</span></span>
<span class="attr-infos">Default:<code class="attr-default">20</code>(<code>number</code>)</span>
</p>
<p class="attr-description important">The number of hits to display per page</p>
<p class="attr-name">
<span class='attr-optional'>`options.templates`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>Object</code>)</span>
</p>
<p class="attr-description">Templates to use for the widget</p>
<p class="attr-name">
<span class='attr-optional'>`options.templates.empty`<span class="show-description">…</span></span>
<span class="attr-infos">Default:<code class="attr-default">&quot;&quot;</code>(<code>string</code> &#124; <code>function</code>)</span>
</p>
<p class="attr-description">Template to use when there are no results.</p>
<p class="attr-name">
<span class='attr-optional'>`options.templates.item`<span class="show-description">…</span></span>
<span class="attr-infos">Default:<code class="attr-default">&quot;&quot;</code>(<code>string</code> &#124; <code>function</code>)</span>
</p>
<p class="attr-description">Template to use for each result. This template will receive an object containing a single record.</p>
<p class="attr-name">
<span class='attr-optional'>`options.showMoreLabel`<span class="show-description">…</span></span>
<span class="attr-infos">Default:<code class="attr-default">&quot;Show more results&quot;</code>(<code>string</code>)</span>
</p>
<p class="attr-description">label used on the show more button</p>
<p class="attr-name">
<span class='attr-optional'>`options.transformData`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>Object</code>)</span>
</p>
<p class="attr-description">Method to change the object passed to the templates</p>
<p class="attr-name">
<span class='attr-optional'>`options.transformData.empty`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>function</code>)</span>
</p>
<p class="attr-description">Method used to change the object passed to the `empty` template</p>
<p class="attr-name">
<span class='attr-optional'>`options.transformData.item`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>function</code>)</span>
</p>
<p class="attr-description">Method used to change the object passed to the `item` template</p>
<p class="attr-name">
<span class='attr-optional'>`options.cssClasses`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>Object</code>)</span>
</p>
<p class="attr-description">CSS classes to add</p>
<p class="attr-name">
<span class='attr-optional'>`options.cssClasses.root`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
</p>
<p class="attr-description">CSS class to add to the wrapping element</p>
<p class="attr-name">
<span class='attr-optional'>`options.cssClasses.empty`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
</p>
<p class="attr-description">CSS class to add to the wrapping element when no results</p>
<p class="attr-name">
<span class='attr-optional'>`options.cssClasses.item`<span class="show-description">…</span></span>
<span class="attr-infos">(<code>string</code> &#124; <code>Array.&lt;string&gt;</code>)</span>
</p>
<p class="attr-description">CSS class to add to each result</p>

<p class="attr-legend">* <span>Required</span></p>
49 changes: 49 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,55 @@ and

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

#### Infinite hits

<div class="codebox-combo">

<img class="widget-icon pull-left" src="{% asset_path icon-widget-results.svg %}">
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/>
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/>
It accepts a [Mustache](https://mustache.github.io/) template string or a function returning a string. See the [templates](#templates) section.
{:.description}

<div class="code-box">
<div class="code-sample-snippet js-toggle-snippet">
{% highlight javascript %}
{% raw %}
search.addWidget(
instantsearch.widgets.infiniteHits({
container: '#infinite-hits-container',
templates: {
empty: 'No results',
item: '<strong>Hit {{objectID}}</strong>: {{{_highlightResult.name.value}}}'
},
hitsPerPage: 3
})
);
{% endraw %}
{% endhighlight %}
</div>
<div class="jsdoc js-toggle-jsdoc">
{% highlight javascript %}
{% raw %}
instantsearch.widgets.infiniteHits(options);
{% endraw %}
{% endhighlight %}

{% include widget-jsdoc/infiniteHits.md %}

</div>
<div class="requirements js-toggle-requirements">
For better control over what kind of data is returned, we suggest you configure the
[attributeToRetrieve](https://www.algolia.com/doc/rest#param-attributesToRetrieve)
and
[attributeToHighlight](https://www.algolia.com/doc/rest#param-attributesToHighlight) of your index.
</div>
</div>

</div>

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


#### hitsPerPageSelector

Expand Down
9 changes: 8 additions & 1 deletion src/components/InfiniteHits.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class InfiniteHits extends React.Component {

render() {
const {cssClasses, hits, results, showMore, showMoreLabel, templateProps} = this.props;
const btn = this.props.isLastPage ?
<button disabled>{showMoreLabel}</button> :
<button onClick={showMore}>{showMoreLabel}</button>;

return (
<div>
<Hits
Expand All @@ -13,7 +17,9 @@ class InfiniteHits extends React.Component {
results={results}
templateProps={templateProps}
/>
<div className={cssClasses.showmore}><button onClick={showMore}>{showMoreLabel}</button></div>
<div className={cssClasses.showmore}>
{btn}
</div>
</div>
);
}
Expand All @@ -32,6 +38,7 @@ InfiniteHits.propTypes = {
showMore: React.PropTypes.function,
showMoreLabel: React.PropTypes.string,
templateProps: React.PropTypes.object.isRequired,
isLastPage: React.PropTypes.bool.isRequired,
};

export default InfiniteHits;
28 changes: 25 additions & 3 deletions src/widgets/infinite-hits/__tests__/infinite-hits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ describe('infiniteHits()', () => {
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
});

it('if it is the last page, then the props should contain isLastPage true', () => {
props = getProps();
const state = {page: 0};
widget.render({
results: {...results, page: 0, nbPages: 2},
state,
});
widget.render({
results: {...results, page: 1, nbPages: 2},
state,
});

expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
const propsWithIsLastPageFalse = {...(getProps({...results, page: 0, nbPages: 2})), isLastPage: false};
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<InfiniteHits {...propsWithIsLastPageFalse} />);
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
const propsWithIsLastPageTrue = {...(getProps({...results, page: 1, nbPages: 2})), isLastPage: true};
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<InfiniteHits {...propsWithIsLastPageTrue} />);
expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
});

it('does not accept both item and allItems templates', () => {
expect(infiniteHits.bind({container, templates: {item: '', allItems: ''}})).toThrow();
});
Expand All @@ -85,10 +106,10 @@ describe('infiniteHits()', () => {
infiniteHits.__ResetDependency__('defaultTemplates');
});

function getProps() {
function getProps(otherResults) {
return {
hits: results.hits,
results,
hits: (otherResults || results).hits,
results: otherResults || results,
templateProps,
cssClasses: {
root: 'ais-infinite-hits root cx',
Expand All @@ -98,6 +119,7 @@ describe('infiniteHits()', () => {
},
showMore: () => {},
showMoreLabel: 'Show more results',
isLastPage: false,
};
}
});
13 changes: 9 additions & 4 deletions src/widgets/infinite-hits/infinite-hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ const bem = bemHelper('ais-infinite-hits');

/**
* Display the list of results (hits) from the current search
* @function hits
* @function infiniteHits
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {number} [options.hitsPerPage=20] The number of hits to display per page [*]
* @param {Object} [options.templates] Templates to use for the widget
* @param {string|Function} [options.templates.empty=''] Template to use when there are no results.
* @param {string|Function} [options.templates.item=''] Template to use for each result. This template will receive an object containing a single record.
* @param {string|Function} [options.templates.empty=""] Template to use when there are no results.
* @param {string|Function} [options.templates.item=""] Template to use for each result. This template will receive an object containing a single record.
* @param {string} [options.showMoreLabel="Show more results"] label used on the show more button
* @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.item] Method used to change the object passed to the `item` template
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the wrapping element
* @param {string|string[]} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
* @param {string|string[]} [options.7cssClasses.item] CSS class to add to each result
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each result
* @return {Object}
*/
const usage = `
Expand All @@ -34,6 +35,7 @@ infiniteHits({
container,
[ cssClasses.{root,empty,item}={} ],
[ templates.{empty,item} | templates.{empty} ],
[ showMoreLabel="Show more results" ]
[ transformData.{empty,item} | transformData.{empty} ],
[ hitsPerPage=20 ]
})`;
Expand Down Expand Up @@ -80,13 +82,16 @@ function infiniteHits({

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

const isLastPage = results.nbPages <= results.page + 1;

ReactDOM.render(
<InfiniteHits
cssClasses={cssClasses}
hits={hitsCache}
results={results}
showMore={this.showMore}
showMoreLabel={showMoreLabel}
isLastPage={isLastPage}
templateProps={this._templateProps}
/>,
containerNode
Expand Down

0 comments on commit 745ed89

Please sign in to comment.