Search code examples
reactjstypescript3.0

React - update <select> when <option> updated


I have made a simple component to fill <select> with array of entries either from REST or local array. The code is used as follows:

<ComboBox id="TestAsync" label="Country List" default="DE" onChange={this.change}>
    <DataSource source="/api/region/country" idField="id" captionField="name" />
</ComboBox>

<ComboBox> is basically a statement with select, but with <DataSource> detection. Below is <ComboBox> render code

constructor(props: IField) {
    super(props);
    this.state = {
        value: props.default
    }
}

public componentDidUpdate(prevProps: IField, prevStat: IState) {
    if (prevProps.default != this.props.default) this.setState({ value: this.props.default });
}

render() {
    return <div className="hx-field" id={this.props.id}>
        <label>{this.props.label}</label>
        <select name={this.props.id} onChange={this.onChangeSelect.bind(this)} value={this.state.value} disabled={this.props.readonly} key={this.props.id}>
            {this.props.children}
        </select>
    </div>
}    

And this is <Datasource> class:

export class DataSource extends React.Component<IDataSource, IDataSourceState> {

    constructor(props: IDataSource<T>) {
        super(props);
        this.state = {
            rawData: null,
            options: [
                <option value="">Loading ... </option>
            ]
        }
    }

    private option(item): JSX.Element {
        return <option value={item[this.props.idField]} key={item[this.props.idField]}>{item[this.props.captionField]}</option>
    }

    private load() {
        Adapter.load(this.props)
            .then(result => {
                if (!result) return;
                this.setState({
                    rawData: result,
                    options: result.map(x => this.option(x))
                })
            })
            .catch(error => {
                console.error("DataSource load error: Cannot load REST data");
                console.error(error);
            })
    }

    public render() {
        return this.state.options;
    }
}

The code is working, with one exception. I can't send a default value to it. The problem I think when that <select> component's value rendered, datasource is still empty. But the code works fine when I send value AFTER datasource populated. i.e picking an option from combobox.

How to force update <select> component when <datasource> modified?


Solution

  • One way is using selected prop on <option>:

    export class DataSource extends React.Component<IDataSource, IDataSourceState> {
    //    ...
      private option(item): JSX.Element {
         const {idField, captionField} = this.props;
         const {[idField]: id, [captionField]: caption} = item;
         return 
            <option 
               value={id} 
               key={id}
               selected={id===this.props.value}
            >
            {caption}
            </option>
       }
    

    This way we need pass actual value into DataSource otherwise our default value will never be changed:

    <ComboBox id="TestAsync" label="Country List" onChange={this.changeCountryListValue}>
        <DataSource source="/api/region/country" idField="id" captionField="name" selected={this.countryListValue || 'DE'} />
    </ComboBox>
    

    Or you can make your ComboBox to modify nested DataSource with injecting either onLoad callback or providing selected value like I did above in direct way. React.children.map and React.cloneElement to rescue on that.

    And finally you can move all the loading data out the ComboBox to either parent element or to some wrapper. I'd rather follow this way since currently your generic-named DataSource both loading data and rendering items into <option> that breaks single responsibility principle. This might disallow you using that in different context(when say you need load the same data but display in different component rather <option>)