How can I use useForm's register with dynamic parameter? - reactjs

I want to create a form by using react-hook-form, but I am having trouble with it. As I found, there was a big change at v7 so the code I used as a reference is not working. I tried to modify it, and I figured out the problem is with the registering, but I cannot pass that name parameter dynamically.
My main component.
import React from 'react';
import i18next from 'i18next';
import { Button, Grid, Typography } from '#material-ui/core';
import { useForm, FormProvider } from 'react-hook-form';
import { Link } from 'react-router-dom';
import FormInput from './CustomTextField'
const AddressForm = ({ next }) => {
const methods = useForm();
return (
<>
<Typography variant="h6" gutterBottom>
{i18next.t('shipping_address')}
</Typography>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => {
console.log(data);
next({ ...data })})}>
<Grid container spacing={3}>
<FormInput required register={methods.register} name='lastName' label={i18next.t('last_name')} />
<FormInput required register={methods.register} name='firstName' label={i18next.t('first_name')} />
<FormInput required register={methods.register} name='email' label={i18next.t('mail')} />
<FormInput required register={methods.register} name='zip' label={i18next.t('zip_code')} />
<FormInput required register={methods.register} name='city' label={i18next.t('city')} />
<FormInput required register={methods.register} name='address1' label={i18next.t('address_1')} />
</Grid>
<br />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button component={Link} to="/cart" variant="outlined">
{i18next.t('back_to_cart')}
</Button>
<Button type="submit" variant="contained" color="primary">
{i18next.t('next_step')}
</Button>
</div>
</form>
</FormProvider>
</>
)
}
export default AddressForm
CustomTextField component:
import React from 'react';
import { TextField, Grid } from '#material-ui/core';
import { useFormContext, Controller } from 'react-hook-form';
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={name}
render = {(field) => (
<TextField
{...register({name})} // <--- It is not working like this
label={label}
required={required}
/>
)}
/>
</Grid>
)
}
export default CustomTextField
By the doc: https://react-hook-form.com/api/useform/register
it takes the input field's name as a parameter, if I pass it in as a string, it works fine. How can I pass the name's value as a parameter to the register() function?

The problem is you're mixing RHF's <Controller /> and register. As Mui's <TextField /> is an external controlled component you should use <Controller />. Check the docs here for more info.
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={name}
render={({ field: { ref, ...field } }) => (
<TextField
{...field}
inputRef={ref}
label={label}
required={required}
/>
)}
/>
</Grid>
)
}
If you really want to use register here, you have to remove the wrapping <Controller /> and pass a name as a string instead as an object like you are doing right now. But i would recommend to use <Controller /> as with register you are losing the functionality of setting up the correct ref for your <TextField /> input element as it is linked via the inputRef prop instead of using ref which RHF register uses.
const CustomTextField = ({ name, label, register, required}) => {
const { control } = useFormContext();
return (
<Grid item xs={12} sm={6}>
<TextField
{...register(name)}
label={label}
required={required}
/>
</Grid>
)
}

Related

React Hook Form & Material UI: errors are undefined

I'm using React Hook Form v7 with Material UI v5. I have a simple form component as shown below. I have marked the TextField as required, however, the error state and helper text are never shown when the value of the field is empty. The value of errors.title is always undefined.
What am I doing wrong here? How can I get the React Hook Form validation working, i.e. errors?
import React from 'react';
import Grid from '#mui/material/Grid';
import TextField from '#mui/material/TextField';
import Button from '#mui/material/Button';
import { useForm, Controller } from 'react-hook-form';
import backEndApi from '../api/backEndApi';
const UserForm = function () {
const { control, handleSubmit, formState: { errors }, getValues } = useForm();
const onFormSubmit = (event) => {
event.preventDefault();
backEndApi.post(getValues());
};
return (
<Grid container alignItems="center" justify="center" direction="column" paddingTop="10%">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Grid container alignItems="center" justify="center" direction="column" spacing={5}>
<Grid item>
<Controller
name="title"
control={control}
rules={{ required: true }}
render={({ field }) => {
return (
<TextField
{...field}
id="title"
label="Title"
required
error={errors.title}
helperText={errors.title && 'Title is required'}
/>
);
}}
/>
</Grid>
<Grid item>
<Button id="job-submit" color="primary" type="submit" variant="contained">
Create Job
</Button>
</Grid>
</Grid>
</form>
</Grid>
);
};
export default UserForm;
I think rules conflicts with required of TextField.
I confirmed it works. (https://codesandbox.io/s/practical-chebyshev-9twrle?file=/src/userForm.js)
<Controller
name="title"
control={control}
rules={{ required: true }}
render={({ field }) => {
return (
<TextField
{...field}
id="title"
label="Title"
error={Boolean(errors.title)}
helperText={errors.title && "Title is required"}
/>
);
}}
/>
Also you don't have to wrap with Controller for such a simple case.
Example: https://codesandbox.io/s/practical-chebyshev-9twrle?file=/src/userFormWithoutController.js

Using React Hook Form with other component

I want to create dynamic form with react-hook-form.
Below is my code.
I want to create a dialog for entering a detailed profile in a separate component and use it in MainForm.
I can display the DetailForm, but the values entered in it are not reflected.
Data on the DetailForm component is not included when submitting.
Any guidance on how to do this would be greatly appreciated.
MainForm
import React from 'react';
import {
useForm,
Controller,
useFieldArray
} from 'react-hook-form';
import {
Button,
TextField,
List,
ListItem,
IconButton,
} from '#material-ui/core';
import DetailForm from '#components/DetailForm'
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
function MainForm(props:any) {
const { control, handleSubmit, getValues } = useForm({
mode: 'onBlur',
defaultValues: {
profiles: [
{
firstName: '',
lastName: '',
email: '',
phone: ''
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'profiles',
});
const onSubmit = () => {
const data = getValues();
console.log('data: ', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<List>
fields.map((item, index) => {
return (
<ListItem>
<Controller
name={`profiles.${index}.firstName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="First Name"
/>
}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="Last Name"
/>
}
/>
<DetailForm index={index} />
</ListItem>
)
})
</List>
<IconButton onClick={() => append({})}>
<AddCircleOutlineIcon />
</IconButton>
<Button
type='submit'
>
SAVE
</Button>
</form>
)
}
DetailForm
import React from 'react';
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '#material-ui/core';
export default function DetailForm(props: any) {
const [dialogState, setDialogState] = React.useState<boolean>(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button
onClick={handleOpen}
>
Set Detail Profile
</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<TextField
name={`profiles.${props.index}.email`}
label="Email Address"
/>
<TextField
name={`profiles.${props.index}.phone`}
label="Phone Number"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleClose}>
Add
</Button>
</DialogActions>
</Dialog>
</>
)
}
You have to register those fields inside your <Dialog /> component - just pass control to it. It's also important to set an inline defaultValue when using <Controller /> and useFieldArray. From the docs:
inline defaultValue is required when working with useFieldArray by integrating with the value from fields object.
One other minor thing: You should also pass the complete fieldId to your <Dialog /> component instead of just passing the index. This way it would be easier to change the fieldId in one place instead of editing all fields in your <Dialog /> component.
MainForm
<ListItem key={item.id}>
<Controller
name={`profiles.${index}.firstName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="First Name" />
)}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Last Name" />
)}
/>
<DetailForm control={control} fieldId={`profiles.${index}`} />
</ListItem>
DetailForm
export default function DetailForm({ control, fieldId }) {
const [dialogState, setDialogState] = React.useState(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button onClick={handleOpen}>Set Detail Profile</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<Controller
name={`${fieldId}.email`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Email Address" />
)}
/>
<Controller
name={`${fieldId}.phone`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Phone Number" />
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Add</Button>
</DialogActions>
</Dialog>
</>
);
}

How to display custom <Errormessage> in formik field component and disable the default message

I am making a web project using React, Material, Formik, formik-material-ui.
I have made a Formik form using yup for validation.
const schema = yup.object({
name: yup.string().trim().lowercase().required("Name is required."),
});
<Formik
initialValues={{
name: "",
}}
validationSchema={schema}
>
{({ submitForm, isSubmitting, handleSubmit, handleChange, values }) => (
<Form noValidate onSubmit={handleSubmit}>
<Grid container direction="row" spacing={2}>
<Grid container item xs={12} spacing={4}>
<Grid item xs={4}>
<InputLabel>Patient Name *</InputLabel>
<TextField fullWidth name="name" type="text" />
<InputLabel>Patient ID: P0006</InputLabel>
</Grid>
</Grid>
</Grid>
</Form>
)}
</Formik>
The TextField is a custom component as follows
import React, { Fragment } from "react";
import { Field, ErrorMessage } from "formik";
import { TextField } from "libs/formik-material-ui/src";
const TextFieldStyle = {
padding: 8,
fontSize: "0.75rem",
};
export default React.memo((props: any) => {
return (
<Fragment>
<Field
component={TextField}
inputProps={{
style: TextFieldStyle,
}}
size="small"
margin="none"
variant="outlined"
{...props} // add props at the key to override any user defined similar props
>
{props.children}
</Field>
<ErrorMessage name={props.name}>{(msg) => <div style={{ color: "red", textAlign: "left" }}>{msg}</div>}</ErrorMessage>
</Fragment>
);
});
Since I want to display ErrorMessage of a different style and not the default one, I have added below the field.
But with this approach, the error message is being printed twice.
How can I disable the default message from being printed ?
You can use helperText="" to disabled default message
<Field
component={TextField}
inputProps={{
style: TextFieldStyle,
}}
size="small"
margin="none"
variant="outlined"
helperText=""
{...props} // add props at the key to override any user defined similar props
>
You would need to remove your {msg} and let formik handle the error message for you
And if you want to style the error, use className from formik:
Formik Props Type
export interface ErrorMessageProps {
name: string;
className?: string;
component?: string | React.ComponentType;
children?: ((errorMessage: string) => React.ReactNode);
render?: ((errorMessage: string) => React.ReactNode);
}
So you would need to use it like so
<ErrorMessage name={props.name} className="your-class" />;

ForwardRef warning React-hook-forms with Material UI TextField

I am trying to build a form with react-hook-forms with Material UI's inputs (my custom variant of TextField in this case). Although the form seems to work completely fine, it triggers a warning message in the console when rendering the form.
Warning: Function components cannot be given refs. Attempts to
access this ref will fail. Did you mean to use React.forwardRef()?
I am using react-hook-form's Controller to wrap my TextField (as suggested by the docs)
Any suggestions or solutions are very welcome!
Below both the TextField component and the form where this issue occurs:
Component TextField
const TextField = props => {
const {
icon,
disabled,
errors,
helperText,
id,
label,
value,
name,
required,
...rest
} = props;
const classes = useFieldStyles();
return (
<MuiTextField
{...rest}
name={name}
label={label}
value={value || ''}
required={required}
disabled={disabled}
helperText={helperText}
error={errors}
variant="outlined"
margin="normal"
color="primary"
InputProps={{
startAdornment: icon,
classes: {
notchedOutline: classes.outline,
},
}}
InputLabelProps={{
className: classes.inputLabel,
}}
/>
)
};
TextField.propTypes = {
icon: PropTypes.node,
disabled: PropTypes.bool,
label: PropTypes.string,
id: PropTypes.string,
value: PropTypes.any,
required: PropTypes.bool,
helperText: PropTypes.string,
};
export default TextField;
Component LoginForm
const LoginForm = () => {
const { handleSubmit, errors, control } = useForm();
const onSubmit = values => console.log(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Typography variant="h5" color="primary" gutterBottom>
Login
</Typography>
<Box py={3} height="100%" display="flex" flexDirection="column">
<Controller
as={TextField}
label="Username"
name="username"
control={control}
errors={errors}
required
/>
<Controller
as={TextField}
label="Password"
type="password"
name="password"
control={control}
errors={errors}
required
/>
<Link>
Forgot your password?
</Link>
</Box>
<Button variant="contained" color="primary" fullWidth type="submit">
Submit
</Button>
</form>
)
};
Try to use Controller's render prop instead of as, because TextField's exposed ref is actually called inputRef, while Controller is trying to access ref.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useForm, Controller } from "react-hook-form";
import Header from "./Header";
import { TextField, ThemeProvider, createMuiTheme } from "#material-ui/core";
import "react-datepicker/dist/react-datepicker.css";
import "./styles.css";
import ButtonsResult from "./ButtonsResult";
let renderCount = 0;
const theme = createMuiTheme({
palette: {
type: "dark"
}
});
const defaultValues = {
TextField: "",
TextField1: ""
};
function App() {
const { handleSubmit, reset, control } = useForm({ defaultValues });
const [data, setData] = useState(null);
renderCount++;
return (
<ThemeProvider theme={theme}>
<form onSubmit={handleSubmit((data) => setData(data))} className="form">
<Header renderCount={renderCount} />
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField"
control={control}
rules={{ required: true }}
/>
</section>
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField1"
control={control}
rules={{ required: true }}
/>
</section>
<ButtonsResult {...{ data, reset, defaultValues }} />
</form>
</ThemeProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
you can click the following link for actual behavior, now with ref assigned properly with Controller, we can successfully focus on the field when there is an error for better accessibility.
https://codesandbox.io/s/react-hook-form-focus-74ecu
The warning is completely right as suggested by the official docs it think you did not reach to the functional components part. Link to the offical docs
You cannot give ref to functional components as they do not have instances
If you want to allow people to take a ref to your function component, you can use forwardRef (possibly in conjunction with useImperativeHandle), or you can convert the component to a class.
You can, however, use the ref attribute inside a function component as long as you refer to a DOM element or a class component like this:
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

Material-UI Multiline TextField scrolls horizontally

I'm trying to use Material UI to implement a multiline TextField. Instead of being multi-line the input field is scrolling horizontally. I have replicated the issue in Code Sandbox (link below). It is the Ad Description field in FormItemDetails.js that I want to be multi-line. I'm grateful for any help! Thanks
Code Sandbox
import React, { useState, useEffect } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
export default function FormItemDetails(props) {
const { values, handleChange } = props;
const [ error, setError ] = useState(null)
console.log(values)
const errorDiv = error
? <div className="error">
<i class="material-icons error-icon">error_outline</i>
{error}
</div>
: '';
useEffect(() => setError(null), []);
function cont(e) {
e.preventDefault();
const requiredFields = ['title', 'description', 'price']
for (const key of requiredFields) {
if (!values[key]) {
setError(`A ${key} is required`)
return
}
}
props.nextStep();
};
function back() {
props.prevStep();
}
return (
<MuiThemeProvider>
<>
<h2>Enter the advertisement details</h2>
{errorDiv}
<TextField
required={true}
fullWidth={true}
hintText="Enter Your Ad Title"
floatingLabelText="Ad Title"
onChange={handleChange('title')}
defaultValue={values.title}
/>
<br />
<TextField
required={true}
fullWidth={true}
multiline
hintText="Enter Your Ad Description"
floatingLabelText="Ad Description"
onChange={handleChange('description')}
defaultValue={values.description}
/>
<br />
<TextField
required
fullWidth
hintText="Enter Your Asking Price"
floatingLabelText="Price"
onChange={handleChange('price')}
defaultValue={values.price}
/>
<br />
<RaisedButton
label="Continue"
primary={true}
style={styles.button}
onClick={cont}
/>
<RaisedButton
label="Back"
primary={false}
style={styles.button}
onClick={back}
/>
</>
</MuiThemeProvider>
)
}
Update the material-ui library used in the project. Actually, you're using material-ui/TextField and have to change to #material-ui/core/TextField for achieved expected behaviour in the textField. Check this demo codesandbox.io/s/t8dkp
Important: Check the last library version as well.

Resources