belle icon indicating copy to clipboard operation
belle copied to clipboard

ComboBox: Improve ability to load asynchronous options

Open alanrsoares opened this issue 8 years ago • 3 comments

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}

alanrsoares avatar Jul 20 '15 22:07 alanrsoares

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>
    );
  }

alanrsoares avatar Jul 20 '15 23:07 alanrsoares

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.

jpuri avatar Jul 21 '15 04:07 jpuri

Hey @alanrsoares ,

Did you had any luck in getting this one fixed.

jpuri avatar Jul 28 '15 12:07 jpuri