Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pagination): add async pagination #1237

Closed
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,34 @@ const getOptions = (input) => {
/>
```

### Async options with pagination

If you want to load additional options asynchronously when the user reaches the bottom of the options menu, you can pass the `pagination` prop.

This will change the signature of `loadOptions` to pass the page which needs to be loaded: `loadOptions(inputValue, page, [callback])`.

An example using the `fetch` API and ES6 syntax, with an API that returns the same object as the previous example:

```javascript
import Select from 'react-select';

const getOptions = (input, page) => {
return fetch(`/users/${input}.json?page=${page}`)
.then((response) => {
return response.json();
}).then((json) => {
return { options: json };
});
}

<Select.Async
name="form-field-name"
value="one"
loadOptions={getOptions}
pagination
/>
```

### Async options loaded externally

If you want to load options asynchronously externally from the `Select` component, you can have the `Select` component show a loading spinner by passing in the `isLoading` prop set to `true`.
Expand Down Expand Up @@ -397,6 +425,7 @@ function onInputKeyDown(event) {
| openOnFocus | bool | false | open the options menu when the control gets focus (requires searchable = true) |
| optionRenderer | func | undefined | function which returns a custom way to render the options in the menu |
| options | array | undefined | array of options |
| pagination | bool | false | Load more options when the menu is scrolled to the bottom. `loadOptions` is given a page: `function(input, page, [callback])`
| placeholder | string\|node | 'Select ...' | field placeholder, displayed when there's no value |
| required | bool | false | applies HTML5 required attribute when needed |
| resetValue | any | null | value to set when the control is cleared |
Expand Down
4 changes: 3 additions & 1 deletion examples/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './example.less';
import Creatable from './components/Creatable';
import Contributors from './components/Contributors';
import GithubUsers from './components/GithubUsers';
import GithubUsersPagination from './components/GithubUsersPagination';
import CustomComponents from './components/CustomComponents';
import CustomRender from './components/CustomRender';
import Multiselect from './components/Multiselect';
Expand All @@ -22,7 +23,8 @@ ReactDOM.render(
<Multiselect label="Multiselect" />
<Virtualized label="Virtualized" />
<Contributors label="Contributors (Async)" />
<GithubUsers label="GitHub users (Async with fetch.js)" />
<GithubUsers label="Github users (Async with fetch.js)" />
<GithubUsersPagination label="Github users (Async Pagination with fetch.js)" />
<NumericSelect label="Numeric Values" />
<BooleanSelect label="Boolean Values" />
<CustomRender label="Custom Render Methods"/>
Expand Down
93 changes: 93 additions & 0 deletions examples/src/components/GithubUsersPagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';


const GithubUsersPagination = React.createClass({
displayName: 'GithubUsersPagination',
propTypes: {
label: React.PropTypes.string,
},
getInitialState () {
return {
backspaceRemoves: true,
multi: true
};
},
onChange (value) {
this.setState({
value: value,
});
},
switchToMulti () {
this.setState({
multi: true,
value: [this.state.value],
});
},
switchToSingle () {
this.setState({
multi: false,
value: this.state.value ? this.state.value[0] : null
});
},
getUsers (input, page) {
if (!input) {
return Promise.resolve({ options: [] });
}

return fetch(`https://api.github.com/search/users?q=${input}&page=${page}`)
.then((response) => response.json())
.then((json) => {
return { options: json.items };
});
},
gotoUser (value, event) {
window.open(value.html_url);
},
toggleBackspaceRemoves () {
this.setState({
backspaceRemoves: !this.state.backspaceRemoves
});
},
toggleCreatable () {
this.setState({
creatable: !this.state.creatable
});
},
render () {
const AsyncComponent = this.state.creatable
? Select.AsyncCreatable
: Select.Async;

return (
<div className="section">
<h3 className="section-heading">{this.props.label}</h3>
<AsyncComponent multi={this.state.multi} value={this.state.value} onChange={this.onChange} onValueClick={this.gotoUser} valueKey="id" labelKey="login" loadOptions={this.getUsers} pagination backspaceRemoves={this.state.backspaceRemoves} />
<div className="checkbox-list">
<label className="checkbox">
<input type="radio" className="checkbox-control" checked={this.state.multi} onChange={this.switchToMulti}/>
<span className="checkbox-label">Multiselect</span>
</label>
<label className="checkbox">
<input type="radio" className="checkbox-control" checked={!this.state.multi} onChange={this.switchToSingle}/>
<span className="checkbox-label">Single Value</span>
</label>
</div>
<div className="checkbox-list">
<label className="checkbox">
<input type="checkbox" className="checkbox-control" checked={this.state.creatable} onChange={this.toggleCreatable} />
<span className="checkbox-label">Creatable?</span>
</label>
<label className="checkbox">
<input type="checkbox" className="checkbox-control" checked={this.state.backspaceRemoves} onChange={this.toggleBackspaceRemoves} />
<span className="checkbox-label">Backspace Removes?</span>
</label>
</div>
<div className="hint">This example uses fetch.js for showing Async options with Promises</div>
</div>
);
}
});

module.exports = GithubUsersPagination;
60 changes: 47 additions & 13 deletions src/Async.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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
Expand All @@ -42,6 +43,7 @@ const defaultProps = {
ignoreCase: true,
loadingPlaceholder: 'Loading...',
options: [],
pagination: false,
searchPromptText: 'Type to search',
};

Expand All @@ -54,10 +56,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 () {
Expand All @@ -80,8 +85,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 (
Expand All @@ -91,34 +96,55 @@ 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) {
this._callback = null;

this.setState({
isLoading: false,
options
isLoadingPage: false,
page,
options,
});
}
};

// 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),
Expand All @@ -131,7 +157,8 @@ export default class Async extends Component {
!this.state.isLoading
) {
this.setState({
isLoading: true
isLoading: true,
isLoadingPage: page > this.state.page,
});
}
}
Expand Down Expand Up @@ -176,22 +203,29 @@ 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, multi, onChange, placeholder, value } = 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),
};

return children({
...this.props,
...props,
isLoading,
onInputChange: this.onInputChange
onInputChange: this.onInputChange,
onMenuScrollToBottom: this.onMenuScrollToBottom,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,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);
}
}

Expand Down
Loading