Home > Mobile >  useFieldArray with Material UI Select doesn't work
useFieldArray with Material UI Select doesn't work

Time:08-21

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..

  1. Set defaultValue doesn't show in Select, but shows in TextField
  2. 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

enter image description here

I wonder what is wrong in there..

Here is my Codesandbox, thanks for help.

Edit Controlled field array (forked)

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"]}

Here's a full working example on CodeSandbox: Edit Controlled field array (forked)

  • Related