Reputation: 111
I have two autocomplete components and an element that when clicked switches the values between the 2 autocomplete components. I am also switching the options when I do this. In the autocomplete components, I am rendering a hidden field with the selected value. When I click the element that switches the values, I can see in the DOM that the hidden field is being updated correctly between the 2 autocomplete components. I can also see the options being switched when I click on the dropdown. But the label/text field is not being updated. The original label is persisted and not updated to the new label.
Parent component:
import React, { useState, useEffect, Suspense } from 'react';
import classes from './AirForm.module.css';
const AirType = React.lazy(() => import('./Controls/Air/AirType'));
const Airport = React.lazy(() => import('./Controls/Air/Airport'));
const AirForm = props => {
const [airTypeSelected, setAirTypeSelected] = useState('RoundTrip');
const [fromSelected, setFromSelected] = useState();
const [fromOptions, setFromOptions] = useState([]);
const [toSelected, setToSelected] = useState();
const [toOptions, setToOptions] = useState([]);
//const [switchFromTo, setSwitchFromTo] = useState(true);
const switchFromToHandler = () => {
setFromOptions(toOptions);
setToOptions(fromOptions);
setFromSelected(toSelected);
setToSelected(fromSelected);
//setSwitchFromTo(!switchFromTo);
}
//useEffect(() => {
// setFromSelected(toSelected);
// setToSelected(fromSelected);
//}, [switchFromTo]);
return (
<Suspense>
<AirType airTypeSelected={airTypeSelected} setAirTypeSelected={setAirTypeSelected} />
<form action={props.data.AirUrl} method="post">
<div className={classes.destinations}>
<div>
<Airport data={props.data} name="DepartureAirport" label="From" options={fromOptions} setOptions={setFromOptions} optionSelected={fromSelected} setOptionSelected={setFromSelected} onSwitch={switchFromToHandler} includeSwitch={true} />
</div>
<div>
<Airport data={props.data} name="ArrivalAirport" label="To" options={toOptions} setOptions={setToOptions} optionSelected={toSelected} setOptionSelected={setToSelected} includeSwitch={false} />
</div>
</div>
</form>
</Suspense>
);
}
export default AirForm;
Component containing the autocomplete component:
import React, { useState, useMemo, useEffect, useRef } from 'react';
import Styles from '../../../../../Theming/Styles';
import classes from './Airport.module.css';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import { debounce } from '@mui/material';
const Airport = props => {
const [inputValue, setInputValue] = useState('');
const [searching, setSearching] = useState(true);
const [loading, setLoading] = useState(false);
const abortConRef = useRef();
const fetchData = useMemo(() =>
debounce((request, callback) => {
if (!props.data.AirportLookupUrl) {
callback({ success: false });
return;
}
if (abortConRef.current) abortConRef.current.abort();
abortConRef.current = new AbortController();
fetch(props.data.AirportLookupUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ q: request }),
signal: abortConRef.current.signal
})
.then(response => response.json())
.then(result => {
callback(result);
})
.catch((error) => {
if (error.name !== 'AbortError') {
throw error;
}
});
}, 400),
[]
);
useEffect(() => {
if (inputValue === '') {
props.setOptions([]);
return undefined;
}
setLoading(true);
setSearching(true);
fetchData(inputValue, (result) => {
setLoading(false);
setSearching(false);
if (result.success) {
const newOptions = result.value.map(val => {
const label = `(${val.Code})${val.City && ' ' + val.City} - ${val.Name}`;
return {
label: label,
value: val.Code
};
});
props.setOptions(newOptions);
} else {
props.setOptions([]);
}
});
}, [inputValue, fetchData]);
return (
<>
<Autocomplete
disablePortal
options={props.options}
value={props.optionSelected}
autoComplete
autoHighlight={true}
includeInputInList
fullWidth
onChange={(event, value) => { props.setOptionSelected(value); }}
onInputChange={(event, value) => {
if (event.type === 'change') {
setInputValue(value);
}
}}
noOptionsText={searching ? 'Type to search' : 'No options'}
loading={loading}
sx={Styles}
renderInput={(params) => (
<>
<input type="hidden" name={props.name} value={props.optionSelected?.value} />
<TextField
{...params}
label={props.label}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
</>
)}
/>
{props.includeSwitch && <div onClick={props.onSwitch} className={classes.switch}><i class="fas fa-sync" aria-hidden="true"></i></div>}
</>
);
};
export default Airport;
UPDATE
Here is a codesandbox. Not sure why it isn't running in stackoverflow but if you navigate to the sandbox it will run. https://codesandbox.io/s/strange-hertz-d8rtb8 If you select an option in either of the autocomplete components and then click switch button, you will see that the options switched but the selected option doesn't change. A hidden field is rendered for each autocomplete and when you click switch button you can see the hidden field's value is updated correctly but autocomplete displayed text value is never updated.
<iframe src="https://codesandbox.io/embed/strange-hertz-d8rtb8?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="strange-hertz-d8rtb8"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
UPDATE 2
The issue was because I was not setting the initial state for the selected option. Setting the state has fixed the issue.
Upvotes: 2
Views: 11804
Reputation: 466
Solution:
For solution on your codesandbox you need 2 steps to fix your problem;
null
inside App.js
const [fromSelected, setFromSelected] = useState(null);
const [toSelected, setToSelected] = useState(null);
onInputChange
event
inside MyAutocomplete
. Since input change happens on controlled state this is not an event and it was failing.onInputChange={(event, value) => {
if (event?.type === "change") {
setInputValue(value);
}
}}
Suggestions:
Also there are couple of possible improvements i can suggest on here;
const [options, setOptions] = useState({
from: {
selected: null,
options: fromList
},
to: {
selected: null,
options: toList
}
});
//
const handleSetOptions = (area, key, value) => {
setOptions({
...options,
[area]: {
...options[area],
[key]: value
}
});
};
//From Autocompolete Props, To is same just need to change from in here
options={options.from.options}
setOptions={(value) => handleSetOptions("from", "options", value)}
optionSelected={options.from.selected}
const switchFromToHandler = () => {
setOptions((prevOptions) => ({
from: prevOptions.to,
to: prevOptions.from
}));
};
You can check updated version of codesandbox
Upvotes: 6