latest react-hook-form error handling with material-ui TextField - reactjs

I have difficulties, using react-hook-form with material-ui.
I prepared a codesandbox example.
import { TextField } from "#material-ui/core";
import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
interface IMyForm {
vasarlo: string;
}
export default function App() {
const {
handleSubmit,
formState: { errors },
register
} = useForm<IMyForm>();
const onSubmit = (data: IMyForm) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>First Name</label>
<TextField
variant="outlined"
margin="none"
label="Test"
{...register("vasarlo", {
required: "error text"
})}
error={errors?.vasarlo ? true : false}
helperText={errors?.vasarlo ? errors.vasarlo.message : null}
/>
<input type="submit" />
</form>
);
}
How can I properly use the register function, to get error message, write to input field, also the onSubmit function to work?
I couldn't find answer in the documentation for this scenario.

In react-hook-form v7, this is how you register an input:
<input {...register('name')} />
Calling register() will return necessary props for your input like onChange, onBlur and ref. These props make it possible for react-hook-form to keep track of your form data. Now when you use register with Material-UI TextField like this:
<TextField {...register('name')} />
You pass the ref property directly to the TextField while the correct place to put it is in the inputRef:
<TextField inputRef={ref} />
So you have to modify your code like this:
const { ref: inputRef, ...inputProps } = register("vasarlo", {
required: "error text"
});
<TextField inputRef={inputRef} {...inputProps} />
How can I properly use the register function, to get error message
There is nothing wrong with your error handling code. Though you can shorten you code a bit more using Typescript's optional chaining operator ?.:
<TextField
error={!!errors.vasarlo}
helperText={errors?.vasarlo?.message}
inputRef={ref}
{...inputProps}
/>
Live Demo

You're misusing Controller. react-hook-form's default functionality is using uncontrolled inputs. Remove <Controller/> and put this on your TextField
inputRef={register({
required: 'This is required',
validate: (data) => myValidationFunction(data)
})}
You'll only want to use a controller if you NEED to modify/intercept/format a value that is being displayed in the TextField that is different from what a user is typing, i.e a phone number getting shown as (xxx)-xxx-xxxx when only typing the digits.

Related

Passing value to hidden input from dropdown menu in react

I have react-select dropdown menu and hidden input which I pass to form when submiting...
using useState hook I created variable which tracks changes to react-select dropdown menu.
Hidden input has this variable as value also. I thought this would be enough.
But when I submit the form, console. log shows me that value of input is empty despite that variable that was selected from dropdown menu is actually updated.
I mean variable that I have chosen console logs some value, but hidden input thinks that it is still empty.
Does it means I have to rerender manually page each time I change that variable so input gets it's new value using useEffect ? Which is bad solution for me, I don't like it, thought it would be done automatically.
Or instead of useState I must create and use variable via Redux ? Which I also don't like, use redux for such small thing fills overcomplicated.
Isn't there any nice elegant solution ? :)
import { useForm } from 'react-hook-form';
const [someVar,setSomeVar]=useState('');
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ mode: 'onBlur' });
const handleFormSubmit = (data) => {
console.error('success');
};
const handleErrors = (errors) => {
console.error(errors);
console.log(document.getElementsByName('hiddenInput')[0].value);
};
const options = {
hiddenInput: {
required: t('hiddenInput is required'),
},
};
.......
<form onSubmit={handleSubmit(handleFormSubmit, handleErrors)}>
<Select
options='...some options list'
onChange={(value) => setSomeVar(value)}
/>
<input
name='hiddenInput'
value={someVar}
{...register('hiddenInput', options.hiddenInput)}
/>
<button>submit</button>
</form>
UPDATED
Its because getElementsByName returns an array of elements.
You probably want
document.getElementsByName('hiddenInput')[0].value
I should add that really you should use a ref attached to the input and not access it via the base DOM API.
const hiddenRef = useRef(null)
// ...
cosnt handleSubmit =(e)=>{
console.log(hiddenRef.current.value);
}
// ...
<input
name='hiddenInput'
value={someVar}
ref={hiddenRef}
/>
However as you are using react-hook-form you need to be interacting with its state store so the library knows the value.
const {
register,
handleSubmit,
formState: { errors },
setValue
} = useForm({ mode: 'onBlur' });
// ...
<form onSubmit={handleSubmit(handleFormSubmit, handleErrors)}>
<Select
options='...some options list'
onChange={(value) => setValue('hiddenInput', value)}
/>
<input
name='hiddenInput'
{...register('hiddenInput', options.hiddenInput)}
/>
<button>submit</button>
</form>
You can remove const [someVar,setSomeVar]=useState('');
However, this hidden input is not really necessary as you mention in comments. You just need to bind the dropdown to react hook form.
// controller is imported from react hook form
<form onSubmit={handleSubmit(handleFormSubmit, handleErrors)}>
<Controller
control={control} // THIS IS FROM useForm return
name="yourDropdown"
rules={{required: true}}
render={({
field: { onChange, value, name, ref }
}) => (
<Select
options={options}
inputRef={ref}
value={options.find(c => c.value === value)}
onChange={val => onChange(val.value)}
/>
)}
/>
<button>submit</button>
</form>

react hook form's getValues() returning undefined

The Question:
How do I access the form's values using react-hook-form's Controller without using setValue() for each input?
The Problem:
I'm creating my own set of reusable components and am trying to use React Hook Form's Controller to manage state, etc. I'm having trouble accessing an input's value and keep getting undefined. However, it will work if I first use setValue().
CodeSandbox example
return (
<form onSubmit={onSubmit}>
<WrapperInput
name="controllerInput"
label="This input uses Controller:"
type="text"
rules={{ required: "You must enter something" }}
defaultValue=""
/>
<button
type="button"
onClick={() => {
// getValues() works if use following setValue()
const testSetVal = setValue("controllerInput", "Hopper", {
shouldValidate: true,
shouldDirty: true
});
// testGetVal returns undefined if setValue() is removed
const testGetVal = getValues("controllerInput");
console.log(testGetVal);
}}
>
GET VALUES
</button>
<button type="submit">Submit</button>
</form>
);
More info:
I don't understand why getValues() is returning undefined. When I view the control object in React dev tools I can see the value is saved. I get undefined on form submission too.
My general approach here is to use an atomic Input.tsx component that will handle an input styling. Then I use a WrapperInput.tsx to turn it into a controlled input using react-hook-fom.
Lift the control to the parent instead and pass it to the reusable component as prop.
// RegistrationForm.tsx
...
const {
setValue,
getValues,
handleSubmit,
formState: { errors },
control,
} = useForm();
...
return (
...
<WrapperInput
control={control} // pass it here
name="controllerInput"
label="This input uses Controller:"
type="text"
rules={{ required: "You must enter something" }}
defaultValue=""
/>
)
// WrapperInput.tsx
const WrapperInput: FC<InputProps> = ({
name,
rules,
label,
onChange,
control, /* use control from parent instead */
defaultValue,
...props
}) => {
return (
<div>
<Controller
control={control}
name={name}
defaultValue={defaultValue}
render={({ field }) => <Input label={label} {...field} />}
/>
</div>
);
};
Codesandbox

Receiving 'Function components cannot be given refs' error when applying 'react-input-mask' library to my 'react-form-hook' form component

I'm currently lost on these two error messages I'm receiving. They began to occur when I added 'react-input-masks' library to my code. The code itself works as I expect it to, but I can't figure out why I'm receiving the following errors concerning refs (especially the first one).
My understanding was that my attempt at using refs within a functional component was the culprit and I attempted to fix it by wrapping React.forwardRef() to my component, but this did nothing to fix the problem. I've tried methods to include refs into the controller and InputMask components, but this has not changed the refs. I'm not even sure if the controller is the issue if not for the error message mentioning it.
Below is my code which I have cut down to what seems to be the area of concern. Everything outside of this worked error-free.
import React, {useRef} from 'react';
import { useForm, Controller } from "react-hook-form";
import InputMask from "react-input-mask";
const CreateAccountForm = React.forwardRef((props, ref) => {
const {register, formState: { errors }, handleSubmit, watch, control} = useForm();
const password = useRef({});
password.current = watch("password", "");
const onSubmit = (register) => {
console.log(register)
};
return (
<div className='sign-in-form'>
<label> Date of Birth
<Controller
control={control}
render={({ field: { ref, ...rest} }) => (
<InputMask
{...rest}
mask="99/99/9999"
placeholder="mm/dd/yyyy"
inputRef={ref}
/>
)}
{...register('DOB',
{
required: 'date of birth missing',
pattern:
{
value: /^\d{1,2}\/?\d{1,2}\/?\d{4}$/,
message: 'Invalid Date'
},
})}
name="DOB"
defaultValue=""
type="date"
/>
</label>
<button type="submit">Create Account</button>
</form>
</div>
)
})
export default CreateAccountForm;
Thanks for any help in advance. I tried looking up solutions but struggled with the answers provided elsewhere.
*Edit
As requested, added the error messages as text below.
index.js:1 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `ForwardRef`.
at Controller (http://localhost:3000/static/js/vendors~main.chunk.js:30829:35)
at label
at form
at div
at http://localhost:3000/static/js/main.chunk.js:385:70
at SessionContainer
at div
at App
console.<computed> # index.js:1
index.js:1 Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of InputElement which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-find-node
at input
at InputElement (http://localhost:3000/static/js/vendors~main.chunk.js:33079:30)
at Controller (http://localhost:3000/static/js/vendors~main.chunk.js:30829:35)
at label
at form
at div
at http://localhost:3000/static/js/main.chunk.js:385:70
at SessionContainer
at div
at App
console.<computed> # index.js:1
I managed to figure out a solution to removing both of the errors I received (which turns out to not be related, but two distinct issues).
First, the 'findDOMNode is deprecated in StrictMode...' error appears to be an issue with the react-input-mask library. The library is no longer active and an alternative library I found that performs the same action but is still active is react-number-format.
The error concerning 'Function components cannot be given refs' is the result of 'register' within the Controller component. Register is already handled by the component and is not needed. Instead, you should use 'rules' to avoid the forwardRefs error. Here is an example that fixed my code:
import React, {useRef} from 'react';
import { useForm, Controller } from "react-hook-form";
import NumberFormat from 'react-number-format';
const CreateAccountForm = (props, ref) => {
const {register, formState: { errors }, handleSubmit, watch, control} = useForm();
const password = useRef({});
password.current = watch("password", "");
const onSubmit = (register) => {
debugger;
console.log(register)
};
return (
<div className='sign-in-form'>
<label> Date of Birth
<Controller
innerRef={ref}
control={control}
render={({field}) => (
<NumberFormat
{...field}
format="##/##/####"
allowEmptyFormatting
mask="_"
/>
)}
rules={{
required: 'date of birth missing',
pattern:
{
value: /^\d{1,2}\/?\d{1,2}\/?\d{4}$/,
message: 'Invalid Date'
},
}}
name="DOB"
defaultValue=""
type="date"
/>
</label>
<button type="submit">Create Account</button>
</form>
</div>
)
}
export default CreateAccountForm;

Creating a controlled form with Grommet throws errors

I am trying to create a basic form using Grommet following the examples at https://v2.grommet.io/form. My specific form looks like this:
import React from 'react';
import { Box, Form, FormField, TextInput, Button } from 'grommet';
const defaultValue = {};
const LoginForm = () => {
const [value, setValue] = React.useState(defaultValue);
function handleSubmit(e) {
e.preventDefault();
const { email, password } = e.value;
console.log('pretending to log in:', email, password);
// doLogin(email, password)
}
return (
<Form
value={value}
onChange={nextValue => {
setValue(nextValue);
}}
onReset={() => setValue(defaultValue)}
onSubmit={handleSubmit}
>
<FormField label="email" name="email" required>
<TextInput name="email" />
</FormField>
<FormField label="password" name="password" required>
<TextInput name="password" />
</FormField>
<Box direction="row" justify="between" margin={{ top: 'medium' }}>
<Button type="reset" label="Reset" />
<Button type="submit" label="Login" primary />
</Box>
</Form>
);
};
As soon as I start typing into either field, I get the following:
Warning: A component is changing an uncontrolled input of type undefined to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: ...
Note that I get the exact same error if I replace my form code with a cut/paste from the example in the link above.
I have a fairly reasonable understanding of what the error means, but I have no idea how to fix it in this case. Is Grommet's implementation of controlled form components broken, or am I missing something elsewhere in my configuration or packages that might be causing this?
The React.js controlled standards are not allowing objects to be undefined.
So the problem starts with how you've defined your defaultValue = {};, since it is an empty object, there is no initial value to the FormField children and that causes them to be undefined and hence the error.
So if you'll change the preset value to be more accommodating to your fields, such as defaultValue = { password: '' }; it will fix your error.
For more reading about React controlled and uncontrolled inputs read this A component is changing an uncontrolled input of type text to be controlled error in ReactJS

How to trigger validation in formik after rendering?

I have one question with formik. Basically, I will have a table which list all the Id of the forms which have errors. When user click on the Id of a form, it will show up the form itself. The requirement is the errors should be showing also when the form is rendered. Does anyone know how to do that with Formik ? Also, if user edit the field the field validation should works as normal.
I put the codesandbox link here. https://codesandbox.io/s/pensive-brattain-yyls2. Basically I want that when the form show up I should see the errors, not just when user move away from the field or changing it. Thank you.
import { Formik, Field, Form } from "formik";
import { TextField } from "formik-material-ui";
class Post0 extends React.Component {
validateEmptyName(value) {
if (!value) {
return "Invalid Name";
}
}
render() {
return (
<div>
<Formik
initialValues={{
email: "",
animal: ""
}}
onSubmit={values => {
this.props.nextStep(values);
}}
render={({ values, isSubmitting }) => (
<Form>
<Field
name="email"
type="email"
value={values.email}
component={TextField}
variant="outlined"
validate={this.validateEmptyName}
/>
<Field
name="animal"
value={values.animal}
component={TextField}
variant="outlined"
/>
<button type="submit">Submit</button>
</Form>
)}
/>
</div>
);
}
}
I made a basic demo version using a custom input component. Formik has no built-in options to do this so unfortunately you need to create your own field components to integrate with Formik's props and bypass the logic that won't show validations if the form's not touched.
const Input = ({ field, form }) => {
useEffect(() => {
form.validateForm();
}, []);
return (
<div>
<input
style={{
border: form.errors[field.name] ? "1px solid red" : "1px solid #ccc"
}}
name={field.name}
value={field.value}
onChange={field.onChange}
/>
{form.errors[field.name] && (
<span style={{ color: "red" }}>{form.errors[field.name]}</span>
)}
</div>
);
};
And then pass this as the component prop on your <Field/>.
Formik does provide an isInitialValid prop which you could set to false on the main Formik component, but again the library TextFields you're using won't display anything without the touched prop.
2021 update:
Use validateOnMount prop:
https://formik.org/docs/api/formik#validateonmount-boolean
validateOnMount works if you also add initialTouched, but it has limitation (...or better say bug) when it shows validation issues after submit which doesn't lead to different view or component.
I have found pretty elegant workaround which works as expected.
const formikRef = React.useRef(null);
React.useEffect(() => formikRef.current.validateForm(), []);
return (
<Formik
innerRef={formikRef}
initialValues={props.customer}
initialTouched={mapObject(props.customer, true)}
onSubmit={values => {
.
.
.
you can use isInitialValid or initialErrors to valid initial values.
check their official docs here.
I accomplished this using Yup's validateAtSync function while populating the initial values of my form from the querystring.
function generateInitialValues(tabs: TabType[], router: any, validationSchema: any) {
const initialValues: { [key: string]: number | string } = {};
_.forEach(tabs, (tab: TabType) => {
_.forEach(tab.formFields, (f: FormField) => {
let isFieldValid;
try {
// https://github.com/jquense/yup#mixedvalidatesyncatpath-string-value-any-options-object-any
console.log('validation schema validateAt: ', validationSchema.validateSyncAt(f.id, router.query[f.id]));
isFieldValid = validationSchema.validateSyncAt(f.id, router.query[f.id]);
} catch (e) {
// do nothing on purpose to stop exception from being thrown
// TODO: Consider doing something here, such as recording a metric
}
initialValues[f.id] = isFieldValid ? router.query[f.id] : f.defaultValue;
})
});
return initialValues;
}

Resources