Reputation: 681
I am currently using this amazing hook I found a couple months ago on the internet somewhere and can't find the article I found it anymore, but basically the issue I have is that when I have a large form and I tab to the next field if I use the onBlur it runs the validation for every field so all fields that are required automatically become red and show an error (because they have nothing in them and my error css).
I want to wait until the field has at least been entered into and then left before the validation related to that specific field.
Basically I want to know if there is a way I can somehow wait until the input has been selected at least once. The only thing I can think of would be having a separate hook for each input in the form or somehow attaching an event to each field- true/false for if it has been selected at least once, but don't know how that would get implemented.
import { useState, useEffect } from "react";
const useFormValidation = (initialState, validate, authenticate) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [isSubmitting, setSubmitting] = useState(false);
useEffect(() => {
if (isSubmitting) {
const noErrors = Object.keys(errors).length === 0;
if (noErrors) {
authenticate();
setSubmitting(false);
} else {
setSubmitting(false);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errors]);
const handleChange = (event) => {
setValues({
...values,
[event.target.name]: event.target.value
});
}
const handleChangeChecked = (event) => {
setValues({...values, [event.target.name] : event.target.checked });
}
//THIS IS THE FUNCTION I AM TALKING ABOUT
const handleBlur = () => {
const validationErrors = validate(values);
setErrors(validationErrors);
}
const handleSubmit = (event) => {
event.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
setSubmitting(true);
}
return {
handleSubmit,
handleChange,
handleChangeChecked,
handleBlur,
values,
errors,
isSubmitting
};
}
export default useFormValidation;
Here is a sample validation also which is the second field passed into useFormValidation function.
const validateCareTeam = (values) => {
let errors = {};
// First Name Errors
if (!values.firstName) {
errors.firstName = "First name is required";
}
// Last Name Errors
if (!values.lastName) {
errors.lastName = "Last name is required";
}
// Email Error
if (values.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = "Please enter a valid email address";
}
const phoneno = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/
// Phone Number Errors
if (!values.phone) {
errors.phoneNumber = "Phone number is required";
} else if (!values.phone.match(phoneno)) {
errors.phoneNumber = "Please enter a phone number with 10 digits. 1 not necessary"
}
return errors;
}
export default validateCareTeam;
So basically if the use tabs after first name then all other required fields- last name and phone number turn red.
I would prefer to not event run the validation until the submit button is hit, but I have been asked for validation immediately.
Upvotes: 5
Views: 9258
Reputation: 681
Medet Tleukabiluly got me going in the right direction with the brilliant spread operator and was close and I couldn't have done it without the code above, but since there is no change if the user tabs to the next section without putting in any inputs the if statement actually needs to be in the handleBlur
function above touchedErrors
. Other changes include not needing touch to be in the return block and submit function. Additionally, it doesn't update touch right away which is a function of this post - basically it doesn't run the error until it is 2 inputs down. The fix for this is to add a new useEffectHook that runs each time touched is changed.
Here is the working code, which ultimately should be using useCallbacks to get rid of the eslint disabling, but works for me for now.
import { useState, useEffect } from "react";
const useFormValidation = (initialState, validate, authenticate) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState([]);
const [isSubmitting, setSubmitting] = useState(false);
useEffect(() => {
if (isSubmitting) {
const noErrors = Object.keys(errors).length === 0;
if (noErrors) {
setTouched([]);
authenticate();
setSubmitting(false);
} else {
setSubmitting(false);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errors]);
// need to rerun after there is a changed to touched
// this checks to see if there are any errors that should be highlighted
useEffect(() => {
const validationErrors = validate(values);
const touchedErrors = Object.keys(validationErrors)
.filter(key => touched.includes(key)) // get all touched keys
.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = validationErrors[key]
}
return acc
}, {})
setErrors(touchedErrors);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [touched, values]);
const handleChange = (event) => {
console.log("event changed")
setValues({
...values,
[event.target.name]: event.target.value
});
}
const handleChangeChecked = (event) => {
setValues({...values, [event.target.name] : event.target.checked });
if (!touched.includes(event.target.name)) {
setTouched([
...touched,
event.target.name
])
}
}
const handleBlur = (event) => {
if (!touched.includes(event.target.name)) {
setTouched([
...touched,
event.target.name
])
}
}
const handleSubmit = (event) => {
event.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
setSubmitting(true);
}
return {
handleSubmit,
handleChange,
handleChangeChecked,
handleBlur,
values,
errors,
isSubmitting
};
}
export default useFormValidation;
Upvotes: 1
Reputation: 11930
Below implementation is not tested, but should give idea how it should work, read comments in code
import {
useState,
useEffect
} from "react";
const useFormValidation = (initialState, validate, authenticate) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState([]); // NEW: stores all touched field names
const [isSubmitting, setSubmitting] = useState(false);
useEffect(() => {
if (isSubmitting) {
const noErrors = Object.keys(errors).length === 0;
if (noErrors) {
// NEW: probably need to flush touched
// setTouched([])
authenticate();
setSubmitting(false);
} else {
setSubmitting(false);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errors]);
const handleChange = (event) => {
setValues({
...values,
[event.target.name]: event.target.value
});
if (!touched.includes(event.target.name)) { // NEW
setTouched([
...touched,
event.target.name
])
}
}
const handleChangeChecked = (event) => {
setValues({ ...values,
[event.target.name]: event.target.checked
});
if (!touched.includes(event.target.name)) { // NEW: check if we touched this field, then add to touched array
setTouched([
...touched,
event.target.name
])
}
}
//THIS IS THE FUNCTION I AM TALKING ABOUT
const handleBlur = () => {
const validationErrors = validate(values);
const touchedErrors = Object.keys(validationErrors) // NEW
.filter(key => touched.includes(key)) // get all touched keys
.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = validationErrors[key]
}
return acc
}, {})
setErrors(touchedErrors); // NEW: touchedErrors has errors that are touched
}
const handleSubmit = (event) => {
event.preventDefault();
const validationErrors = validate(values);
// NEW do the same
const touchedErrors = Object.keys(validationErrors) // NEW
.filter(key => touched.includes(key)) // get all touched keys
.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = validationErrors[key]
}
return acc
}, {})
setErrors(touchedErrors); // NEW: touchedErrors has errors that are touched
setSubmitting(true);
}
return {
handleSubmit,
handleChange,
handleChangeChecked,
handleBlur,
values,
touched, // NEW: just extend API
errors,
isSubmitting
};
}
export default useFormValidation;
Upvotes: 2