+ );
+ }
+});
+
+module.exports = GithubUsersPagination;
diff --git a/src/Async.js b/src/Async.js
index 66810ee168..3059b0ab9a 100644
--- a/src/Async.js
+++ b/src/Async.js
@@ -23,6 +23,7 @@ const propTypes = {
onChange: PropTypes.func, // onChange handler: function (newValue) {}
onInputChange: PropTypes.func, // optional for keeping track of what is being typed
options: PropTypes.array.isRequired, // array of options
+ pagination: PropTypes.bool, // automatically load more options when the option list is scrolled to the end; default to false
placeholder: PropTypes.oneOfType([ // field placeholder, displayed when there's no value (shared with Select)
PropTypes.string,
PropTypes.node
@@ -46,6 +47,7 @@ const defaultProps = {
ignoreCase: true,
loadingPlaceholder: 'Loading...',
options: [],
+ pagination: false,
searchPromptText: 'Type to search',
};
@@ -58,10 +60,13 @@ export default class Async extends Component {
this.state = {
inputValue: '',
isLoading: false,
+ isLoadingPage: false,
+ page: 1,
options: props.options,
};
this.onInputChange = this.onInputChange.bind(this);
+ this.onMenuScrollToBottom = this.onMenuScrollToBottom.bind(this);
}
componentDidMount () {
@@ -84,8 +89,8 @@ export default class Async extends Component {
this._callback = null;
}
- loadOptions (inputValue) {
- const { loadOptions } = this.props;
+ loadOptions (inputValue, page = 1) {
+ const { loadOptions, pagination } = this.props;
const cache = this._cache;
if (
@@ -95,18 +100,30 @@ export default class Async extends Component {
this._callback = null;
this.setState({
- isLoading: false,
- options: cache[inputValue]
+ isLoading: false,
+ options: cache[inputValue].options,
+ page: cache[inputValue].page,
});
- return;
+ if (
+ !pagination ||
+ (pagination && (cache[inputValue].page >= page || cache[inputValue].hasReachedLastPage))
+ ) {
+ return;
+ }
}
const callback = (error, data) => {
- const options = data && data.options || [];
+ let options = data && data.options || [];
+
+ const hasReachedLastPage = pagination && options.length === 0;
+
+ if(page > 1) {
+ options = this.state.options.concat(options);
+ }
if (cache) {
- cache[inputValue] = options;
+ cache[inputValue] = { page, options, hasReachedLastPage };
}
if (callback === this._callback) {
@@ -114,7 +131,9 @@ export default class Async extends Component {
this.setState({
isLoading: false,
- options
+ isLoadingPage: false,
+ page,
+ options,
});
}
};
@@ -122,7 +141,14 @@ export default class Async extends Component {
// Ignore all but the most recent request
this._callback = callback;
- const promise = loadOptions(inputValue, callback);
+ let promise;
+
+ if (pagination) {
+ promise = loadOptions(inputValue, page, callback);
+ } else {
+ promise = loadOptions(inputValue, callback);
+ }
+
if (promise) {
promise.then(
(data) => callback(null, data),
@@ -135,7 +161,8 @@ export default class Async extends Component {
!this.state.isLoading
) {
this.setState({
- isLoading: true
+ isLoading: true,
+ isLoadingPage: page > this.state.page,
});
}
}
@@ -186,14 +213,20 @@ export default class Async extends Component {
this.select.focus();
}
+ onMenuScrollToBottom (inputValue) {
+ if (!this.props.pagination || this.state.isLoading) return;
+
+ this.loadOptions(inputValue, this.state.page + 1);
+ }
+
render () {
const { children, loadingPlaceholder, placeholder } = this.props;
- const { isLoading, options } = this.state;
+ const { isLoading, isLoadingPage, options } = this.state;
const props = {
noResultsText: this.noResultsText(),
placeholder: isLoading ? loadingPlaceholder : placeholder,
- options: (isLoading && loadingPlaceholder) ? [] : options,
+ options: (isLoading && loadingPlaceholder && !isLoadingPage) ? [] : options,
ref: (ref) => (this.select = ref),
};
@@ -201,7 +234,8 @@ export default class Async extends Component {
...this.props,
...props,
isLoading,
- onInputChange: this.onInputChange
+ onInputChange: this.onInputChange,
+ onMenuScrollToBottom: this.onMenuScrollToBottom,
});
}
}
diff --git a/src/Select.js b/src/Select.js
index d8b8542d20..ac00236868 100644
--- a/src/Select.js
+++ b/src/Select.js
@@ -563,7 +563,7 @@ class Select extends React.Component {
if (!this.props.onMenuScrollToBottom) return;
let { target } = event;
if (target.scrollHeight > target.offsetHeight && (target.scrollHeight - target.offsetHeight - target.scrollTop) <= 0) {
- this.props.onMenuScrollToBottom();
+ this.props.onMenuScrollToBottom(this.state.inputValue);
}
}
diff --git a/test/Async-test.js b/test/Async-test.js
index ff663d42ea..81c409ada4 100644
--- a/test/Async-test.js
+++ b/test/Async-test.js
@@ -169,9 +169,9 @@ describe('Async', () => {
// TODO: How to test this?
setTimeout(function() {
- expect(instance._cache.t, 'to equal', res.t.options);
- expect(instance._cache.te, 'to equal', res.te.options);
- expect(instance._cache.tes, 'to equal', res.tes.options);
+ expect(instance._cache.t.options, 'to equal', res.t.options);
+ expect(instance._cache.te.options, 'to equal', res.te.options);
+ expect(instance._cache.tes.options, 'to equal', res.tes.options);
cb();
}, 30);
});
@@ -536,4 +536,67 @@ describe('Async', () => {
expect(asyncInstance._callback, 'to equal', null);
});
});
+
+ describe('with pagination', () => {
+ it('should pass the page to loadOptions', () => {
+ createControl({
+ pagination: true
+ });
+ typeSearchText('a');
+ expect(loadOptions, 'was called with', 'a', 1);
+ });
+
+ it('should not try to load a page it has cached', () => {
+ createControl({
+ pagination: true,
+ cache: {
+ a: { options: [], page: 1 },
+ }
+ });
+ typeSearchText('a');
+ expect(loadOptions, 'was not called');
+ });
+
+ it('should load the next a page it on scroll to bottom', () => {
+ createControl({
+ pagination: true,
+ cache: {
+ a: { options: [], page: 1 },
+ }
+ });
+ typeSearchText('a');
+ expect(loadOptions, 'was not called');
+ asyncInstance.onMenuScrollToBottom('a');
+ expect(loadOptions, 'was called with', 'a', 2);
+ });
+
+ it('should not load the next a page it on scroll to bottom when pagination is false', () => {
+ createControl({
+ pagination: false,
+ cache: {
+ a: { options: [], page: 1 },
+ }
+ });
+ typeSearchText('a');
+ expect(loadOptions, 'was not called');
+ asyncInstance.onMenuScrollToBottom('a');
+ expect(loadOptions, 'was not called');
+ });
+
+ it('should combine the existing options with the additional options', () => {
+ createControl({
+ pagination: true,
+ loadOptions: (value, page, cb) => {
+ cb(null, createOptionsResponse(['bar']));
+ },
+ cache: {
+ a: { options: createOptionsResponse(['foo']).options, page: 1 },
+ }
+ });
+ asyncInstance.onMenuScrollToBottom('a');
+ expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 2);
+ expect(asyncNode.querySelectorAll('[role=option]')[0].textContent, 'to equal', 'foo');
+ expect(asyncNode.querySelectorAll('[role=option]')[1].textContent, 'to equal', 'bar');
+ });
+ });
});