I've built a form in React using MUI and React Hook Form. I'm trying to create a custom TextField element that works as a Select Input. I would like it to be an uncontrolled component with a Ref prop. I've tried to pass the inputRef prop as the MUI and React Hook Form docs recommend but with no success.
<TextField
id="id"
name="name"
select
native="true"
className={classes.textField}
label="label"
margin="normal"
variant="outlined"
inputRef={register({ required: "Choose one option" })}
error={!!errors.name}
>
<MenuItem value="">Choose one option</MenuItem>
<MenuItem value="3">03</MenuItem>
<MenuItem value="6">06</MenuItem>
<MenuItem value="9">09</MenuItem>
<MenuItem value="12">12</MenuItem>
<MenuItem value="16">16</MenuItem>
<MenuItem value="18">18</MenuItem>
</TextField>
One thing that I've found is that if I use the native select with ref, it works just fine.
Besides, I tried to change the inputRef prop to a SelectProps one but it didn't work too.
Using Select component from Material-ui with react hook form need you to implement custom logic with a Controller https://react-hook-form.com/api#Controller
Here is a reusable component that will hopefully simplify the code to use that Select component in your app:
import FormControl from "#material-ui/core/FormControl";
import InputLabel from "#material-ui/core/InputLabel";
import Select from "#material-ui/core/Select";
import { Controller } from "react-hook-form";
const ReactHookFormSelect = ({
name,
label,
control,
defaultValue,
children,
...props
}) => {
const labelId = `${name}-label`;
return (
<FormControl {...props}>
<InputLabel id={labelId}>{label}</InputLabel>
<Controller
as={
<Select labelId={labelId} label={label}>
{children}
</Select>
}
name={name}
control={control}
defaultValue={defaultValue}
/>
</FormControl>
);
};
export default ReactHookFormSelect;
You can use it in your app like this:
<ReactHookFormSelect
id="numero_prestacao"
name="numero_prestacao"
className={classes.textField}
label="Em quantas parcelas?"
control={control}
defaultValue={numero_prestacao || ""}
variant="outlined"
margin="normal"
>
<MenuItem value="">Escolha uma opção</MenuItem>
<MenuItem value="3">03 parcelas</MenuItem>
<MenuItem value="6">06 parcelas</MenuItem>
<MenuItem value="9">09 parcelas</MenuItem>
<MenuItem value="12">12 parcelas</MenuItem>
<MenuItem value="16">16 parcelas</MenuItem>
<MenuItem value="18">18 parcelas</MenuItem>
</ReactHookFormSelect>
Here is your codeSandBox updated with this component for the selects in the Information form:
https://codesandbox.io/s/unit-multi-step-form-kgic4?file=/src/Register/Information.jsx:4406-5238
RHF v7 update
Below is a minimal code example of MUI Select in a RHF form:
const { formState, getValues, watch, register, handleSubmit } = useForm();
const { errors } = formState;
<TextField
select
fullWidth
label="Select"
defaultValue=''
inputProps={register('currency', {
required: 'Please enter currency',
})}
error={errors.currency}
helperText={errors.currency?.message}
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
Accepted version is correct but outdated.
At least in the version that I'm using: "react-hook-form": "^7.30.0" you should use the render parameter.
Here is the "updated" version that perfectly works for me:
<FormControl>
<InputLabel id="level-label">Level</InputLabel>
<Controller
name="level"
id="level"
defaultValue={level}
control={control}
render={({ field }) => (
<Select labelId="level-label" {...field}>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
</Select>
)}
/>
<FormHelperText error={true}>{errors.level?.message}</FormHelperText>
</FormControl>
The important here is to propagate the field properties down to the child element (Select in our case)
PS. I don't think you need a separate component for it, it is pretty straight forward.
[Updated]
Here is a full code of one of my dialog. As per request from Deshan.
import {
Box, Chip, FormControl, Input, Stack,
} from '#mui/material';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import debounce from '../#utils/debounce';
import useRawParams from '../#utils/useRawParams';
import { useBrandsSearchQuery } from '../data/products';
import { SearchRoute } from '../SBRoutes';
import LoadingDiv from './LoadingDiv';
import SBDialog from './SBDialog';
import { useSearchBarContext } from '../contexts/SearchBarContext';
const context = { suspense: false };
/**
* Show the modal dialog with the list of brands, and search box for it
* Eeach brand will be as a link, for the SEO purposes
*/
export default function AllBrandsDialog({ open, setOpen }) {
const [t] = useTranslation();
const [query, setQuery] = useState('');
const [brands, setBrands] = useState([]);
const params = useRawParams(true);
const paramsBrands = params.brands?.split(',') || [];
const { setFilterActive } = useSearchBarContext();
const variables = useMemo(() => (query.length ? {
filterText: query,
} : null), [query]);
const [{ data, fetching: loading }] = useBrandsSearchQuery({ variables, pause: Boolean(!variables), context });
const debounceSetQuery = useCallback(debounce(200, (text) => {
setQuery(text);
}));
useEffect(() => {
if (!data || !open) return;
setBrands(data.brands || []);
}, [data, open]);
return (
<SBDialog open={open} setOpen={setOpen} title={t('Search and select a brand')}>
<Stack direction="column" spacing={2}>
<FormControl>
<Input
id="tagSearch"
placeholder={t('Start typing to see the brands')}
onChange={(e) => debounceSetQuery(e.target.value)}
autoFocus={true}
/>
</FormControl>
<Box display="grid" width={220} height={300} overflow="auto" gap={1} position="relative">
{brands?.map((brand) => (
<Chip
component={Link}
key={brand.id}
disabled={paramsBrands.indexOf(brand.url) > -1}
to={SearchRoute.generatePath({
...params,
brands: [...paramsBrands, brand.url],
page: undefined,
})}
size="small"
label={brand.nicename}
variant="outlined"
onClick={() => {
setOpen(false);
setFilterActive(false);
}}
clickable={true}
/>
))}
{loading && <LoadingDiv modal={true} />}
</Box>
</Stack>
</SBDialog>
);
}
AllBrandsDialog.propTypes = {
open: PropTypes.bool.isRequired,
setOpen: PropTypes.func.isRequired,
};
Here my code that working, hope it can help, need to use setValue
<TextField
fullWidth
inputRef={register({
name: 'name',
})}
select
onChange={e => setValue('name', e.target.value, true)}
label={label}
defaultValue={defaultValue}
>
{options.map((option) => (
<MenuItem key={option.label} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
Here using native select, do not need setValue, but value alway string
<TextField
fullWidth
select
SelectProps={{
native: true,
inputProps: { ref: register, name: 'name' }
}}
label={label}
defaultValue={defaultValue}
>
{options.map((option) => (
<option key={option.label} value={option.value}>
{option.label}
</option>
))}
</TextField>
This is an example that uses Material-UI with React hook form. You need to add the validation in 'inputRef' prop of TextField. Also you need to add 'onChange' function to keep the state updated. 'shouldValidate' will trigger the validation.
<TextField
select
name='city'
inputRef={register({ required: true })}
onChange={e => setValue('city', e.target.value, { shouldValidate: true })}
label="City"
defaultValue="">
{cityList.map((option, index) => (
<MenuItem key={index} value={option}>
{option}
</MenuItem>
))}
</TextField>
{errors.city && <ErrorText>City is required</ErrorText>}
✔ I came across this same issue, and this is how i solved mine:
<Select ... onChange={e => register({ name: 'academicLevel', value: e.target.value })}/>
more info
When you using react-hook-form with material UI, you don´t need to use onChange and setState. Only use inputRef and all works!
Just need to pass the register to the Input Ref
<Select
variant="outlined"
name="reason"
inputRef={register({ required: true })}
>
just use mui-react-hook-form-plus
Here is an example:
import { HookSelect, useHookForm } from 'mui-react-hook-form-plus';
const defaultValues = {
person: {
firstName: 'Atif',
lastName: 'Aslam',
sex: '',
},
};
const App = () => {
const { registerState, handleSubmit } = useHookForm({
defaultValues,
});
const onSubmit = (_data: typeof defaultValues) => {
alert(jsonStringify(_data));
};
return (
<HookSelect
{...registerState('person.sex')}
label='SEX'
items={[
{ label: 'MALE', value: 'male' },
{ label: 'FEMALE', value: 'female' },
{ label: 'OTHERS', value: 'others' },
]}
/>
)
}
Repo: https://github.com/adiathasan/mui-react-hook-form-plus
Demo: https://mui-react-hook-form-plus.vercel.app/?path=/docs/
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
I created a Select using React that allows the user to select multiple options.
The problem is that the Select displays the ID of the selected item, instead of their name.
How can I change the code in a way that the Select display the names separated by commas (now shows the IDs separated by commas), while keeping the array of ids for later processing.
Any idea how to fix it? Here is the code to CodeSanbox
I have the following array in a Material UI Select:
const names = [
{ id: "1", value: "Oliver Hansen" },
{ id: "2", value: "Van Henry" },
{ id: "3", value: "Omar Alexander" }
];
This is the code that renders the Multiple Select:
<Select
labelId="demo-mutiple-checkbox-label"
id="demo-mutiple-checkbox"
multiple
value={personName}
name="first"
onChange={handleChange}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => selected.join(", ")}
>
{names.map((name) => (
<MenuItem key={name.id} value={name.id}>
<Checkbox checked={personName.indexOf(name.id) > -1} />
<ListItemText primary={name.value} />
</MenuItem>
))}
</Select>
I found one possible solution for your issue.
check if it works for you.
import React from "react";
import { makeStyles } from "#material-ui/core/styles";
import { Input, OutlinedInput } from "#material-ui/core";
import InputLabel from "#material-ui/core/InputLabel";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import MenuItem from "#material-ui/core/MenuItem";
import ListItemText from "#material-ui/core/ListItemText";
import Checkbox from "#material-ui/core/Checkbox";
const useStyles = makeStyles((theme) => ({
formControl: {
margin: theme.spacing(1),
minWidth: 300
},
selectEmpty: {
marginTop: theme.spacing(2)
}
}));
const names = [
{ id: "1", value: "Oliver Hansen" },
{ id: "2", value: "Van Henry" },
{ id: "3", value: "Van Henry" }
];
export default function NativeSelects() {
const classes = useStyles();
const [personName, setPersonName] = React.useState([]);
const handleChange = (event) => {
const {
target: { value }
} = event;
setPersonName(
// On autofill we get a the stringified value.
typeof value === "string" ? value.split(",") : value
);
};
return (
<div>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="age-native-simple">
Names here to select from
</InputLabel>
<Select
labelId="demo-mutiple-checkbox-label"
id="demo-mutiple-checkbox"
multiple
value={personName}
name="first"
onChange={handleChange}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => selected.map(obj=> names[obj - 1].value).join(", ")}
>
{names.map((name) => (
<MenuItem key={name.id} value={name.id}>
<Checkbox checked={personName.indexOf(name.id) > -1} />
<ListItemText primary={name.value} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
}
Updated Code
just you can do this:
renderValue={
(selected) =>
names.filter( name => selected.includes(name.id) )
.map( record => record.name )
.join(", ")
}
To show the selected user's names, you can update the map part, where currently, you're using id.
You can update this to use the name.value to store/show the person's names.
{names.map((name) => (
<MenuItem key={name.value} value={name.value}>
<Checkbox checked={personName.indexOf(name.value) > -1} />
<ListItemText primary={name.value} />
</MenuItem>
))}
Updated Sandbox
the simple steps I did, just focus on the renderValue property, on the Select component:
renderValue={(selected) => names.find((val) => val.id === selected).value}
the logic I use, find the value in the 'names' array, where the id is the selected id, then take the 'value' value in the 'names' array, to display.
Update the Menu Items' value to be a object instead of an id.
<Select
labelId="demo-mutiple-checkbox-label"
id="demo-mutiple-checkbox"
multiple
value={personName}
name="first"
onChange={handleChange}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => selected.map((item) => item.value)?.join(",")}
>
{names.map((name) => (
<MenuItem key={name.id} value={name}>
<Checkbox
checked={personName.find((p) => p.id === name.id) !== undefined}
/>
<ListItemText primary={name.value} />
</MenuItem>
))}
</Select>
I am having an error when submiting a form build using React Hook Form and material-ui checkboxes components. The number of checkboxes are build from a list from my api:
<Grid item xs={12}>
<FormControl
required
error={errors.project?.stack ? true : false}
component='fieldset'>
<FormLabel component='legend'>Tech Stack</FormLabel>
<FormGroup>
<Grid container spacing={1}>
{techs.map((option, i) => (
<Grid item xs={4} key={i}>
<FormControlLabel
control={
<Checkbox
id={`stack${i}`}
name='project.stack'
value={option.id}
inputRef={register({required: 'Select project Tech Stack'})}
/>
}
label={option.name}
/>
</Grid>
))}
</Grid>
</FormGroup>
<FormHelperText>{errors.project?.stack}</FormHelperText>
</FormControl>
</Grid>
When the form is submited I got the following error ( several times , 1 for each checkbox rendered ) :
Uncaught (in promise) Error: Objects are not valid as a React child
(found: object with keys {type, message, ref}). If you meant to render
a collection of children, use an array instead.
I don't understand this error. The message apparently says it is a rendering issue, but the component renders fine. The problems happens on submit. Any advices ?
Thank you
UPDATE: if you are using RHF >= 7, you should use props.field to call props.field.value and props.field.onChange.
You can use the default Checkbox Controller:
<FormControlLabel
control={
<Controller
name={name}
control={control}
render={({ field: props }) => (
<Checkbox
{...props}
checked={props.value}
onChange={(e) => props.onChange(e.target.checked)}
/>
)}
/>
}
label={label}
/>
I used the controller example from RHF but had to add checked={props.value}:
https://github.com/react-hook-form/react-hook-form/blob/master/app/src/controller.tsx
I managed to make it work without using Controller.
The props should be inside the FormControlLabel and not inside Checkbox
<Grid item xs={4} key={i}>
<FormControlLabel
value={option.id}
control={<Checkbox />}
label={option.name}
name={`techStack[${option.id}]`}
inputRef={register}
/>
</Grid>
))}
If someone struggle to achieve multiselect checkbox with React material-ui and react-hook-form you can check my codesandbox example
Also, there is a code example provided by react-hook-form in their documentation under useController chapter (don't forget to switch to the checkboxes tab).
Any of that examples works, I´m using this one:
const checboxArray = [{
name: '1h',
label: '1 hora'
},
{
name: '12h',
label: '12 horas'
},
{
name: '24h',
label: '24 horas'
},
{
name: '3d',
label: '3 dias'
},
];
//This inside render function:
{
checboxArray.map((checboxItem) => (
<Controller name = {
checboxItem.name
}
control = {
control
}
key = {
checboxItem.name
}
rules = {
{
required: true
}
}
render = {
({
field: {
onChange,
value
}
}) =>
<
FormControlLabel
control = { <Checkbox
checked = {!!value
}
onChange = {
(event, item) => {
onChange(item);
}
}
name = {
checboxItem.name
}
color = "primary" /
>
}
label = {
checboxItem.label
}
/>
}
/>
))
}
Material UI + React Hook Form + Yup .
Example page:
https://moiseshp.github.io/landing-forms/
Without extra dependences
Show and hide error messages
// import { yupResolver } from '#hookform/resolvers/yup'
// import * as yup from 'yup'
const allTopics = [
'React JS',
'Vue JS',
'Angular'
]
const defaultValues = {
topics: []
}
const validationScheme = yup.object({
topics: yup.array().min(1),
})
const MyForm = () => {
const resolver = yupResolver(validationScheme)
const {
control,
formState: { errors },
handleSubmit
} = useForm({
mode: 'onChange',
defaultValues,
resolver
})
const customSubmit = (data) => alert(JSON.stringify(data))
return (
<form onSubmit={handleSubmit(customSubmit)}>
<FormControl component="fieldset" error={!!errors?.topics}>
<FormLabel component="legend">Topics</FormLabel>
<FormGroup row>
<Controller
name="topics"
control={control}
render={({ field }) => (
allTopics.map(item => (
<FormControlLabel
{...field}
key={item}
label={item}
control={(
<Checkbox
onChange={() => {
if (!field.value.includes(item)) {
field.onChange([...field.value, item])
return
}
const newTopics = field.value.filter(topic => topic !== item)
field.onChange(newTopics)
}}
/>
)}
/>
))
)}
/>
</FormGroup>
<FormHelperText>{errors?.topics?.message}</FormHelperText>
</FormControl>
</form>
)
}
export default MyForm
Here is the simplest way to do it using Controller
<Box>
<Controller
control={control}
name={`${dimension.id}-${dimension.name}`}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<FormControlLabel
control={
<Checkbox checked={value} onChange={onChange} />
}
/>
)}
/>
</Box>
I am trying to use a custom Material-UI Autocomplete component and connect it to react-hook-form.
TLDR: Need to use MUI Autocomplete with react-hook-form Controller without defaultValue
My custom Autocomplete component takes an object with the structure {_id:'', name: ''} it displays the name and returns the _id when an option is selected. The Autocomplete works just fine.
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) => {
handler(name, reason === 'clear' ? null : newValue._id);
}}
renderInput={params => <TextField {...params} {...inputProps} />}
/>
In order to make it work with react-hook-form I've set the setValues to be the handler for onChange in the Autocomplete and manually register the component in an useEffect as follows
useEffect(() => {
register({ name: "country1" });
},[]);
This works fine but I would like to not have the useEffect hook and just make use of the register somehow directly.
Next I tried to use the Controller component from react-hook-form to proper register the field in the form and not to use the useEffect hook
<Controller
name="country2"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) =>
reason === "clear" ? null : newValue._id
}
renderInput={params => (
<TextField {...params} label="Country" />
)}
/>
}
control={control}
/>
I've changed the onChange in the Autocomplete component to return the value directly but it doesn't seem to work.
Using inputRef={register} on the <TextField/> would not cut it for me because I want to save the _id and not the name
HERE is a working sandbox with the two cases. The first with useEffect and setValue in the Autocomplete that works. The second my attempt in using Controller component
Any help is appreciated.
LE
After the comment from Bill with the working sandbox of MUI Autocomplete, I Managed to get a functional result
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, { _id }]) => _id}
control={control}
/>
The only problem is that I get an MUI Error in the console
Material-UI: A component is changing the uncontrolled value state of Autocomplete to be controlled.
I've tried to set an defaultValue for it but it still behaves like that. Also I would not want to set a default value from the options array due to the fact that these fields in the form are not required.
The updated sandbox HERE
Any help is still very much appreciated
The accepted answer (probably) works for the bugged version of Autocomplete. I think the bug was fixed some time after that, so that the solution can be slightly simplified.
This is very useful reference/codesandbox when working with react-hook-form and material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5?
From the above link, I modified the Autocomplete example:
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
const ControlledAutocomplete = ({ options = [], renderInput, getOptionLabel, onChange: ignored, control, defaultValue, name, renderOption }) => {
return (
<Controller
render={({ onChange, ...props }) => (
<Autocomplete
options={options}
getOptionLabel={getOptionLabel}
renderOption={renderOption}
renderInput={renderInput}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
onChange={([, data]) => data}
defaultValue={defaultValue}
name={name}
control={control}
/>
);
}
With the usage:
<ControlledAutocomplete
control={control}
name="inputName"
options={[{ name: 'test' }]}
getOptionLabel={(option) => `Option: ${option.name}`}
renderInput={(params) => <TextField {...params} label="My label" margin="normal" />}
defaultValue={null}
/>
control is from the return value of useForm(}
Note that I'm passing null as defaultValue as in my case this input is not required. If you'll leave defaultValue you might get some errors from material-ui library.
UPDATE:
Per Steve question in the comments, this is how I'm rendering the input, so that it checks for errors:
renderInput={(params) => (
<TextField
{...params}
label="Field Label"
margin="normal"
error={errors[fieldName]}
/>
)}
Where errors is an object from react-hook-form's formMethods:
const { control, watch, errors, handleSubmit } = formMethods
So, I fixed this. But it revealed what I believe to be an error in Autocomplete.
First... specifically to your issue, you can eliminate the MUI Error by adding a defaultValue to the <Controller>. But that was only the beginning of another round or problems.
The problem is that functions for getOptionLabel, getOptionSelected, and onChange are sometimes passed the value (i.e. the _id in this case) and sometimes passed the option structure - as you would expect.
Here's the code I finally came up with:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "#material-ui/core";
import { Autocomplete } from "#material-ui/lab";
import { Button } from "#material-ui/core";
export default function FormTwo({ options }) {
const { register, handleSubmit, control } = useForm();
const getOpObj = option => {
if (!option._id) option = options.find(op => op._id === option);
return option;
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => getOpObj(option).name}
getOptionSelected={(option, value) => {
return option._id === getOpObj(value)._id;
}}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, obj]) => getOpObj(obj)._id}
control={control}
defaultValue={options[0]}
/>
<Button type="submit">Submit</Button>
</form>
);
}
import { Button } from "#material-ui/core";
import Autocomplete from "#material-ui/core/Autocomplete";
import { red } from "#material-ui/core/colors";
import Container from "#material-ui/core/Container";
import CssBaseline from "#material-ui/core/CssBaseline";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import AdapterDateFns from "#material-ui/lab/AdapterDateFns";
import LocalizationProvider from "#material-ui/lab/LocalizationProvider";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
export default function App() {
const [itemList, setItemList] = useState([]);
// const classes = useStyles();
const {
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
mode: "onChange",
defaultValues: { item: null }
});
const onSubmit = (formInputs) => {
console.log("formInputs", formInputs);
};
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setValue("item", { id: 3, name: "item3" });
}, [setValue]);
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Controller
control={control}
name="item"
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Autocomplete
onChange={(event, item) => {
onChange(item);
}}
value={value}
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
renderInput={(params) => (
<TextField
{...params}
label="items"
margin="normal"
variant="outlined"
error={!!errors.item}
helperText={errors.item && "item required"}
required
/>
)}
/>
)}
/>
<button
onClick={() => {
setValue("item", { id: 1, name: "item1" });
}}
>
setValue
</button>
<Button
type="submit"
fullWidth
size="large"
variant="contained"
color="primary"
// className={classes.submit}
>
submit
</Button>
</form>
</Container>
</LocalizationProvider>
);
}
I do not know why the above answers did not work for me, here is the simplest code that worked for me, I used render function of Controller with onChange to change the value according to the selected one.
<Controller
control={control}
name="type"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo
options={['field', 'select', 'multiple', 'date']}
onChange={(event, values) => onChange(values)}
value={value}
renderInput={(params) => (
<TextField
{...params}
label="type"
variant="outlined"
onChange={onChange}
/>
)}
/>
)}
Thanks to all the other answers, as of April 15 2022, I was able to figure out how to get this working and render the label in the TextField component:
const ControlledAutocomplete = ({
options,
name,
control,
defaultValue,
error,
rules,
helperText,
}) => (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
render={({ field }) => (
<Autocomplete
disablePortal
options={options}
getOptionLabel={(option) =>
option?.label ??
options.find(({ code }) => code === option)?.label ??
''
}
{...field}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(error)}
helperText={helperText}
/>
)}
onChange={(_event, data) => field.onChange(data?.code ?? '')}
/>
)}
/>
);
ControlledAutocomplete.propTypes = {
options: PropTypes.arrayOf({
label: PropTypes.string,
code: PropTypes.string,
}),
name: PropTypes.string,
control: PropTypes.func,
defaultValue: PropTypes.string,
error: PropTypes.object,
rules: PropTypes.object,
helperText: PropTypes.string,
};
In my case, options is an array of {code: 'US', label: 'United States'} objects. The biggest difference is the getOptionLabel, which I guess needs to account for if both when you have the list open (and option is an object) and when the option is rendered in the TextField (when option is a string) as well as when nothing is selected.
I have made it work pretty well including multiple tags selector as follow bellow. It will work fine with mui5 and react-hook-form 7
import { useForm, Controller } from 'react-hook-form';
import Autocomplete from '#mui/material/Autocomplete';
//setup your form and control
<Controller
control={control}
name="yourFiledSubmitName"
rules={{
required: 'required field',
}}
render={({ field: { onChange } }) => (
<Autocomplete
multiple
options={yourDataArray}
getOptionLabel={(option) => option.label}
onChange={(event, item) => {
onChange(item);
}}
renderInput={(params) => (
<TextField {...params} label="Your label" placeholder="Your placeholder"
/>
)}
)}
/>
Instead of using controller, with the help of register, setValue of useForm and value, onChange of Autocomplete we can achieve the same result.
const [selectedCaste, setSelectedCaste] = useState([]);
const {register, errors, setValue} = useForm();
useEffect(() => {
register("caste");
}, [register]);
return (
<Autocomplete
multiple
options={casteList}
disableCloseOnSelect
value={selectedCaste}
onChange={(_, values) => {
setSelectedCaste([...values]);
setValue("caste", [...values]);
}}
getOptionLabel={(option) => option}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option}
</React.Fragment>
)}
style={{ width: "100%" }}
renderInput={(params) => (
<TextField
{...params}
id="caste"
error={!!errors.caste}
helperText={errors.caste?.message}
variant="outlined"
label="Select caste"
placeholder="Caste"
/>
)}
/>
);
I'm using Material UI Select as a reusable component, and I want to validate it with react-hook-form by passing props from parent to child component. So far, I've tried to use from RHF, and to pass some props to the child, but somehow error wont disappear when I select option. This is my code
import React from 'react';
import styled from '#emotion/styled';
import { Select, MenuItem } from '#material-ui/core';
import { ASSelect } from '../../ASSelect';
import { Controller, useForm } from 'react-hook-form';
const { register, handleSubmit, watch, errors, control, setValue } = useForm();
const onSubmit = (data: any) => {
console.log(errors);
};
const defaultDashboard = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
const Parent = () => {
return (
<Controller
as={
<ASSelect
menuItems={defaultDashboard}
label="Default dashboard*"
handleChange={dashboardHandler}
value={dashboardValue}
}
name="Select"
control={control}
rules={{ required: true }}
onChange={([selected]) => {
return { value: selected };
}}
/>
{errors.Select ? <span>Default dashboard is required</span> : null}
)
}
export {Parent};
const ASSelect = ({menuItems, label, handleChange, value}) => {
return (
<div>
<Select>
{menuItems.map((el, index)=> {
return <MenuItem key={index}>{el.label}</MenuItem>
}}
</Select>
</div>
)}
export {ASSelect};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
So when I submit a form without selecting anything, the error will popup. But when I select an option, the error will remain there. What am I doing wrong?
You can use Select with Controller from react-hook-forms in the following way:
const SelectController = ({ name, menuItems, control }) => (
<Controller
as={
<Select>
{menuItems.map(({ value, label }, index) => (
<MenuItem key={index} value={value}>
{label}
</MenuItem>
))}
}
</Select>
}
name={name}
control={control}
defaultValue=""
rules={{ required: true }}
/>
);
or use Select with setValue from react-hook-forms as follows:
const SelectSetValue = ({ name, menuItems, setValue }) => {
return (
<Select
name={name}
defaultValue=""
onChange={e => setValue(name, e.target.value)}
>
{menuItems.map(({ value, label }, index) => (
<MenuItem key={index} value={value}>
{label}
</MenuItem>
))}
}
</Select>
);
};
In both cases you get error-validation initially onSubmit event. Error display is updated afterwards for SelectController when you select a value and for SelectSetValue when you select a value and re-trigger onSubmit.
You can check a working example at following the link:
https://codesandbox.io/s/somaterialselecthookform-kdext?file=/src/App.js