I am trying to use useFieldArray for dynamic Select field of Material UI and react-hook-form. It works fine with TextField, but when used with Select it doesn't work..
What doesn't work..
- Set
defaultValue
doesn't show in Select, but shows in TextField - Value selected on Select and inputted value in TextField doesn't come when click in Submit.
Here is the code of Select component
import * as React from "react";
import {
FormControl,
FormHelperText,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent
} from "@mui/material";
import { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Placeholder from "./Placeholder";
export interface IOptionTypes {
id: string;
label: string;
value: string;
}
interface IFormElementTypes {
name: string;
label: string;
required?: boolean;
defaultValue: string;
options: IOptionTypes[];
placeholder: string;
}
export default function MultiSelectField({
name,
label,
required,
defaultValue,
options,
placeholder
}: IFormElementTypes) {
const {
control,
register,
formState: { errors }
} = useFormContext();
// const defaultVal = control._defaultValues[name];
const [selectedVal, setSelectedVal] = useState<string[]>([]);
const handleChange = (event: SelectChangeEvent<typeof selectedVal>) => {
const {
target: { value }
} = event;
setSelectedVal(
// On autofill we get a stringified value.
typeof value === "string" ? value.split(",") : value
);
};
const labelText = `${label}${required ? "*" : ""}`;
const labelId = `multi-select-${name}`;
return (
<Controller
name={name}
defaultValue={defaultValue}
control={control}
render={({ field }) => (
<FormControl fullWidth>
<InputLabel
shrink
sx={{ backgroundColor: "white", padding: "0px 2px" }}
error={!!errors[name]}
id={labelId}
>
{labelText}
</InputLabel>
<Select
{...field}
label={labelText}
labelId={labelId}
value={selectedVal}
displayEmpty
multiple
{...register(`${name}` as const)}
variant="outlined"
fullWidth
error={!!errors[name]}
input={
<OutlinedInput
id={`multi-select-${label}`}
label={`${label}${required ? "*" : ""}`}
/>
}
renderValue={
selectedVal.length > 0
? undefined
: () => <Placeholder>{placeholder}</Placeholder>
}
onChange={handleChange}
>
{options.map((option) => (
<MenuItem key={option.id} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
<FormHelperText>{String(errors[name]?.message ?? "")}</FormHelperText>
</FormControl>
)}
/>
);
}
and How it is been called finally
import * as React from "react";
import {
useForm,
useFieldArray,
FormProvider,
SubmitHandler
} from "react-hook-form";
import { Button, Grid, Stack, Box } from "@mui/material";
import MultiSelectField, { IOptionTypes } from "./MultiSelectField";
import TextFieldElement from "./TextFieldElement";
type FormValues = {
items: { name: string; age: string }[];
};
const defaultValues = {
items: [{ name: "john", age: "10" }]
};
const names: IOptionTypes[] = [
{ id: "1", label: "John", value: "john" },
{ id: "2", label: "Smith", value: "smith" },
{ id: "3", label: "Julia", value: "julia" },
{ id: "4", label: "David", value: "david" }
];
export default function App() {
const methods = useForm<FormValues>({
defaultValues,
mode: "all"
});
const { control, formState, setError } = methods;
const { fields, append, remove } = useFieldArray({ name: "items", control });
const formSubmitHandler: SubmitHandler<FormValues> = async (
data: FormValues
) => {
console.log("Form values ", data);
};
return (
<Grid container>
<Grid item xs={12}>
<Stack direction="row" spacing={2} my={2}>
<Button
variant="outlined"
color="primary"
type="submit"
disableElevation
fullWidth
onClick={() => {
append({ name: "", age: "" });
}}
>
Add Field
</Button>
<Button
variant="outlined"
color="primary"
type="submit"
disableElevation
fullWidth
onClick={() => {
remove(fields.length - 1);
}}
>
Remove Field
</Button>
</Stack>
</Grid>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(formSubmitHandler)}>
<Grid item xs={12}>
<Stack spacing={2}>
{fields.map((field, index) => (
<Stack direction="row" spacing={2} key={field.id}>
<MultiSelectField
name={`items.${index}.name`}
label="Choose Name"
placeholder="Name"
defaultValue=""
options={names}
/>
<TextFieldElement
name={`items.${index}.age`}
label="Input Age"
placeholder="Age"
/>
</Stack>
))}
</Stack>
</Grid>
<Grid container my={2}>
<Grid item>
<Button
variant="outlined"
color="primary"
type="submit"
disableElevation
fullWidth
>
Save Changes
</Button>
</Grid>
</Grid>
</form>
</FormProvider>
</Grid>
);
}
As in the code above, I have set default values, which works for TextField but not for the SelectField
I wonder what is wrong in there..
Here is my Codesandbox, thanks for help.
CodePudding user response:
You're going to want to make sure that you set the value
prop on your Select
inside of MultiSelectField
to selectedVal
:
<Select
{...field}
label={labelText}
labelId={labelId}
value={selectedVal}
displayEmpty
multiple
{...register(`${name}` as const)}
variant="outlined"
fullWidth
error={!!errors[name]}
input={
<OutlinedInput
id={`multi-select-${label}`}
label={`${label}${required ? "*" : ""}`}
/>
Also remove the defaultValue={defaultValue}
from the Controller
in MultiSelectfield
so the Controller
looks like this:
<Controller
name={name}
control={control}
render={({ field }) => (
Since you are using controlled components, we need to make sure we set the default value of selectedVal
to the default value passed in as a prop:
const [selectedVal, setSelectedVal] = useState<string[]>(defaultValue);
selectedVal
is supposed to be a string[]
not a string
, so we should change the type of defaultValue
to a string[]
:
interface IFormElementTypes {
name: string;
label: string;
// eslint-disable-next-line react/require-default-props
required?: boolean;
defaultValue: string[];
options: IOptionTypes[];
placeholder: string;
}
Finally, back in App.tsx
we should pass an array to MultiSelectField
as the defaultValue
prop. Here's what it would look like if we wanted "John" to be selected by default:
<MultiSelectField
name={`items.${index}.name`}
label="Choose Name"
placeholder="Name"
defaultValue={["john"]}
options={names}
/>
Or, if we wanted to have the default be "John" and "Smith" then we could set the default like this:
defaultValue={["john", "smith"]}