I have an Autocomplete component which displays the coutries name and flags as in the example from the MUI doc.
My goal is simply the following: once the Autocomplete component is clicked, the country's name must be cleared displaying only the placeholder.
I achieved this with a simple onClick event in the renderInput which triggers the following function:
const handleClear = (e) => {
e.target.value = "";
};
If trying the code everything works as expected, apparently.
Actually, the clearing happens only when the country's name is clicked, but if a different portion of the component is clicked, like the flag or the dropdown arrow, the country's name is simply focused, not cleared.
In short, here the current behaviour:
and here the expected behaviour:
Is there a way to fix this?
That's behavior occurs because when you click on the flag, the e.target won´t be the input element, but the wrapper div. You can see this just adding a console.log to the handleClear function:
const handleClear = (e) => {
console.log("clicked TARGET ELEMENT: ", e.target);
// If you click on the input, will see:
// <input ...
// And if you click on the flag, you will see:
// <div ...
};
If you want to control the input state value and the text value separately, you probably should go with the two states control - check it on MUI docs.
The code will be something like:
export default function CountrySelect() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = React.useState("");
const handleClear = (e) => {
console.log("clicked TARGET ELEMENT: ", e.target);
setInputValue("");
};
return (
<Autocomplete
id="country-select-demo"
disableClearable
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
openOnFocus
sx={{ width: 300 }}
options={countries}
autoHighlight
getOptionLabel={(option) => option.label}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.code.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.code.toLowerCase()}.png 2x`}
alt=""
/>
{option.label} ({option.code}) +{option.phone}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label="Choose a country"
placeholder="Choose a country"
onClick={handleClear}
InputProps={{
...params.InputProps,
startAdornment: value ? (
<InputAdornment disablePointerEvents position="start">
<img
loading="lazy"
width="48"
src={`https://flagcdn.com/w20/${value.code.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${value.code.toLowerCase()}.png 2x`}
alt=""
/>
</InputAdornment>
) : null
}}
/>
)}
/>
);
}
Instead of using onClick on TextField, you can use onOpen props and pass handleClear function in it. It works then. Selected value gets cleared whenever autocomplete is open.
Working Demo: CodeSandBox.io
Related
The following will allow you to prepare your own behavior like the one in the title, but it is not possible to do it in the following way
However, since debounce() is called each time onInputChange is performed, if the key is pressed for a long time
This method is not realistic because the processing becomes heavy and the input value becomes choppy.
How can the inputBase value be reset in such a case?
const [q, setQ] = useState('');
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
value: string
) => {
setQ(value);
debounce(value);
};
const handleClickClear = () => {
setQ('');
debounce('');
};
<Autocomplete
getOptionLabel={(option) =>
typeof option === "string" ? option : option.word
}
inputValue={q}
options={data}
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<InputBase
inputProps={{
...params.inputProps,
name: "search",
type: "text",
}}
/>
{q && (
<ButtonBase onClick={handleClickClear}>
<IconButton size="small">
<ClearIcon fontSize="small" />
</IconButton>
</ButtonBase>
)}
</div>
)}
blurOnSelect
freeSolo
openOnFocus
onChange={handleChange}
onFocus={handleFocus}
onInputChange={handleInputChange}
/>;
So I have an async combo box that pulls options from an API. The options are just an Id and a description. This component is part of a form that I'm showing to add or edit data. What I'd like to see happen is have the option be empty when adding new data and to have the current value selected when editing. Instead, it simply shows the label.
This is my code that's almost a copypaste of the example from the docs.
export default function AsyncAutoComplete(props:AsyncAutoCompleteProps) {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState<EntityWithIdAndDescription[]>([]);
const loading = open && options.length === 0;
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
props.populateWith().then((options)=> {
if (active) {
setOptions(options);
}})
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="async-autocomplete"
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={props.onChange}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.description}
options={options}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label={props.label}
variant="outlined"
margin="dense"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
What I want is to pass an Id value to this component and have it show the description for that value as the selected option (after making the API call). Using defaultValue doesn't seem to work.
Any advice with either modifying this or taking a different approach would be helpful.
Looks like what you're after is a controlled component. There's an example of this in Material UI's Autocomplete demo under Playground called controlled.
I'm not sure how you're getting the id of the initial value you want to pass to the component to show that it's selected. It could be something like the following.
Create a separate state for the value you select from this Autocomplete in your parent component. In fact, I would not have a separate component called AsyncAutocomplete at all. This is so you control all your state in the parent component and the Autocomplete component becomes purely presentational.
After your API call is complete and the setOptions(options) is called, call setValue with the value that you would like to show selected. This must be of type EntityWithIdAndDescription.
Create an inline-function for the onChange prop of the Autocomplete component which takes a the second parameter as the EntityWithIdAndDescription | null type. This is what's required from Autocomplete's onChange. Call setValue with this parameter as the argument.
Pass options, value, onChange and loading as props into the Autocomplete component. The additional props I've passed over and above what you've done in your code are:
<Autocomplete
...
disabled={loading}
value={value}
...
/>
Let me know how you go
const [value, setValue] = useState<EntityWithIdAndDescription | null>(null); // (1)
const [options, setOptions] = useState<EntityWithIdAndDescription[]>([]);
const loading = options.length === 0;
useEffect(() => {
populateWith().then((options)=> {
setOptions(options);
})
// (2)
setValue({
id: "something",
description: "something",
})
return () => {};
}, []);
// (4)
<Autocomplete
id="async-autocomplete"
disabled={loading}
onChange={(event: any, newValue: EntityWithIdAndDescription | null) => {
setValue(newValue); // (3)
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.description}
options={options}
loading={loading}
value={value}
renderInput={(params) => (
<TextField
{...params}
label={"My Entities"}
variant="outlined"
margin="dense"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
Code Sandbox
Here's an example with the Autocomplete in a separate component I called MyAutocomplete. It includes an API call and setting a value I want to be selected first.
https://codesandbox.io/s/autumn-silence-lkjrf
import React, { useEffect, useRef } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
export default function FreeSolo(props) {
const [vendors, setVendors] = React.useState([]);
const [value, setValue] = React.useState();
const nameRef = useRef();
useEffect(() => {
sendDataToParent();
}, [value]);
const sendDataToParent = async () => {
await props.parentFunction(value);
};
return (
<div style={{}}>
<Autocomplete
freeSolo
id="free-solo-2-demo"
options={props.vendorData.map((option) => option.name)}
renderInput={(params) => (
<TextField
{...params}
value={value}
required
inputRef={nameRef}
onChange={(e) => {
setValue(e.target.value);
sendDataToParent();
}}
label="Vendor Name"
margin="normal"
variant="standard"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
</div>
);
}
I tried to do it using renderOption but could not get it working. I need to have the options to be clickable links so that whenever user selects of the options, he is redirected to the link.
EDIT: Solved using renderOption
renderOption={(option) => (
<React.Fragment>
<span
style={{ cursor: "pointer" }}
onClick={() => {
window.location.href = `/allvendors/${option.id}`;
}}
>
{option.name} - Click to visit the Vendor
</span>
</React.Fragment>
)}
Instead of making the options clickable links, you can redirect to the link using the onChange prop of the Autocomplete component.
I'm assuming each option in your vendorData has a name and also a link e.g.
{
name: "Google",
link: "https://www.google.com"
}
To be able to access the link from this object in the Autocomplete component's onChange, you'll need to change the options map function to return the whole option. After this change, if you try to click to open the dropdown, it will throw an error because the option label needs to be a string (e.g. the option name) and not an object (e.g. option). So, we need to add the getOptionLabel prop and return the option.name.
Finally, in the onChange function, we set the window.location.href equal to the option.link, which changes the current page's URL to the link and directs the user to that link.
<div style={{}}>
<Autocomplete
freeSolo
id="free-solo-2-demo"
getOptionLabel={(option) => option.name}
options={props.vendorData.map((option) => option)}
onChange={(event: any, option: any) => {
window.location.href = option.link;
}}
renderInput={(params) => (
<TextField
{...params}
value={value}
required
inputRef={nameRef}
onChange={(e) => {
setValue(e.target.value);
sendDataToParent();
}}
label="Vendor Name"
margin="normal"
variant="standard"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
</div>
I have 2 autocomplete fields on my view by default, but you can add more fields per click.
Currently I have the problem that every autocomplete field opens every result list of every autocomplete field, because every field uses "open". How do I get it to implement the whole thing dynamically?
const [value, setValue] = useState<string>('')
const [open, setOpen] = useState(false)
<Autocomplete
options={props.results.map((option) => option.name)}
renderOption={(option) => (
<Typography noWrap>
{option}
</Typography>
)}
onClose={() => {
setOpen(false)
}}
open={open}
renderInput={(params) => (
<Paper className={search.root} ref={params.InputProps.ref}>
<IconButton className={search.iconButton} disabled>
<FiberManualRecordIcon color="secondary" />
</IconButton>
<InputBase
{...params.inputProps}
className={search.input}
placeholder="Test"
value={value}
onChange={(event: any) => setValue(event.target.value)}
/>
<IconButton
className={search.iconButton}
disabled={!value}
>
<SearchIcon />
</IconButton>
</Paper>
)}
/>
Probably you already solved this due to the date of this issue. But I'm gonna put here what I did to resolve this problem because I was facing the same issue using FieldArray from Formik.
Material UI documentation sometimes is a little confusing, so try to ignore some stuff in their examples.
Remove the open, onOpen, and onClose props. You just need these props if you want to create some kind of automatic opening/closing mechanic.
Here it's an example of what I was doing and how I solved it.
<Autocomplete
id={`items.${index}.partNumber`}
name={`items.${index}.partNumber`}
freeSolo
style={{ width: 300 }}
open={openPartNumber}
onOpen={() => setOpenPartNumber(true)}
onClose={() => setOpenPartNumber(false)}
options={partNumbers}
clearOnBlur={false}
getOptionLabel={option => (option.label ? option.label : '')}
value={item.partNumber}
inputValue={item.partNumber}
onInputChange={(event, newInputValue) => {
if (event) {
setFieldValue(`items.${index}.partNumber`, newInputValue);
getPartNumberList(newInputValue);
}
}}
onChange={(event, optionSelected, reasson) => {
if (reasson === 'select-option') {
setFieldValue(`items.${index}.partNumber`, optionSelected.partNumber);
setFieldValue(`items.${index}.vendorNumber`, optionSelected.vendorNumber);
setFieldValue(`items.${index}.description`, optionSelected.description);
setFieldValue(`items.${index}.ncm`, optionSelected.ncm);
}
}}
filterOptions={x => x}
...otherPrps...
/>
Then I just remove the props open, onOpen and onClose.
<Autocomplete
id={`items.${index}.partNumber`}
name={`items.${index}.partNumber`}
freeSolo
style={{ width: 300 }}
options={partNumbers}
clearOnBlur={false}
getOptionLabel={option => (option.label ? option.label : '')}
value={item.partNumber}
inputValue={item.partNumber}
onInputChange={(event, newInputValue) => {
if (event) {
setFieldValue(`items.${index}.partNumber`, newInputValue);
getPartNumberList(newInputValue);
}
}}
onChange={(event, optionSelected, reasson) => {
if (reasson === 'select-option') {
setFieldValue(`items.${index}.partNumber`, optionSelected.partNumber);
setFieldValue(`items.${index}.vendorNumber`, optionSelected.vendorNumber);
setFieldValue(`items.${index}.description`, optionSelected.description);
setFieldValue(`items.${index}.ncm`, optionSelected.ncm);
}
}}
filterOptions={x => x}
...otherPrps...
/>
If you want automatic opening/closing mechanics to exist. One suggestion is to control open starting from an array.
In the onOpen and onClose props, your callback must control an array by adding and removing the AutoComplete ID and in open just check the existence of this ID inside the array with array.includes(index).
const [value, setValue] = useState<string>('')
//const [open, setOpen] = useState(false)
const [inputsOpen, setInputsOpen] = useState([])
function automatedOpening(id) {
if (id && inputsOpen.length === 0) {
setInputsOpen([...inputsOpen, id]);
}
}
// lenght must be zero to ensure that no other autocompletes are open and will be true on the Open prop
function automatedClosing(id) {
if (id && inputsOpen.length !== 0) {
setInputsOpen(inputsOpen.filter(item => item !== id));
}
}
<Autocomplete
options={props.results.map((option) => option.name)}
renderOption={(option) => (
<Typography noWrap>
{option}
</Typography>
)}
onClose={(e) => automatedClosing(e.id)}
onOpen={(e) => automatedClosing(e.id)}
open={inputsOpen.includs(id)}
renderInput={(params) => (
<Paper className={search.root} ref={params.InputProps.ref}>
<IconButton className={search.iconButton} disabled>
<FiberManualRecordIcon color="secondary" />
</IconButton>
<InputBase
{...params.inputProps}
className={search.input}
placeholder="Test"
value={value}
onChange={(event: any) => setValue(event.target.value)}
/>
<IconButton
className={search.iconButton}
disabled={!value}
>
<SearchIcon />
</IconButton>
</Paper>
)}
/>
you'll probably need to find a way to make the id/index available to the callbacks, but it's open to what you think is best
In the hooks version of material UI I can't seem to be able to clear the autocomplete after an onChange event:
// #flow
import React, { useRef, useState } from "react";
import "./Autocomplete.scss";
import AutocompleteUI from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
function Autocomplete(props) {
const { options } = props;
const [value, setValue] = useState();
const container = useRef();
const input = useRef();
function onChange(event, newValue) {
if (!newValue) return;
props.onChange(newValue);
setValue(undefined);
input.current.value = "";
event.target.value = "";
}
function renderInput(params) {
return (
<TextField
inputRef={input}
{...params}
inputProps={{
...params.inputProps,
autoComplete: "disabled", // disable autocomplete and autofill
}}
margin="none"
fullWidth
/>
);
}
return (
<div className="Autocomplete-container">
{value}
<AutocompleteUI
ref={container}
options={options}
autoHightlight={true}
clearOnEscape={true}
autoSelect={true}
// freeSolo={true}
getOptionLabel={option => option.title}
renderInput={renderInput}
value={value}
onChange={onChange}
/>
</div>
);
}
export default Autocomplete;
Diving into the source code I've noticed the component uses useAutocomplete hook internally. However, neither setInputValue nor resetInputValue which live internally inside that hook are exposed outside. Is there a way to accomplish an input clear after an onChange?
You need to set the inputValue prop to your valueState and on onhange function just clear the valueState
<Autocomplete
inputValue={valueState}
onChange={(value, option) =>
{
setOptions([])
setValueState("")
}}
renderInput={params => (
<TextField
dir="rtl"
onChange={(event) =>
{
setValueState(event.target.value)
}}
{...params}
label="Search Patient"
variant="filled"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
I had the same issue and I solved it with this :
const [search, setSearch] = useState("");
...
<Autocomplete
id="grouped-demo"
options={tagsList}
getOptionLabel={(option) => option.tag}
onChange={(event, value) =>value ? setSearch(value.tag) : setSearch(event.target.value)}
style={{width: 700}}
renderInput={(params) => <TextField {...params} label="Search" variant="outlined"/>}
/>
I encountered a similar scenario using Autocomplete/Textfield in a Search/Nav bar. The value would always be left behind after using a history.push or other Router function in the onChange event. The trick is to set the inputValue = "" . Every time the component renders, the previous value will be removed. See below
<Autocomplete
{...defaultProps}
onChange={(event, value) => {
if(value)
router.history.push(`/summary`);
}}
filterOptions={filterOptions}
clearOnEscape={true}
inputValue=""
renderInput={params => <TextField {...params}
label="Quick Search" fullWidth
InputProps={{...params.InputProps,
'aria-label': 'description',
disableUnderline: true,
}}/>}
/>
yo! I'm pretty sure the Textfield component from material takes an "autoComplete" prop, and you can pass that the string "false". Also, it does not go in inputProps, try that out.
<Textfield autoComplete="false" />
I had the same issue and I solved it with this :
const [value, setValue] = useState(null);
Then you don't need to use refs.