I'm working with Autocomplete and Chip component from the mui library. There's the DEMO (standard boilerplate).
I can't get the Chip contents before deleting it:
<Autocomplete
multiple
id="tags-filled"
options={top100Films.map((option) => option.title)}
defaultValue={[top100Films[1].title]}
freeSolo
onKeyDown={(prop) => {
if (prop.key === 'Enter') {
console.log(prop.target.value)
}
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
onDelete={(s) => console.log("the one", option)}
key={index} variant="outlined"
label={option} {...getTagProps({ index })} />
))
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="freeSolo"
placeholder="Favorites"
/>
)}
/>
The issue is this
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
onDelete={(s) => console.log("the one", option)}
key={index} variant="outlined"
label={option} {...getTagProps({ index })} />
))
}
If I remove {...getTagProps({ index })} I do get the onDelete working the way I need it to but then the actual removal doesn't work. Again, the DEMO here
you can use
onChange={(e, value, situation, option) => {
if (situation === "removeOption") {
//write your code here
console.log("--->", e, value, situation, option);
}
setReceivers((state) => value);
}}
instead of the onDelete like this :
import * as React from "react";
import Chip from "#mui/material/Chip";
import Autocomplete from "#mui/material/Autocomplete";
import TextField from "#mui/material/TextField";
import Stack from "#mui/material/Stack";
export default function Tags() {
const [val, setVal] = React.useState({});
const [receivers, setReceivers] = React.useState([]);
console.log(receivers);
const handleClick = () => {
setVal(top100Films[0]); //you pass any value from the array of top100Films
// set value in TextField from dropdown list
};
return (
<Stack spacing={1} sx={{ width: 500 }}>
<Autocomplete
multiple
id="tags-filled"
options={top100Films.map((option) => option.title)}
defaultValue={[top100Films[13].title]}
freeSolo
onChange={(e, value, situation, option) => {
if (situation === "removeOption") {
console.log("--->", e, value, situation, option);
}
setReceivers((state) => value);
}}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="freeSolo"
placeholder="Favorites"
/>
)}
/>
</Stack>
);
}
codesandbox
Related
I am trying to implement multi-select Mui autocomplete.
Whenever user selects an option, I want Chip component to be displayed underneath.
I am using react-form-hook to check validation. categories field is an array, and I want it to have at least one item.
Problem is when I delete a chip, the value does not update properly. I am using react state to keep track of selectedItems so I can display Chip component.
When I delete the chip, it is deleted from selectedItem state, but actual value from Controller does not change.
Please review my code, and give me some feedbacks. Thank you all!!
mui_autocomplete
import React, { useEffect, useState } from 'react';
import { Autocomplete, Chip, FormControl, FormLabel, Stack, TextField } from '#mui/material';
import { Controller } from 'react-hook-form';
import CloseIcon from '#mui/icons-material/Close';
export default function FormAutoComplete({ name, control, label, error, ...props }) {
const [selectedItems, setSelectedItems] = useState([]);
const selectedItemChip = selectedItems.map((item) => {
return (
<Chip
key={item}
label={item}
deleteIcon={<CloseIcon />}
onDelete={() => {
setSelectedItems((prev) => prev.filter((entry) => entry !== item));
}}
/>
);
});
return (
<FormControl fullWidth>
<FormLabel>{label}</FormLabel>
<Controller
name={name}
control={control}
render={({ field: { onChange, value } }) => (
<Autocomplete
multiple
filterSelectedOptions
options={options}
getOptionLabel={(option) => option}
renderTags={() => {}}
value={selectedItems}
onChange={(e, newValue) => {
const addedItem = newValue[newValue.length - 1];
setSelectedItems((prev) => [...prev, addedItem]);
onChange(selectedItems);
return selectedItems;
}}
renderInput={(params) => (
<TextField
{...params}
{...props}
error={!!error}
helperText={error && error.message}
/>
)}
/>
)}
/>
<Stack direction="row" marginTop={2} gap={1} flexWrap="wrap">
{selectedItemChip}
</Stack>
</FormControl>
);
}
export const options = [
'Building Materials',
'Tools',
'Decor & Furniture',
'Bath',
'Doors & Windows',
'Cleaning',
'Electrical',
'Heating & Cooling',
'Plumbing',
'Hardware',
'Kitchen',
'Lawn & Garden',
'Lighting & Fans',
];
Sandbox
Once chip is deleted, react-hook-form is not updated with latest value. So, when form is submitted, old data is logged.
No need to track selectedItems in a state variable. You can use useWatch hook to get latest value from react-hook-form
use setValue to update value in formState
With these changes your FormAutoComplete component should look like this
export default function FormAutoComplete({
name,
control,
label,
error,
setValue, // Pass this prop from parent component. Can be destructured from useForm hook
...props
}) {
// const [selectedItems, setSelectedItems] = useState([]);
const selectedItems = useWatch({control,name}) // import useWatch from react-hook-form
const selectedItemChip = selectedItems.map((item) => {
return (
<Chip
key={item}
label={item}
deleteIcon={<CloseIcon />}
onDelete={() => {
// setSelectedItems((prev) => [...prev.filter((entry) => entry !== item)]);
setValue(name,selectedItems.filter((entry) => entry !== item))
}}
/>
);
});
return (
<FormControl fullWidth>
<FormLabel>{label}</FormLabel>
<Controller
name={name}
control={control}
render={({ field: { onChange, value } }) => (
<Autocomplete
multiple
filterSelectedOptions
options={options}
getOptionLabel={(option) => option}
renderTags={() => {}}
// value={selectedItems}
value={value}
onChange={(e, newValue) => {
// setSelectedItems(newValue);
onChange(newValue);
}}
renderInput={(params) => (
<TextField
{...params}
{...props}
error={!!error}
helperText={error && error.message}
/>
)}
/>
)}
/>
<Stack direction="row" marginTop={2} gap={1} flexWrap="wrap">
{selectedItemChip}
</Stack>
</FormControl>
);
}
Documentation :
https://react-hook-form.com/api/useform/setvalue
https://react-hook-form.com/api/usewatch
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}
/>;
I am trying to add a minimum input string length as three for the filter in autocomplete Box of material UI, My code is given below and it is not working properly. Anybody help me for solving these issues.
const [inputValue, setInputValue] = useState("");
const filterOptions = ( options: string[],{ inputValue }:
FilterOptionsState<string>,)
=>
{
if (inputValue.length > 2) {
return matchSorter(options, inputValue);
} else {
return [];
}
};
<Autocomplete
id={id}
value={value}
inputValue={inputValue}
className={className}
onInputChange={(event: React.ChangeEvent<any>) =>
setInputValue(event?.target?.value)
}
classes={classes}
onChange={handleChange}
ListboxComponent={ListboxComponent}
renderGroup={renderGroup}
options={options || []}
filterOptions={filterOptions}
groupBy={(option) => option[0].toUpperCase()}
renderInput={(params) => (
<TextField {...params} variant="outlined" placeholder="Vendor name" />
)}
renderOption={(option) => {
return (
<>
<Tooltip title={option} placement="right-end">
<Typography noWrap>{option}</Typography>
</Tooltip>
</>
);
}}
open={inputValue !== "" && inputValue?.length > 2}
/>;
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
I want to use onChange event on Autocomplete component to get current selected values.
The problem is that it does not working as expected, so when I click to check/uncheck value checkbox is still unchecked but in console i can see that new value was added
uncoment this part to make it works:
value={myTempVal}
onChange={(event, newValue) => {
setMyTempVal(newValue);
console.log(newValue);
}}
online demo:
https://codesandbox.io/embed/hardcore-snowflake-7chnc?fontsize=14&hidenavigation=1&theme=dark
code:
const [myTempVal, setMyTempVal] = React.useState([]);
<Autocomplete
open
multiple
value={myTempVal}
onChange={(event, newValue) => {
setMyTempVal(newValue);
console.log(newValue);
}}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
You need to get donors receivers and options variables out of the function. Those variables get re-created at each render, this means that their reference changes at each render, and as Autocomplete makes a reference equality check to decide if an option is selected he never finds the options selected.
const donors = [...new Set(data.map(row => row.donor))].map(row => {
return {
groupName: "Donors",
type: "donor",
title: row || "null"
};
});
const receivers = [...new Set(data.map(row => row.receiver))].map(row => {
return {
groupName: "Receivers",
type: "receiver",
title: row || "null"
};
});
const option2 = [...donors, ...receivers];
export const App = props => {
const [myTempVal, setMyTempVal] = React.useState([]);
return (
<Autocomplete
open
multiple
...
You can also add getOptionSelected to overwrite the reference check :
<Autocomplete
open
multiple
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
getOptionSelected={(option, value) => option.title === value.title}
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
This can help:
Replace
checked={selected}
To
checked={myTempVal.filter(obj=>obj.title===option.title).length!==0}
The complete solution
import React from "react";
import "./styles.css";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import CheckBoxOutlineBlankIcon from "#material-ui/icons/CheckBoxOutlineBlank";
import CheckBoxIcon from "#material-ui/icons/CheckBox";
import Checkbox from "#material-ui/core/Checkbox";
import SearchIcon from "#material-ui/icons/Search";
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const data = [
{ donor: "Trader Joe's", receiver: "Person-to-Person" },
{ donor: "Trader Joe's", receiver: "Homes with Hope" },
{ donor: "Santa Maria", receiver: "Gillespie Center" },
{ donor: "Santa Maria", receiver: null }
];
export const App = props => {
const donors = [...new Set(data.map(row => row.donor))].map(row => {
return {
groupName: "Donors",
type: "donor",
title: row || "null"
};
});
const receivers = [...new Set(data.map(row => row.receiver))].map(row => {
return {
groupName: "Receivers",
type: "receiver",
title: row || "null"
};
});
const option2 = [...donors, ...receivers];
const [myTempVal, setMyTempVal] = React.useState([]);
return (
<Autocomplete
open
multiple
value={myTempVal}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
onClick={
()=>{
if(myTempVal.filter(obj=>obj.title===option.title).length!==0){
setMyTempVal([...myTempVal.filter(obj=>obj.title!==option.title)],console.log(myTempVal))
}else{
setMyTempVal([...myTempVal.filter(obj=>obj.title!==option.title),option],console.log(myTempVal))
}
}
}
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={myTempVal.filter(obj=>obj.title===option.title).length!==0}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
);
};
export default App;
It is bit late to Answer this question but it might help someone.
In your code you have added onChange event in Autocomplete. When you click on checkbox it will trigger 2 times, one for checkbox and one for Autocomplte. Hence 2nd time trigger makes again checkbox unchecked so u get value in console but still checkbox is empty.
You can remove your checkbox in renderOption and use checked and uncheked icon instaed of checkbox.
renderOption={(option, { selected }) => {
return (
<React.Fragment>
{selected ? <CheckedIcon> : <uncheckedIcon>}
<div>
{option.title}
</div>
</React.Fragment>
</>
);
}}