React MUI Autocomplete with Formik and custom render options - reactjs

I am trying to implement React Material UI Autocomplete component by using Formik (setFormValue) and custom renderOption property, so I don't use TextField component, but custom input field.
Filtering options works as expected, so when I enter some value in custom input, the results are displayed depending on what I typed in, but I have an issue with selecting an option. When I click when I click one of the offered ones, nothing happens.
I created FilterDropdown component so it can be reusable. Please see my code and tell me where I wrong.
I did debugging through Google chrome breakpoints, and when I click on any available dropdown option, nothing happens.
filter-dropdown/index.tsx
import { createElement } from 'react'
import { Divider } from '#eframe-ui/react'
import { FilterDropdownProps, DropdownOptionProps } from './types'
import { Translate } from '../translate'
import { Autocomplete } from '#mui/material'
import { FilterDropdownInput } from './Input'
export const FilterDropdown = ({
data,
dropdownDataLayout,
selectFieldLabel,
fieldValue,
errors,
setFieldValue,
}: FilterDropdownProps) => {
return (
<Autocomplete
freeSolo
className="gap-y-1 flex flex-col"
options={data}
getOptionLabel={(option: string | DropdownOptionProps) =>
(option as { value: string }).value
} // filter value
onChange={(e, value) => setFieldValue(fieldValue, value)}
renderInput={(params) => (
<FilterDropdownInput
error={errors}
params={params}
label={selectFieldLabel}
/>
)}
renderOption={(props, option: DropdownOptionProps) => {
const { label, mixin } = option
return dropdownDataLayout
? createElement(dropdownDataLayout, mixin as Record<string, string>)
: label && (
<div className="flex flex-col w-full">
<Translate label={label} />
<Divider className="!my-2" />
</div>
)
}}
/>
)
}
Input.tsx
import { TextField } from '#/../ui'
import { useTranslation } from 'react-i18next'
import { AutocompleteRenderInputParams } from '#mui/material'
interface FilterDropdownProps {
params: AutocompleteRenderInputParams
label: string
error: string | JSX.Element | undefined
}
export const FilterDropdownInput = ({
params,
label,
error,
}: FilterDropdownProps) => {
const { t } = useTranslation()
return (
<div ref={params.InputProps.ref}>
<TextField
{...params.inputProps}
label={t(label)}
placeholder={t('common:Field.Search', 'Enter search term')}
error={!!error}
helperText={error}
/>
</div>
)
}
transfer-assets.tsx
const TransferAssetsLayout = (option: Record<string, string>) => (
<div
className="funds flex flex-col w-full cursor-pointer"
onClick={() => setFieldValue('pension_fund_name', option.name)}
>
<p className="text-text-primary typo-regular-300 px-2">{option.name}</p>
{option.recipient && (
<p
className={clsx(
classes.italic,
'text-text-secondary typo-regular-200 px-2 pb-1',
)}
>
{option.recipient}
</p>
)}
<p
className={clsx(
classes.italic,
'text-text-secondary typo-regular-200 px-2 pb-1',
)}
>
{`${option.address}, ${option.zip_code} ${option.location}`}
</p>
<Divider className="!m-0" />
</div>
)
<FilterDropdown
data={pensionFundsData}
dropdownDataLayout={TransferAssetsLayout}
selectFieldLabel="USM-PortfolioEdit:TransferAssets.InstitutionName"
fieldValue={'pension_fund_name'}
buttonLabel={pension_fund_name}
setFieldValue={() => setFieldValue}
errors={errors.pension_fund_name}
/>

You need to apply the MUI input props or Autocomplete loses track of your custom input.
Instead of this:
renderInput={(params) => (
<FilterDropdownInput
error={errors}
params={params}
label={selectFieldLabel}
/>
)}
Use this:
renderInput={(params) => (
<FilterDropdownInput
{...params}
error={errors}
params={params}
label={selectFieldLabel}
/>
)}
Example working sandbox of MUI Autocomplete passing props down to through renderInput. You must use {...params} exactly as can be verified.
<Autocomplete
options={someData.sort((a, b) => a.value - b.value)}
getOptionLabel={(option) => option.label}
value={value}
onChange={handleChange}
disabled={(someData || []).length >= 5}
renderInput={(params) => (
<TextField
{...params}
size="small"
label="Type some stuff"
variant="outlined"
/>
)}
/>

I fixed the issue , this is my solution
import { createElement, HTMLAttributes } from 'react'
import { Divider } from '#eframe-ui/react'
import { FilterDropdownProps, DropdownOptionProps } from './types'
import { Translate } from '../translate'
import { Autocomplete, Paper } from '#mui/material'
import { FilterDropdownInput } from './Input'
export const FilterDropdown = ({
data,
dropdownDataLayout,
selectFieldLabel,
fieldValue,
errors,
setFieldValue,
}: FilterDropdownProps) => {
/* eslint-disable #typescript-eslint/no-explicit-any */
const CustomPaper = (props: any) => (
<Paper {...props} sx={{ background: '#f4f7fa', marginTop: '4px' }} />
)
return (
<Autocomplete
freeSolo
className="gap-y-1 flex flex-col"
options={data}
getOptionLabel={(option: string | DropdownOptionProps) =>
(option as { value: string }).value
} // filter value
onChange={(e, value) => setFieldValue(fieldValue, value)}
renderInput={(params) => (
<FilterDropdownInput
{...params}
error={errors}
params={params}
label={selectFieldLabel}
/>
)}
renderOption={(
props: HTMLAttributes<HTMLLIElement>,
option: DropdownOptionProps,
) => {
const { label, mixin } = option
return (
<>
<li {...props} className="w-full cursor-pointer">
{dropdownDataLayout
? createElement(
dropdownDataLayout,
mixin as Record<string, string>,
)
: label && (
<>
<Translate label={label} />
<Divider className="!my-2" />
</>
)}
</li>
</>
)
}}
PaperComponent={CustomPaper}
/>
)
}

Related

React MUI Autocomplete - Is it possible to add independent option at the rendered option list?

At the end of the list, it should be visible option Other which should be static (manually added) option.
The idea is to add it manually to the DOM and should be independent from all other dynamically created list elements.
My question, is that possible and if so, how ?
When user click on that option, additional form input should appear, but that logic is already implemented, I just need to trigger click handler and use
setFieldValue('pension_fund_name', CommonDropdownItems.Other)
import { createElement, HTMLAttributes } from 'react'
import { Divider } from '#eframe-ui/react'
import { FilterDropdownProps, DropdownOptionProps } from './types'
import { Translate } from '../translate'
import { Autocomplete, Paper } from '#mui/material'
import { FilterDropdownInput } from './Input'
export const FilterDropdown = ({
data,
dropdownDataLayout,
selectFieldLabel,
fieldValue,
errors,
setFieldValue,
}: FilterDropdownProps) => {
/* eslint-disable #typescript-eslint/no-explicit-any */
const CustomPaper = (props: any) => (
<Paper {...props} sx={{ background: '#f4f7fa', marginTop: '4px' }} />
)
return (
<Autocomplete
freeSolo
className="gap-y-1 flex flex-col"
options={data}
getOptionLabel={(option: string | DropdownOptionProps) =>
(option as { value: string }).value
} // filter value
onChange={(e, value) => setFieldValue(fieldValue, value)}
renderInput={(params) => (
<FilterDropdownInput
error={errors}
params={params}
label={selectFieldLabel}
/>
)}
renderOption={(
props: HTMLAttributes<HTMLLIElement>,
option: DropdownOptionProps,
state
) => {
const { label, mixin } = option
return (
<>
<li {...props} className="w-full cursor-pointer">
{dropdownDataLayout
? createElement(
dropdownDataLayout,
mixin as Record<string, string>,
)
: label && (
<>
<Translate label={label} />
<Divider className="!my-2" />
</>
)}
</li>
</>
)
}}
PaperComponent={CustomPaper}
/>
)
}
Dynamic options are rendered through this layout
const TransferAssetsLayout = (option: Record<string, string>) => (
<>
<p className="text-text-primary typo-regular-300 px-2 pt-1">
{option.name}
</p>
{option.recipient && (
<p
className={clsx(
classes.italic,
'text-text-secondary typo-regular-200 px-2 pb-1',
)}
>
{option.recipient}
</p>
)}
<p
className={clsx(
classes.italic,
'text-text-secondary typo-regular-200 px-2 pb-1',
)}
>
{`${option.address}, ${option.zip_code} ${option.location}`}
</p>
<Divider className="!m-0" />
</>
)

Material UI autocomplete with react-form-hook validation value is not changing properly

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

In autocomplete from materials ui how can I can click a button and clear the autocomplete like make it empty

The autocomplete looks like this
<Autocomplete options={speceificLocation.locationOptions} onChange = {(event,value) => (speceificLocation.locationOptions.includes(value)) ? dispatch({allCities:state.allCities, mappedCities:true}):dispatch({allCities:state.allCities, mappedCities:false})} renderInput = {(params) => <TextField {...params} label = 'Cities'/>}/>
Here is a live demo of an Autocomplete that can be cleared by a button.
import * as React from "react";
import Button from "#mui/material/Button";
import TextField from "#mui/material/TextField";
import Autocomplete from "#mui/material/Autocomplete";
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState<string | null>(options[0]);
const [inputValue, setInputValue] = React.useState("");
return (
<div>
<div>{`value: ${value !== null ? `'${value}'` : "null"}`}</div>
<div>{`inputValue: '${inputValue}'`}</div>
<br />
<Button onClick={() => setValue("")}>Clear</Button>
<br />
<br />
<Autocomplete
value={value}
onChange={(event: any, newValue: string | null) => {
setValue(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
id="controllable-states-demo"
options={options}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Controllable" />}
/>
</div>
);
}

Display number of matching search options on Textfield input in Material UI Autocomplete

I am trying to customize the Material UI Autocomplete and show a count of options that are currently shown in a Popper menu after a user searches and enters a string in the search box (this is before you select an option and it becomes a value which is easy to get a count of with value.length). I may be missing the prop in order to grab the number of options showing out of the entire option array when a user enters a keystroke in the <Textfield/> of the renderInput prop in the <Autocomplete/>.
Here is the code:
<Autocomplete
value={value}
onClose={handleClose}
onChange={(event, newValue) => {
setValue(newValue);
}}
options={options}
getOptionLabel={option => option.title}
renderInput={params => (
<React.Fragment>
<TextField
{...params}
variant="underlined"
placeholder="Search"
/>
<span className={classes.visibleFilterNum}>
Showing X of {options.length}
</span>
</React.Fragment>
)}
/>
Here is the component so far, need to get a constantly updated count for the showing X of total number of options
Here is a codepen of something similar pulled from Material UI example docs for a better example of the starting set of code
It is straightforward using the useAutocomplete hook since it exposes the groupedOptions variable (which is the filtered options).
Here's an example:
export default function UseAutocomplete() {
const classes = useStyles();
const {
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: "use-autocomplete-demo",
options: top100Films,
getOptionLabel: (option) => option.title
});
return (
<div>
<div {...getRootProps()}>
<label className={classes.label} {...getInputLabelProps()}>
useAutocomplete
</label>
<input className={classes.input} {...getInputProps()} />
</div>
{groupedOptions.length > 0 ? (
<>
<div>
Showing {groupedOptions.length} of {top100Films.length}
</div>
<ul className={classes.listbox} {...getListboxProps()}>
{groupedOptions.map((option, index) => (
<li {...getOptionProps({ option, index })}>{option.title}</li>
))}
</ul>
</>
) : null}
</div>
);
}
groupedOptions is not exposed in any straightforward way by the Autocomplete component, but it is possible to determine the number of displayed options by inspecting the HTML to count the options after it renders. The example below does this by overriding the Paper component. The options are displayed within the Paper element and all the options receive a data-option-index attribute. The example below uses these aspects to count the options using querySelectorAll from the Paper element:
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import Paper from "#material-ui/core/Paper";
const NumResultsHeader = ({ children, ...other }) => {
const headerRef = React.useRef();
const countRef = React.useRef();
const paperRef = React.useRef();
React.useEffect(() => {
const numOptions = paperRef.current.querySelectorAll(
"li[data-option-index]"
).length;
countRef.current.innerHTML = numOptions;
if (numOptions > 0) {
headerRef.current.style.display = "block";
} else {
headerRef.current.style.display = "none";
}
});
return (
<>
<div ref={headerRef} style={{ display: "none" }}>
Showing <span ref={countRef}></span> of {top100Films.length}
</div>
<Paper {...other} ref={paperRef}>
{children}
</Paper>
</>
);
};
export default function ComboBox() {
return (
<Autocomplete
id="combo-box-demo"
options={top100Films}
getOptionLabel={(option) => option.title}
style={{ width: 300 }}
PaperComponent={NumResultsHeader}
renderInput={(params) => (
<TextField {...params} label="Combo box" variant="outlined" />
)}
/>
);
}

Material-UI Autocomplete onChange not updates value

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>
</>
);
}}

Resources