Skip to content
Open
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@
"typeahead",
"combobox",
"a11y"
]
],
"dependencies": {
"lodash.throttle": "^3.0.2"
}
}
48 changes: 35 additions & 13 deletions src/Combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ var {PureRenderMixin} = React.addons;

var emptyFunction = require('./helpers/emptyFunction');
var getUniqueId = require('./helpers/getUniqueId');
var throttle = require('lodash.throttle');

/**
* <Combobox> is a combobox-style widget that supports both inline- and
* <Combobox> is a combobox-style widget that supports both inline- and
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for removing white space at the end of the lines!

* menu-based autocompletion based on an asynchonously-loaded result set.
*/
var Combobox = React.createClass({
Expand Down Expand Up @@ -41,15 +42,15 @@ var Combobox = React.createClass({
/**
* The type of autocompletion behavior:
* - `menu` to display a popup menu with autocompletion options.
* - `inline` to display the first autocompletion option as text
* - `inline` to display the first autocompletion option as text
* "typed ahead" of the user's input.
* - `both` to display both at once.
* Default is `both`.
*/
autocomplete: React.PropTypes.oneOf(['menu', 'inline', 'both']),

/**
* Event handler fired when `value` changes to a new non-`null` value.
* Event handler fired when `value` changes to a new non-`null` value.
* Function called is passed `value`.
*/
onComplete: React.PropTypes.func,
Expand All @@ -64,15 +65,21 @@ var Combobox = React.createClass({
* The component to render for the list in the popup.
* Default is `List`.
*/
listComponent: React.PropTypes.func
listComponent: React.PropTypes.func,

/**
* The number of milliseconds to throttle `getOptionsForInputValue()`.
*/
getOptionsThrottleMs: React.PropTypes.number,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this optionsGetterThrottleMs? I initially thought this prop was supposed to be a function returning the number of seconds to throttle for, instead of just a number of seconds to throttle for.

},

getDefaultProps: function() {
return {
autocomplete: 'both',
onComplete: emptyFunction,
getLabelForOption: (option) => option+'',
listComponent: List
listComponent: List,
getOptionsThrottleMs: 500,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Trailing comma

};
},

Expand All @@ -89,12 +96,15 @@ var Combobox = React.createClass({
},

componentWillReceiveProps: function(nextProps) {
if (this.props.value !== nextProps.value &&
if (this.props.value !== nextProps.value &&
nextProps.value !== null) {
this.setState({
inputValue: this.props.getLabelForOption(nextProps.value)
});
}
if (this.props.getOptionsThrottleMs !== nextProps.getOptionsThrottleMs) {
this._bindUpdateOptions(nextProps.getOptionsThrottleMs);
}
},

isInlineCompleting: function() {
Expand All @@ -114,26 +124,26 @@ var Combobox = React.createClass({

getInputTypeaheadValue: function() {
var {options, optionIndex} = this.state;

if (!this.isInlineCompleting() || optionIndex === null) {
return null;
}

return this.props.getLabelForOption(options[optionIndex]);
},

updateOptionsForInputValue: function(inputValue) {
_updateOptionsForInputValue: function(inputValue) {
var optionsPromise = this.optionsPromise =
this.props.getOptionsForInputValue(inputValue);

optionsPromise.then((options) => {
// It's possible that when we're fetching, we may get out-of-order
// promise resolutions, even for cases like a contrived setTimeout demo.
// This leads to really wonky behavior.
//
//
// Ensure that we only update the state based on the most recent promise
// that was started for fetching.

if (this.optionsPromise !== optionsPromise) {
return;
}
Expand All @@ -146,6 +156,18 @@ var Combobox = React.createClass({
});
},

_bindUpdateOptions: function(throttleMs) {
var func = this._updateOptionsForInputValue;
if (throttleMs > 0) {
func = throttle(func, throttleMs, {trailing: true});
}
this.updateOptionsForInputValue = func;
},

componentWillMount: function() {
this._bindUpdateOptions(this.props.getOptionsThrottleMs);
},

handleInputChange: function(event) {
var inputValue = event.target.value;

Expand Down Expand Up @@ -204,7 +226,7 @@ var Combobox = React.createClass({
typeaheadValue={this.getInputTypeaheadValue()}
value={this.state.inputValue}
inputComponent={TypeaheadInput}>
<ListComponent
<ListComponent
options={this.state.options}
optionIndex={this.state.optionIndex}
onChange={this.handleListChange}
Expand All @@ -218,4 +240,4 @@ var Combobox = React.createClass({

});

module.exports = Combobox;
module.exports = Combobox;