使用带有 Material-UI 自动完成功能的 react-hook-form Controller 的正确方法

Proper way to use react-hook-form Controller with Material-UI Autocomplete

我正在尝试使用自定义 Material-UI Autocomplete 组件并将其连接到 react-hook-form

TLDR: Need to use MUI Autocomplete with react-hook-form Controller without defaultValue

我的自定义Autocomplete组件将对象带有结构{_id:'', name: ''}它显示名称,return s _id选择了一个选项。 Autocomplete 工作得很好。

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

为了使其与 react-hook-form 一起工作,我将 setValues 设置为 AutocompleteonChange 的处理程序并手动注册组件useEffect 如下

useEffect(() => {
  register({ name: "country1" });
},[]);

这很好用,但我不想使用 useEffect 挂钩,而是直接以某种方式使用寄存器。

接下来,我尝试使用 react-hook-form 中的 Controller 组件在表单中正确注册字段,而不是使用 useEffect 挂钩

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

我直接将Autocomplete组件中的onChange改成了return的值,但是好像不行。

<TextField/> 上使用 inputRef={register} 对我来说不合适,因为我想保存 _id 而不是 name

HERE 是一个包含这两种情况的工作沙箱。第一个 useEffectsetValueAutocomplete 中起作用。第二次尝试使用 Controller component

感谢任何帮助。

LE

在 Bill 对 MUI 自动完成的工作沙箱发表评论后,我设法得到了一个功能性结果

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

唯一的问题是我在控制台中得到一个 MUI Error

Material-UI: 某个组件正在将Autocomplete的不受控值状态更改为受控。

我已经尝试为它设置 defaultValue,但它仍然如此。此外,我不想从选项数组中设置默认值,因为表单中的这些字段不是必需的。

更新后的沙盒HERE

任何帮助仍然非常感谢

所以,我解决了这个问题。但它揭示了我认为是自动完成中的错误。

首先……针对您的问题,您可以通过向 <Controller> 添加 defaultValue 来消除 MUI Error。但这只是另一轮或问题的开始。

问题是 getOptionLabelgetOptionSelectedonChange 的函数有时传递值(即本例中的 _id),有时传递期权结构 - 如您所料。

这是我最终想出的代码:

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

已接受的答案(可能)适用于有缺陷的自动完成版本。我认为该错误在之后的一段时间内得到了修复,因此可以稍微简化解决方案。

这在 reference/codesandbox 使用 react-hook-form 和 material-ui 时非常有用:https://codesandbox.io/s/react-hook-form-controller-601-j2df5?

从上面link,我修改了自动完成的例子:

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

与用法:

<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 来自 useForm(}

的 return 值

请注意,我将 null 作为 defaultValue 传递,因为在我的情况下不需要此输入。如果您离开 defaultValue,您可能会从 material-ui 库中得到一些错误。

更新:

根据史蒂夫评论中的问题,这就是我呈现输入的方式,以便它检查错误:

renderInput={(params) => (
                  <TextField
                    {...params}
                    label="Field Label"
                    margin="normal"
                    error={errors[fieldName]}
                  />
                )}

其中 errors 是来自 react-hook-formformMethods 的对象:

const { control, watch, errors, handleSubmit } = formMethods

不使用controller,借助于register,useForm的setValue和autocomplete的value,onChange我们也可以达到同样的效果。

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

感谢所有其他答案,截至 2022 年 4 月 15 日,我能够弄清楚如何让它工作并在 TextField 组件中呈现标签:

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,
};

在我的例子中,options 是一个包含 {code: 'US', label: 'United States'} 个对象的数组。最大的区别是 getOptionLabel,我想如果在打开列表时(并且 option 是一个对象)和在 TextField 中呈现选项时都需要考虑到这一点(when option is a string) as well as when nothing is selected.