LINUX.ORG.RU

[Решено] MUI - выпадающий список, потдягивающий опции с сервера

 


0

1

Есть компонент, представляющий выпадающий список и поле ввода. Когда пользователь вводит текст, с сервера подтягиваются опции для выбора.
Помогите исправить такой баг: label опции, установленной в начале (через проп defaultList) отображается пустым.

import React, { Component } from 'react';
import Autocomplete from '@material-ui/lab/Autocomplete';
import TextField from '@material-ui/core/TextField';
import CheckIcon from '@material-ui/icons/Check';

/*

SearchSelect Component

Props:
- selectedIds: array of currently selected options
- onItemSelect: function, a callback function that is called when an option is selected or unselected
- label: string, the caption for the select input
- fetchList: function, a callback function that takes a query and returns a Promise that 
    resolves to an array of available objects from the server. The callback will only 
    be fired if the user has entered at least 3 characters, excluding trailing white spaces.
    Returned array:
    [
        { label: 'Option 1', value: 'option1' },
        { label: 'Option 2', value: 'option2' },
        // ... additional options
    ]
- defaultList: array of the objects available without fetching from server. If fetchList is called later 
    and finds options with same values, their labels will be overwritten by those from server. Has the 
    same format as returned by fetchList.
- noOptionsMessage: string, the message to display when no options are found on the server or no proper query
    
Usage Example:
<SearchSelect
  selectedIds={currentOptions}
  onItemSelect={(selectedItems) => {
    console.log('Selected items:', selectedItems);
  }}
  label="Caption"
  fetchList={(query) => {
    return fetch(https://api.example.com/search?q=${query})
      .then(response => response.json())
      .then(data => data.results);
  }}
/>

*/


class SearchSelect extends Component {
    constructor(props) {
        super(props);
        this.state = {
            availableOptions: []
        }
    }
    
    componentDidMount() {
        this.setState({
            availableOptions: this.props.defaultList || []
        })
    }
    
    componentDidUpdate(prevProps) {
        if (prevProps.defaultList !== this.props.defaultList) {
            this.setState({
                availableOptions: this.props.defaultList || []
            });
        }
    }

    handleFetchList = (query) => {
        if (query.trim().length >= 3) {
            this.props.fetchList(query).then((options) => {
                const mergedOptions = options.map((option) => {
                    const existingOption = this.state.availableOptions.find((existing) => existing.value === option.value);
                    return existingOption ? { ...existingOption, label: option.label } : option;
                });
                this.setState({
                    availableOptions: [...this.props.defaultList, ...mergedOptions]
                });
            });
        } 
        else {
            this.setState({
                availableOptions: this.props.defaultList || []
            });
        }
    }

    isOptionEqualToValue = (option, value) => {
        return (option.value == value)
    }
    
    getOptionSelected = (option, value) => option.value == value;

    render() {
        const { selectedIds, onItemSelect, label, noOptionsMessage } = this.props;
        console.log(this.props);
        console.log(this.state);
        
        return (
            <Autocomplete
                multiple
                options={this.state.availableOptions}
                getOptionLabel={ (option) => option.label }
                getOptionSelected={this.getOptionSelected}
                renderOption={ (option, { selected }) => (
                    <React.Fragment>
                        { selected && <CheckIcon /> }
                        { option.label }
                    </React.Fragment> ) }
                renderInput={(params) => (
                    <TextField {...params} label={label} variant="outlined" />
                )}
                value={selectedIds}
                onChange={ (event, newValue) => onItemSelect(newValue) }
                onInputChange={ (event, newInputValue) => this.handleFetchList(newInputValue) }
                noOptionsText={noOptionsMessage || "Ничего не найдено"}
                clearText="Очистить"
                openText="Открыть"
                closeText="Закрыть"
                style={{ marginTop: 7 }}
            />
        )
    }

}

SearchSelect.defaultProps = {
    defaultList: [],
};

export default SearchSelect;

★★★

Последнее исправление: damix9 (всего исправлений: 1)

if (prevProps.defaultList !== this.props.defaultList)

сравниваете ссылки на массивы как примитивы?

Syncro ★★★★★
()

За классовые компоненты коллеги ещё не обливают смузи? Или это какое-то легаси?

Nervous ★★★★★
()
Ответ на: комментарий от Syncro

Поправил так, работает.

import React, { Component } from 'react';
import Autocomplete from '@material-ui/lab/Autocomplete';
import TextField from '@material-ui/core/TextField';
import CheckIcon from '@material-ui/icons/Check';

/*

SearchSelect Component

Props:
- selectedIds: array of currently selected options
- onItemSelect: function, a callback function that is called when an option is selected or unselected
- label: string, the caption for the select input
- fetchList: function, a callback function that takes a query and returns a Promise that 
    resolves to an array of available objects from the server. The callback will only 
    be fired if the user has entered at least 3 characters, excluding trailing white spaces.
    Returned array:
    [
        { label: 'Option 1', value: 'option1' },
        { label: 'Option 2', value: 'option2' },
        // ... additional options
    ]
- defaultList: array of the objects available without fetching from server. Has the 
    same format as returned by fetchList.
- noOptionsMessage: string, the message to display when no options are found on the server or no proper query
    
Usage Example:
<SearchSelect
  selectedIds={currentOptions}
  onItemSelect={(selectedItems) => {
    console.log('Selected items:', selectedItems);
  }}
  label="Caption"
  fetchList={(query) => {
    return fetch(https://api.example.com/search?q=${query})
      .then(response => response.json())
      .then(data => data.results);
  }}
/>

*/

class SearchSelect extends Component {
    constructor(props) {
        super(props);
        this.state = {
            availableOptions: [],
            selectedOptions: []
        }
    }
    
    componentDidMount() {
        this.updateOptions(this.props.defaultList);
    }
    
    componentDidUpdate(prevProps) {
        if (prevProps.defaultList !== this.props.defaultList) {
            this.updateOptions(this.props.defaultList);
        }
    }

    updateOptions = (defaultList) => {
        // Update the available options and selected options based on the defaultList
        const availableOptions = [...defaultList];
        const selectedOptions = this.props.selectedIds.map(id => {
            const option = availableOptions.find(option => option.value === id);
            return option || { value: id, label: '' }; // Fallback to an empty label if not found
        });
        this.setState({ availableOptions, selectedOptions });
    }

    handleFetchList = (query) => {
        if (query.trim().length >= 3) {
            this.props.fetchList(query).then((options) => {
                this.setState({
                    availableOptions: options
                });
            });
        } else {
            this.setState({
                availableOptions: this.props.defaultList || []
            });
        }
    }
        
    onChange = (event, newValue) => {
        this.setState({ selectedOptions: newValue });
        this.props.onItemSelect(newValue.map((item) => item.value));
    }

    render() {
        const { label, noOptionsMessage } = this.props;
        return (
            <Autocomplete
                multiple
                options={this.state.availableOptions}
                getOptionLabel={(option) => option.label}
                value={this.state.selectedOptions}
                onChange={this.onChange}
                onInputChange={(event, newInputValue) => this.handleFetchList(newInputValue)}
                noOptionsText={noOptionsMessage || "Ничего не найдено"}
                renderOption={(option, { selected }) => (
                    <React.Fragment>
                        {selected && <CheckIcon />}
                        {option.label}
                    </React.Fragment>
                )}
                renderInput={(params) => (
                    <TextField {...params} label={label} variant="outlined" />
                )}
                style={{ marginTop: 7 }}
            />
        );
    }
}

SearchSelect.defaultProps = {
    defaultList: [],
};

export default SearchSelect;

damix9 ★★★
() автор топика
Ответ на: комментарий от DumLemming

Ой блин, а как щаз модно?

Использовать нечистые (impure) функциональные компоненты, в которых управление состоянием и жизненным циклом идёт через хуки, то есть функции с побочными эффектами, сохраняющие своё состояние где-то в кишках реакта.

static_lab ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.