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

Posted

技术标签:

【中文标题】使用带有 Material-UI 自动完成功能的 react-hook-form 控制器的正确方法【英文标题】:Proper way to use react-hook-form Controller with Material-UI Autocomplete 【发布时间】:2020-08-22 14:18:43 【问题描述】:

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

TLDR:需要在没有 defaultValue 的情况下将 MUI Autocomplete 与 react-hook-form 控制器一起使用

我的自定义Autocomplete 组件采用结构为_id:'', name: '' 的对象,它显示名称并在选择选项时返回_idAutocomplete 工作得很好。

<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 更改为直接返回值,但它似乎不起作用。

&lt;TextField/&gt; 上使用inputRef=register 对我来说并不合适,因为我想保存_id 而不是name

HERE 是具有两种情况的工作沙箱。第一个在Autocomplete 中使用useEffectsetValue 有效。我第二次尝试使用Controller 组件

感谢任何帮助。

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

任何帮助仍然非常感谢

【问题讨论】:

codesandbox.io/s/react-hook-form-controller-079xx你看到了吗? @Bill 谢谢你的链接,我通过它是一个工作示例,但我现在仍然面临一些与自动完成组件状态相关的其他问题。我已经用 LE 更新了这个问题。谢谢 如果你按照代码框里的内容,应该可以解决问题,对吧? codesandbox.io/s/… 【参考方案1】:

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

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

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

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(的返回值

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

更新:

根据 cmets 中的 Steve 问题,这就是我呈现输入的方式,以便它检查错误:

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

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

const  control, watch, errors, handleSubmit  = formMethods

【讨论】:

我很好奇您如何处理需要自动完成字段 - 对我来说,它似乎不会触发错误 @Steve - 我可以看到错误没有出现的多种原因,具体取决于:验证库和验证模式、组件结构、字段命名(例如,您需要在字段名称中使用索引如果您使用的是数组字段)。如果您提供有关您的案例的更多详细信息,我可能会调整回复。就我而言,我使用yup 进行验证,因此该字段的架构中有.required()。我还将添加一个如何渲染输入的快速示例,以便显示该错误。如果有帮助,请告诉我。【参考方案2】:

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

首先...针对您的问题,您可以通过向&lt;Controller&gt; 添加一个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>
  );

【讨论】:

感谢您抽出宝贵时间回答。设置默认值对我来说不是一个选项,因为这些字段不是强制性的,因此它们必须为空。我还分叉了我的沙箱并使用您的代码进行了更新。清除选项会破坏应用程序,检查它codesandbox.io/s/busy-forest-tmb6s 这两个问题很容易解决。只需要在 getOpObj 和 getOptionSelected 中允许一个空/null 选项。然后,您可以将 null 作为 defaultOption 传递。 CodeSandbox 出于某种原因不允许我登录。不知道您可以在不登录的情况下保存分叉的沙箱。但这似乎奏效了。查看:codesandbox.io/s/angry-bogdan-w9eg8 codesandbox.io/s/…【参考方案3】:
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";
// const useStyles = makeStyles((theme) => (
//   input: 
//     "&:invalid": 
//       borderColor: red
//     
//   ,
//   submit: 
//     margin: theme.spacing(3, 0, 2)
//   
// ));

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

【讨论】:

codesandbox.io/s/… 为我工作。谢谢!【参考方案4】:

不用控制器,借助register,useForm的setValue和value,Autocomplete的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"
                    />
                  )
                />
);

【讨论】:

我们可以,但是我们改变了组件的复杂性...... 这看起来更直接,但上面提到的方法将使其更具可扩展性并且更容易在整个应用程序中重用。由于 Control 组件是在专用的ControlledAutoComplete 文件中定义的,因此我们可以将其导入到应用程序中的任何表单中,并简单地从该表单的文件中传入控件和表单特定选项。如果表单文件中的 api 调用正在检索 AutoComplete 的选项,这将特别方便。

以上是关于使用带有 Material-UI 自动完成功能的 react-hook-form 控制器的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在 onChange 后清除 Material-ui 中的自动完成输入?

Material-ui 自动完成过滤列表

如何在 Material-UI 自动完成控件中自定义填充?

使用 React Material-UI 自动完成始终显示默认选项

material-ui TextField 禁用浏览器自动完成

从 material-ui 自动完成组合框中清除所有选定的值