belle
belle copied to clipboard
ComboBox: Improve ability to load asynchronous options
Here's my case: I have three comboBoxes whose selection of one triggers preloading data for the second and so on. The thing is, for the first one, I have this async call that fires on componentWillMount - which is probably the right place to have this kind of action - and for some reason, even though the render method is being called with the new state, the Options only appear after I type something in the input field, when in fact I would expect the Options to be available when I focus the input.
Some code for illustrating it:
import React from 'react';
import { ComboBox, Option } from 'belle';
import { merge } from '../shared/util';
const toOptions = (items) => {
return items.reduce((memo, item) => {
memo[item.name] = item;
return memo;
}, {});
};
const oneOf = (xs, y) => xs.some((x) => x === y);
const testPartial = (pattern) =>
(target) => !pattern || (
pattern &&
target &&
(new RegExp(pattern.toLowerCase())).test(target.toLowerCase())
);
export default class TimeEntryFormSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
options: {},
selected: null
};
}
componentWillMount() {
console.log('componentWillMount', this.state);
this.query('');
}
render() {
console.log('render', this.state);
return (
<div>
<ComboBox
disabled={ this.props.disabled }
placeholder={ this.props.placeholder }
className={ this.props.className }
displayCaret={ true }
enableHint={ true }
onUpdate={ this.handleUpdate.bind(this) }
defaultValue={ this.props.value }>
{ this.renderOptions() }
</ComboBox>
</div>
);
}
renderOptions() {
return Object.keys(this.state.options).map((value, key) => {
return (
<Option
value={ value }
key={ key }>
{ this.state.options[value].name }
</Option>
);
});
}
query(term) {
if (this.props.disabled) {
return;
}
let params = this.props.parentIds ? [...this.props.parentIds, term] : [term];
let request = this.props.service.query.apply(this, params.concat(false));
request.then((data) => {
console.log('query', data);
if (!data || !data.items) {
return;
}
let options = merge({}, this.state.options, toOptions(data.items));
this.setState({ options });
});
}
filter(input, option) {
return !input || testPartial(input)(option);
}
handleUpdate({ value }) {
if (oneOf(Object.keys(this.state.options), value)) {
this.handleOptionSelected(value);
} else {
this.handleInputChange(value);
}
}
handleInputChange(input) {
let hasMatches = Object.keys(this.state.options).some(testPartial(input));
if (!hasMatches) {
this.query(input);
}
}
handleOptionSelected(selected) {
this.setState({ selected });
this.props.onChange(this.state.options[selected]);
}
}
Console output after first loading:
componentWillMount Object {options: Object, selected: null}
render Object {options: Object, selected: null}
query Object {next: null, maxId: 195701, items: Array[8]}
render Object {options: Object, selected: null}
Managed to get it working by forcing a complete re-render.
render() {
if (!Object.keys(this.state.options).length) {
return (
<ComboBox
disabled={ true }
placeholder={ this.props.placeholder }
className={ this.props.className }
displayCaret={ true }>
</ComboBox>
);
}
return (
<div>
<ComboBox
disabled={ this.props.disabled }
placeholder={ this.props.placeholder }
className={ this.props.className }
displayCaret={ true }
enableHint={ true }
onUpdate={ this.handleUpdate.bind(this) }
defaultValue={ this.props.value }>
{ this.renderOptions() }
</ComboBox>
</div>
);
}
Hey @alanrsoares , @nikgraf ,
@alanrsoares : first of all thanks a lot for digging so deep into it.
I think its definitely good to have a method which is called when user selection an option or hint (typeahead.js) has it. Lets add it :+1: . But again my concern here is if user prefer to type whole value rather than select from option, we might miss that. So I think onBlur/ onUpdate is the best place to handle user inputs.
Again @alanrsoares , belle combo-box opens on focus. But its not happening for you, might be async call in 'componentWillMount' is delaying in getting the options.
Here: Console output after first loading:
componentWillMount Object {options: Object, selected: null}
render Object {options: Object, selected: null}
query Object {next: null, maxId: 195701, items: Array[8]}
render Object {options: Object, selected: null}
Last render does not shows data in state, seems like something not going correct.
Hey @alanrsoares ,
Did you had any luck in getting this one fixed.