Skip to content

[Enhancement] Add menuRenderer prop to support windowing for large lists #859

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

Merged
merged 20 commits into from
Apr 1, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,52 @@ You can also completely replace the method used to filter either a single option

For multi-select inputs, when providing a custom `filterOptions` method, remember to exclude current values from the returned array of options.

#### Effeciently rendering large lists with windowing

The `menuRenderer` property can be used to override the default drop-down list of options.
This should be done when the list is large (hundreds or thousands of items) for faster rendering.
The custom `menuRenderer` property accepts the following named parameters:

| Parameter | Type | Description |
|:---|:---|:---|
| focusedOption | `Object` | The currently focused option; should be visible in the menu by default. |
| focusOption | `Function` | Callback to focus a new option; receives the option as a parameter. |
| labelKey | `String` | Option labels are accessible with this string key. |
| options | `Array<Object>` | Ordered array of options to render. |
| selectValue | `Function` | Callback to select a new option; receives the option as a parameter. |
| valueArray | `Array<Object>` | Array of currently selected options. |

Windowing libraries like [`react-virtualized`](https://github.com/bvaughn/react-virtualized) can then be used to more efficiently render the drop-down menu like so:

```js
menuRenderer({ focusedOption, focusOption, labelKey, options, selectValue, valueArray }) {
const focusedOptionIndex = options.indexOf(focusedOption);
const option = options[index];
const isFocusedOption = option === focusedOption;

return (
<VirtualScroll
ref="VirtualScroll"
height={200}
rowHeight={35}
rowRenderer={(index) => (
<div
onMouseOver={() => focusOption(option)}
onClick={() => selectValue(option)}
>
{option[labelKey]}
</div>
)}
rowsCount={options.length}
scrollToIndex={focusedOptionIndex}
width={200}
/>
)
}
```

Check out the demo site for a more complete example of this.

### Further options


Expand Down Expand Up @@ -235,6 +281,7 @@ For multi-select inputs, when providing a custom `filterOptions` method, remembe
matchProp | string | 'any' | (any, label, value) which option property to filter on
scrollMenuIntoView | bool | true | whether the viewport will shift to display the entire menu when engaged
menuBuffer | number | 0 | buffer of px between the base of the dropdown and the viewport to shift if menu doesnt fit in viewport
menuRenderer | func | undefined | Renders a custom menu with options; accepts the following named parameters: `menuRenderer({ focusedOption, focusOption, options, selectValue, valueArray })`
multi | bool | undefined | multi-value input
name | string | undefined | field name, for hidden `<input />` tag
newOptionCreator | func | undefined | factory to create new options when `allowCreate` is true
Expand Down
2 changes: 2 additions & 0 deletions examples/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import CustomComponents from './components/CustomComponents';
import CustomRender from './components/CustomRender';
import Multiselect from './components/Multiselect';
import NumericSelect from './components/NumericSelect';
import Cities from './components/Cities';
import States from './components/States';

ReactDOM.render(
<div>
<Cities label="Cities" searchable />
<States label="States" searchable />
<Multiselect label="Multiselect" />
<Contributors label="Contributors (Async)" />
Expand Down
114 changes: 114 additions & 0 deletions examples/src/components/Cities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import Select from 'react-select';
import { AutoSizer, VirtualScroll } from 'react-virtualized';

const DATA = require('../data/cities');

const OPTION_HEIGHT = 35;
const MAX_OPTIONS_HEIGHT = 200;

var CitiesField = React.createClass({
displayName: 'CitiesField',
propTypes: {
label: React.PropTypes.string,
searchable: React.PropTypes.bool,
},
getDefaultProps () {
return {
label: 'Cities:',
searchable: true,
};
},
getInitialState () {
return {
country: 'AU',
disabled: false,
searchable: this.props.searchable,
selectValue: 'new-south-wales',
clearable: true,
};
},
switchCountry (e) {
var newCountry = e.target.value;
console.log('Country changed to ' + newCountry);
this.setState({
country: newCountry,
selectValue: null
});
},
updateValue (newValue) {
console.log('State changed to ' + newValue);
this.setState({
selectValue: newValue
});
},
focusStateSelect () {
this.refs.stateSelect.focus();
},
toggleCheckbox (e) {
let newState = {};
newState[e.target.name] = e.target.checked;
this.setState(newState);
},
renderMenu({ focusedOption, focusOption, labelKey, options, selectValue, valueArray }) {
const focusedOptionIndex = options.indexOf(focusedOption);
const height = Math.min(MAX_OPTIONS_HEIGHT, options.length * OPTION_HEIGHT);

return (
<AutoSizer disableHeight>
{({ width }) => (
<VirtualScroll
ref="VirtualScroll"
height={height}
rowHeight={OPTION_HEIGHT}
rowRenderer={(index) => (
<div
onMouseOver={() => focusOption(options[index])}
onClick={() => selectValue(options[index])}
style={{
backgroundColor: options[index] === focusedOption ? '#eee' : '#fff',
height: OPTION_HEIGHT,
display: 'flex',
alignItems: 'center',
padding: '0 .5rem'
}}
>
{options[index][labelKey]}
</div>
)}
rowsCount={options.length}
scrollToIndex={focusedOptionIndex}
width={width}
/>
)}
</AutoSizer>
);
},
render () {
var options = DATA.CITIES;
return (
<div className="section">
<h3 className="section-heading">World's Largest Cities</h3>
<h4>Uses react-virtualized to display data</h4>
<Select ref="stateSelect"
autofocus
options={options}
simpleValue
clearable={this.state.clearable}
name="selected-state"
disabled={this.state.disabled}
value={this.state.selectValue}
onChange={this.updateValue}
searchable={this.state.searchable}
labelKey="name"
valueKey="name"
menuStyle={{ overflow: 'hidden' }}
menuRenderer={this.renderMenu}
/>
</div>
);
}
});


module.exports = CitiesField;
Loading