Estoy tratando de usar un componente Material-UI Autocomplete
personalizado y conectarlo a react-hook-form
.
TLDR: es necesario usar MUI Autocompletar con el controlador de forma de gancho de reacción sin
defaultValue
Mi componente Autocomplete
personalizado toma un objeto con la estructura {_id:'', name: ''}
, muestra el nombre y devuelve el _id
cuando se selecciona una opción. El Autocomplete
funciona bien.
<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} />} />
Para que funcione con react-hook-form
configuré setValues
para que sea el controlador de onChange
en Autocomplete
y registré manualmente el componente en un useEffect
de la siguiente manera
useEffect(() => { register({ name: "country1" }); },[]);
Esto funciona bien, pero me gustaría no tener el gancho useEffect
y simplemente hacer uso del registro de alguna manera directamente.
A continuación, traté de usar el componente Controller
de react-hook-form
para registrar correctamente el campo en el formulario y no usar el gancho 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} />
Cambié onChange
en el componente Autocomplete
para devolver el valor directamente, pero parece que no funciona.
Usar inputRef={register}
en <TextField/>
no me serviría porque quiero guardar el _id
y no el name
AQUÍ hay una caja de arena de trabajo con los dos casos. El primero con useEffect
y setValue
en el Autocomplete
que funciona. El segundo mi intento de usar el componente Controller
Cualquier ayuda es apreciada.
LE
Después del comentario de Bill con el sandbox de trabajo de MUI Autocompletar, logré obtener un resultado funcional
<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} />
El único problema es que me sale un MUI Error
en la consola
Material-UI: un componente está cambiando el estado de valor no controlado de Autocompletar para ser controlado.
Intenté establecer un defaultValue
para él, pero aún se comporta así. Además, no me gustaría establecer un valor predeterminado de la matriz de opciones debido al hecho de que estos campos en el formulario no son obligatorios.
El sandbox actualizado AQUÍ
Cualquier ayuda sigue siendo muy apreciada.
Entonces, arreglé esto. Pero reveló lo que creo que es un error en Autocompletar.
Primero... específicamente para su problema, puede eliminar el MUI Error
agregando un valor predeterminado al <Controller>
. Pero eso fue solo el comienzo de otra ronda o problemas.
El problema es que a las funciones getOptionLabel
, getOptionSelected
y onChange
a veces se les pasa el valor (es decir, el _id
en este caso) y, a veces, se les pasa la estructura de opciones, como era de esperar.
Aquí está el código que finalmente se me ocurrió:
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> ); }
La respuesta aceptada (probablemente) funciona para la versión con errores de Autocompletar. Creo que el error se solucionó algún tiempo después, por lo que la solución se puede simplificar un poco.
Esta es una referencia/codesandbox muy útil cuando se trabaja con react-hook-form y material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5 ?
Desde el enlace anterior, modifiqué el ejemplo de Autocompletar:
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} /> ); }
Con el uso:
<ControlledAutocomplete control={control} name="inputName" options={[{ name: 'test' }]} getOptionLabel={(option) => `Option: ${option.name}`} renderInput={(params) => <TextField {...params} label="My label" margin="normal" />} defaultValue={null} />
el control
es del valor de retorno de useForm(}
Tenga en cuenta que estoy pasando null
como valor defaultValue
ya que en mi caso esta entrada no es necesaria. Si deja defaultValue
, es posible que obtenga algunos errores de la biblioteca material-ui.
ACTUALIZAR:
Según la pregunta de Steve en los comentarios, así es como represento la entrada, para que verifique si hay errores:
renderInput={(params) => ( <TextField {...params} label="Field Label" margin="normal" error={errors[fieldName]} /> )}
Donde errors
es un objeto de formMethods
de react-hook-form
:
const { control, watch, errors, handleSubmit } = formMethods
En lugar de usar el controlador, con la ayuda de register, setValue de useForm y value, onChange of Autocompletar podemos lograr el mismo resultado.
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> ); }