Yup errors don't fire before onBlur - reactjs

I want this input field's Yup errors to fire as soon as a key is logged for maximum interactivity. However, I'm only getting the errors once I click out of the text field. After the first onBlur event happens and I click back the text field, the next errors are showed in real time.
How can I alter this behavior? I want an error to appear immediately if my first character is not a letter or if my name is not 5-12 characters long.
This is my code:
import ReactDOM from 'react-dom';
import { useFormik } from 'formik'
import * as Yup from 'yup';
function Container(){
const formik = useFormik({
initialValues: {
prodName: ''
},
validationSchema: Yup.object(
{
prodName : Yup.string().required('This field is required')
.matches(/^[aA-zZ\s]+$/, "Only letters are allowed for this field ")
.min(5,'Minimum is 5').max(12, 'No more than 12')
}
)
})
return(
<div id='prd-contain'>
<form onSubmit={formik.handleSubmit}>
<label className='prd-labels'>Product name</label>
<input type="text" id="prodName" name="prodName" value={formik.values.prodName}
onChange={formik.handleChange} onBlur={formik.handleBlur}/>
{formik.touched.prodName && formik.errors.prodName ? (<div>{formik.errors.prodName}</div>) : null}
<input type="submit" value="Submit"/>
</form>
</div>
)
}
ReactDOM.render(
<Container />,
document.getElementById('root')
);

You can simply change
<input type="text" id="prodName" name="prodName" value={formik.values.prodName} onChange={formik.handleChange} onBlur={formik.handleBlur}/>
to
<input type="text" id="prodName" name="prodName" value={formik.values.prodName} onChange={formik.handleChange} onkeyup={formik.handleBlur}/>

Related

Value type error message displaying instead of required error message on empty input submission

I'm using react-hook-form and yup together and for some reason it's displaying the wrong error message.
When the phone number input is left empty on submitting the form, it displays the typeError error message instead of the 'required' error message which I can't understand why that's happening since the input field is blank and there's no value to determine that it's of type 'number' or not.
import { yupResolver } from "#hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import * as yup from "yup";
const schema = yup.object().shape({
name: yup.string().required("Field is required*"),
email: yup.string().email().required("Field is required*"),
phoneNumber: yup
.number()
.required("Field is required*") //this message should be displayed instead of typeError message
.typeError("Value must be a number*"),
message: yup.string().required("Please provide your message*")
});
export default function App() {
const {
reset,
register,
handleSubmit,
formState: { errors }
} = useForm({ resolver: yupResolver(schema) });
const submitForm = (data) => {
console.log(data);
reset();
};
return (
<div className="App">
<form className="contact-form" onSubmit={handleSubmit(submitForm)}>
<h2>Send Us a Message </h2>
<p>
<label for="name">Name</label>
<input name="name" type="text" {...register("name")} />
<p className="error-msg"> {errors.name?.message} </p>
</p>
<p>
<label for="email">Email</label>
<input name="email" type="text" {...register("email")} />
<p className="error-msg"> {errors.email?.message} </p>
</p>
<p>
<label for="phoneNumber">Phone</label>
<input
name="phoneNumber"
type="text"
{...register("phoneNumber")}
/>
</p>
<p className="error-msg"> {errors.phoneNumber?.message} </p>
<input type="submit" id="submit" />
</form>
</div>
);
Because Yup receives an empty string instead of a number, it thinks you're passing the wrong type. You need to tell RHF to return undefined if the input is empty:
<input
name="phoneNumber"
type="text"
{...register("phoneNumber", {
setValueAs: (v) => {
return v === "" ? undefined : parseInt(v, 10);
}
})}

React state update input data

I have developed react application, with router which has 2 routes. In one Page I am displaying the value, in the other page I have one form which has two input fields(Email and Phone) and one button. I have Onchange event for each input field. as soon as I do changes in input field and without clicking on update button, if I go back to the first page the data is updating without submitting. But after refresh it will get back to original value.
Can you please help me out, How to avoid the updating data.
Thanks in advance.
Here is my code:
import NumberFormat from 'react-number-format'
import {
useForm,
Controller,
ErrorMessage,
} from 'react-hook-form/dist/react-hook-form.ie11'
import isValidEmailAddress from '../utility/isValidEmailAddress'
import Messages from './Messages'
import Button from './Button'
import { useLocation } from 'react-router-dom'
export default function EditContactInformation({
changeContactInfoSmall,
contactInfo,
updateContactInfo,
}) {
const { handleSubmit, register, control, errors } = useForm()
return (
<form onSubmit={handleSubmit(() => changeContactInfo(contactInfo))}>
<h3>Edit contact information</h3>
<Messages results={contactInfo} />
<div className="edit-contact-information__form-col">
<div className="form-group form-group--email">
<label htmlFor="email">Preferred email address</label>
<input
name="email"
id="email"
type="text"
ref={register({
required: 'Please enter an email address',
validate: (value) =>
isValidEmailAddress(value) ||
'Please enter a valid email address',
})}
onChange={(e) => {
updateContactInfo('email', e.currentTarget.value)
return e.currentTarget.value
}}
defaultValue={contactInfo.get('email')}
/>
<ErrorMessage
errors={errors}
name="email"
as={<div className="form-errors" />}
/>
</div>
<div className="form-group form-group--phone">
<label htmlFor="phone">Preferred phone</label>
<Controller
as={<NumberFormat id="phone" format="(###) ###-####" mask="_" />}
name="phone"
rules={{
required: 'Please enter a phone number',
minLength: {
value: 10,
message: 'Please enter a valid phone number',
},
}}
onChangeName="onValueChange"
onChange={([{ value }]) => {
updateContactInfo('phone', value)
return value
}}
control={control}
defaultValue={contactInfo.get('phone')}
/>
<ErrorMessage
errors={errors}
name="phone"
as={<div className="form-errors" />}
/>
</div>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="updateAll"
data-testid="update-all-button"
onChange={(e) => {
updateContactInfo('updateAll', e.currentTarget.checked)
}}
ref={register()}
/>{' '}
Update my contact information for all properties
</label>
</div>
<Button
className="button--primary"
arrow
type="submit"
isLoading={contactInfo.get('submitting')}
>
Update contact
</Button>
</form>
)
}
EditContactInformation.propTypes = {
changeContactInfo: PropTypes.func.isRequired,
contactInfo: PropTypes.object.isRequired,
updateContactInfo: PropTypes.func.isRequired,
}
you should use session storage for this situation. React states are deleted when the page is refreshed due to its nature. You should keep the information you want to keep session storage.
States in react are non persistent, meaning that data stored in state is purged when the site is refreshed or navigated out of. So, if you want to hold on to your data, depending on your need, you can either use cookies or your browser's local storage to persist.
react-persist is a great library that you can use to achieve this using localStorage.

react-hooks-form conflict with reactstrap

I'm trying to add react-hook-form to my reactstrap form and inputs, but looks like it has a conflict between the packages.
import React, { useState, useEffect } from 'react';
import { useForm } from "react-hook-form";
import {
Button,
Form,
Label,
Input
} from 'reactstrap';
import { FormControlLabel, Checkbox, FormGroup } from '#material-ui/core';
...
const { register, handleSubmit, errors } = useForm();
...
const updDetails = (data) => {
console.log(data);
}
return (
<Form onSubmit={handleSubmit(updDetails)}>
<FormGroup className="mr-10 mb-10">
<Label for="compName" className="mr-sm-10">Company Name</Label>
<Input type="text" name="compName" id="compName" placeholder="Company Name"
ref={register({ required: true })} aria-invalid={errors.name ? "true" : "false"}
value={compDetails.compName} onChange={(e) => setCompDetails({...compDetails, compName: e.target.value})}
/>
{errors.name && errors.name.type === "required" && (
<span role="alert">This is required</span>
)}
</FormGroup>
<Button type="submit" className="col-sm-3" color="primary">Update Details</Button>
</Form>
)
...
When I load the page, I can see in the browser console warnings for all my inputs: Field is missing `name` attribute . However, I am adding the name to every input as in the code above. Also, when I click on Update Details with an empty value for the compName it submits and the data is empty object.
Can't I use react-hook-form with reactstrap?
Thanks
You probably have to use innerRef instead of ref on the Input component. i found this in the source here.
<Input
type="text"
name="compName"
id="compName"
placeholder="Company Name"
innerRef={register({ required: true })}
aria-invalid={errors.compName ? "true" : "false"}
/>
I am not familiar with the reactstrap code, but a thing you could do is pass the name to the register function, use it this way:
ref={register({name: 'input-name', /* rest of options*/})}
Hope I could help :D

Conditional validation with react hook form

Here is my form looks like and also CodeSanbox. currently I'm using react-hook-form
as you can see form has 3 inputs. Submit button should be disabled until all the required fields are entered.
Two use case:
If "Check" is unchecked:
only "id" should be validated and submit button should get enabled. "firt" and "last" names should not be part of form data
If "Check" is checked
all the fields should be validated
first and last names are only required if "Check" is checked. so its not checked then form should only validate "ID" field. if "Check" is checked then all fields should get validated.
problem I'm having is if I enter id, form state is still "invalid". Form is expecting to enter values for first and last name.
I would appreciate any help.
I have updated your CodeSanBox code and also adding the full code here:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./index.css";
function App() {
const {
register,
handleSubmit,
errors,
formState,
unregister,
setValue,
getValues,
reset
} = useForm({
mode: "onBlur",
reValidateMode: "onBlur",
shouldUnregister: true
});
//console.log(formState.isValid);
console.log(errors);
const [disabled, setDisabled] = useState(true);
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
useEffect(() => {
// #ts-ignore
if (disabled) {
console.log("unregister");
reset({ ...getValues(), firstName: undefined, lastName: undefined });
unregister(["firstName", "lastName"]);
} else {
console.log("register");
register("firstName", { required: true });
register("lastName", { required: true });
}
}, [disabled]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="id">ID</label>
<input
name="id"
placeholder="id"
ref={register({ required: true, maxLength: 50 })}
/>
{errors.id && <p>"ID is required"</p>}
<fieldset disabled={disabled}>
<legend>
<input
type="checkbox"
name={"name"}
ref={register}
onClick={() => setDisabled(!disabled)}
/>
<span>Check</span>
</legend>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
placeholder="Bill"
onChange={(e) => {
console.log(e.target.value);
setValue("firstName", e.target.value);
}}
ref={register({ required: !disabled })}
/>
{errors.firstName && <p>"First name is required"</p>}
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
placeholder="Luo"
onChange={(e) => setValue("lastName", e.target.value)}
ref={register({ required: !disabled })}
/>
{errors.lastName && <p>"Last name is required"</p>}
</fieldset>
<input type="submit" disabled={!formState.isValid} />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
First I found that you set disabled state as false which should be true as an initial value, and regarding the issue, I have used reset and getValues functions when the disabled state changes.
EDIT for you to recognize code changes easy, I have restored all the code at CodeSanBox.
This whole validation behavior (UX) is definitely making things a bit harder, however, there are a couple of things that you should leverage from the library such as:
watch
validate
getValues
import React from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./index.css";
function App() {
const {
register,
handleSubmit,
errors,
formState: { isValid, touched },
getValues,
trigger,
watch
} = useForm({
mode: "onBlur"
});
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
const validate = (value) => {
if (getValues("name")) { // read the checkbox value
return !!value;
}
return true;
};
const isChecked = watch("name"); // watch if the name is checked
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="id">ID</label>
<input
name="id"
placeholder="id"
ref={register({ required: true, maxLength: 50 })}
/>
{errors.id && <p>"ID is required"</p>}
<fieldset disabled={!isChecked}>
<legend>
<input
type="checkbox"
name={"name"}
ref={register}
onChange={() => trigger()} // you want update isValid due to state change, and also those extra two inputs become required
/>
<span>Check</span>
</legend>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
placeholder="Bill"
ref={register({
validate
})}
/>
// make sure input is touched before fire an error message to the user
{errors.firstName && touched["firstName"] && (
<p>"First name is required"</p>
)}
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
placeholder="Luo"
ref={register({
validate
})}
/>
{errors.lastName && touched["lastName"] && (
<p>"Last name is required"</p>
)}
</fieldset>
<input type="submit" disabled={!isValid} />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
CSB:
https://codesandbox.io/s/react-hook-form-conditional-fields-forked-n0jig?file=/src/index.js:0-1831
on your ref, dont use hard coded bool true, ref={register({ required: true})}, but your dynamic ref={register({ required: disabled })}
do notice that because your mode: "onBlur" config, the button won't be abled until id field blurred
You just need to replace true .from ref: required:true..... Instead use const 'disabled' ....in input of first and last name .
So as to achieve dynamic change

React-hook-form input fields match validation best practice

What's the best practice when doing input fields match validation when dealing with React-hook-form? For example, when matching email inputs, etc.
While looking into email match validation with React-hook-form found an issue while trying to separate error messages from "coupled elements" through their validation method. The ref only takes one argument that is used for React-hook-form register, while needing to use useRef to access the current.value for value matching, as follows:
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
function App() {
const { register, handleSubmit, errors } = useForm();
const inputEmail = useRef(null)
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={inputEmail}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === inputEmail.current.value) || 'Email confirmation error!',
}
})}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
While this pattern seems to be an option when doing input field matching it does not play well with React-hook-form!
For example, the error message is coupled with one input case only and has no separate messages for each independent field, or one of the input fields does not have the register assigned to it, this means that the property required is not set, etc.
So, I'm looking into a good practice or pattern that solves:
Keeping error messages separated by the input field
The validation method, when testing the match should be able to reference the twin field value in a React compliant way and not
through the DOM (document.querySelector, etc)
You shouldn't need the manual ref for inputEmail. Instead, use the getValues method to fetch the current value of your whole form.
const { register, getValues } = useForm()
Then you register both inputs and call getValues from your custom validation.
<input
name="email"
type="email"
ref={register}
/>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === getValues().email) || 'Email confirmation error!',
}
})}
/>
For this you could use Yup library, which is great:
Add validationSchema to your config object when instantiating useForm and pass a valid Yup schema. Like so:
const Schema = yup.object().shape({
email: yup.string().required('Required field'),
emailConfirmation: yup
.string()
.oneOf([yup.ref('email')], 'Emails must match')
.required('Required field'),
});
// How to add it to your useForm
const { register } = useForm({
validationSchema: Schema
})
Your component should look something like this:
function App() {
const { register, handleSubmit, errors } = useForm({
validationSchema: Schema
});
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={register}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}

Resources