I have a Sign Up Component in my current project, and I'm trying to implement validation for the email and phone number.
Code:
export default function Form() {
// States for registration
const [firstname, setFirstName] = useState('');
const [lastname, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phonenumber, setPhoneNumber] = useState('');
// States for checking the errors
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState(false);
// Handling the email change
const handleEmail = (e) => {
setEmail(e.target.value);
setSubmitted(false);
};
// Handling the phonenumber change
const handlePhoneNumber = (e) => {
setPhoneNumber(e.target.value);
setSubmitted(false);
};
// Handling the form submission
const handleSubmit = (e) => {
e.preventDefault();
if (email === '' || phonenumber === '') {
setError(true);
} else {
setSubmitted(true);
setError(false);
}
};
// Showing error message if error is true
const errorMessage = () => {
return (
<div
className="error"
style={{
display: error ? '' : 'none',
}}>
<h1>Please enter all the fields</h1>
</div>
);
};
return (
<div className="form">
<div className="messages">
{errorMessage()}
{successMessage()}
</div>
<div className='inputval'>
<div className="d-flex justify-content-center flex-column">
<label className="label">Email</label>
<input onChange={handleEmail} className="input"
value={email} type="email" />
<label className="label">Phone Number</label>
<input onChange={handlePhoneNumber} className="input"
value={phonenumber} type="email" />
</div>
<div className="d-inline-block justify-content-center align-items-center">
<button className="btn" onClick={handleSubmit} type="submit">
Submit
</button>
</div>
</div>
</div>
);
}
For the most part, I tried implementing /^(([^<>()[\]\.,;:\s#\"]+(\.[^<>()[\]\.,;:\s#\"]+)*)|(\".+\"))#(([^<>()[\]\.,;:\s#\"]+\.)+[^<>()[\]\.,;:\s#\"]{2,})$/i for the format constant in my email but I had no luck. I have a useState hook that checks if the boxes are empty, but if I could get some assistance on this, it would be much appreciated!
There are a lot of form validation npm tools that will help alot. But if you want to do everything custom and understand about how it will work, here is a quick project demonstrating how to go about it. I would recommend putting some of the helper functions in different files so they can be used everywhere in your app. CodeSandbox: https://codesandbox.io/s/simple-form-validation-jqfvpy?file=/src/Input.js:0-325
export default function App() {
const [form, setForm] = useState({ name: "", email: "", phone: "" });
const [errors, setErrors] = useState({ name: [], email: [], phone: [] });
const checkRules = (input, rules) => {
let errors = [];
let value = input;
if (typeof value === "string") value = input.trim();
if (rules.required) {
if (value === "") errors.push("*This field is required.");
}
if (rules.phone) {
let phoneno = new RegExp(/^\(?(\d{3})\)?[-. ]?(\d{3})[-. ]?(\d{4})$/);
if (!phoneno.test(value))
errors.push("*Please Enter valid phone number XXX-XXX-XXXX");
}
if (rules.email) {
let pattern = new RegExp(
/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(#((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(#\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i
);
if (!pattern.test(value)) errors.push("*Please enter a valid email.");
}
return errors;
};
const checkFormValidation = (f) => {
const errors = {};
errors.name = checkRules(f.name, { required: true });
errors.phone = checkRules(f.phone, { phone: true });
errors.email = checkRules(f.email, { email: true });
for (const [, value] of Object.entries(errors)) {
if (value.length > 0) return { noErrors: false, errors };
}
return { noErrors: true, errors };
};
const handleSubmit = (f) => {
const { errors, noErrors } = checkFormValidation(f);
setErrors(errors);
if (noErrors) {
alert(JSON.stringify(f));
}
};
return (
<div className="App">
<div style={{ display: "grid", placeItems: "center" }}>
<Input
name="Name"
value={form.name}
errors={errors.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<Input
name="Email"
value={form.email}
errors={errors.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<Input
name="Phone"
value={form.phone}
errors={errors.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
/>
<button onClick={() => handleSubmit(form)}>Submit</button>
</div>
</div>
);
}
export const Input = ({ name, value, onChange, errors }) => {
return (
<>
<input type="text" placeholder={name} value={value} onChange={onChange} />
{errors.length > 0
? errors.map((e) => (
<p style={{ fontSize: "9px", color: "red" }}>{e}</p>
))
: null}
</>
);
};
Related
I'm trying to make a simple form that has validation and after the validation is success send a mail with Email.js. I'm using only email.js. I'm trying to set an if statement that would check that my formErrors object is empty and if it is to "fire" the mail sending. But so far I've tried a few methods but they all send mail even if the form hasn't passed the validation (formErrors object has value). My code here (I removed my service, template and key from .sendForm) with the latest check I attempted (Object.keys(formErrors).length === 0) that still doesn't prevent the mail to be sent:
import React, { useState, useRef } from "react";
import emailjs from "#emailjs/browser";
import "./App.css";
function App() {
const initialValues = { user: "", email: "", pass: "" };
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
};
const form = useRef();
const handleSubmit = (e) => {
e.preventDefault();
setFormErrors(validate(formValues));
setIsSubmit(true);
if (Object.keys(formErrors).length === 0) {
emailjs
.sendForm(
"",
"",
form.current,
""
)
.then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
}
};
const validate = (v) => {
const errors = {};
const regex = /^[^\s#]+#[^\s#]+\.[^\s#]{2,}$/i;
if (!v.user) {
errors.user = "This field can't be empty";
}
if (!v.email) {
errors.email = "This field can't be empty";
} else if (!regex.test(v.email)) {
errors.email = "Incorrect email";
}
if (!v.pass) {
errors.pass = "This field can't be empty";
} else if (v.pass.length < 6) {
errors.pass = "Password needs to be at least 6 characters";
}
return errors;
};
return (
<div className="container">
{Object.keys(formErrors).length === 0 && isSubmit ? (
<div className="title">Success</div>
) : (
<div className="title">Fill the form</div>
)}
<form ref={form} onSubmit={handleSubmit}>
<h1>Login Form</h1>
<hr />
<div className="form">
<div className="field">
<label>Username</label>
<input
type="text"
name="user"
placeholder="Username"
value={formValues.user}
onChange={handleChange}
/>
</div>
<p>{formErrors.user}</p>
<div className="field">
<label>Email</label>
<input
type="text"
name="email"
placeholder="Email"
value={formValues.email}
onChange={handleChange}
/>
</div>
<p>{formErrors.email}</p>
<div className="field">
<label>Password</label>
<input
type="password"
name="pass"
placeholder="Password"
value={formValues.pass}
onChange={handleChange}
/>
</div>
<p>{formErrors.pass}</p>
<button className="btn">Submit</button>
</div>
</form>
</div>
);
}
export default App;
I guess your issue related with non async behaviour of states. In handleSubmit function you set formErrors and right after you are checking it in conditional statement. If you replace your
if (Object.keys(formErrors).length === 0)
with
if (Object.keys(validate(formValues)).length === 0)
it should work as expected.
Another solution would be adding this in the beginning of handleSubmit function and it will not enter to any other logic of the submit
if (Object.keys(validate(formValues)).length > 0) {
return
}
I'm developing a custom Form validation for my React project with Typescript.
I'm facing an issue with the useState that is not updating immediately the state containing the errors when I submit a form.
Let me provide you an example.
const initialFormState = {
email: '',
password: '',
}
const SignUpForm = () => {
const [formValues, setFormValues] = useState(initialFormState);
const [validationErrors, setValidationErrors] = useState<string>([]);
const handleChange = () => {
// handle the change implementation updating the formValues ...
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
doValidationOnSubmit();
if (validationErrors.length > 0) {
console.log('validation errors!!');
return;
}
doLogin();
};
const doValidationOnSubmit = () => {
Object.entries(formValues).forEach(([inputName, value]) => {
if (formValues[inputName] === '') {
setValidationErrors((oldValidationErrors) => [...oldValidationErrors, `${inputName} is not valid`]);
}
});
}
const doLogin = () => {
// do login logic
}
return (
<>
<form onSubmit={handleSubmit}>
<input type="email" name="email" onChange={handleChange} />
<input type="password" name="email" onChange={handleChange} />
<button type="submit">Login</button>
</form>
</>
);
}
export default SignUpForm;
When I'm checking for the errors in the handleSubmit, there are no errors, even if errors should be present there:
if (validationErrors.length > 0) {
console.log('validation errors!!');
return;
}
In general, I'm wondering what is the best practice in order to avoid these kinds of issues with the react state not updating immediately the state?
I already tried with useEffect, listening on the validationErrors changes but nothing changes actually, the behavior is pretty the same.
I'm sure I'm missing something.
useState is asynchronous, so state changes (setValidationErrors) are not applied immediately. Therefore, you cannot get the latest state of validationErrors in the next line.
We can do validation and set state separately. In that case, you can leverage the latest value (not the latest state) to check values validity.
const initialFormState = {
email: '',
password: '',
}
const SignUpForm = () => {
const [formValues, setFormValues] = useState(initialFormState);
const [validationErrors, setValidationErrors] = useState<string>([]);
const handleChange = () => {
// handle the change implementation updating the formValues ...
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
//get all invalid values
const invalidValues = returnInvalidValues();
//update state
setValidationErrors(prev => [...prev, ...invalidValues])
if (invalidValues.length > 0) {
console.log('validation errors!!');
return;
}
doLogin();
};
const returnInvalidValues = () => {
return Object.entries(formValues).filter(([inputName, value]) => formValues[inputName] === ''). map(invalidValue => `${inputName} is not valid`);
}
const doLogin = () => {
// do login logic
}
return (
<>
<form onSubmit={handleSubmit}>
<input type="email" name="email" onChange={handleChange} />
<input type="password" name="email" onChange={handleChange} />
<button type="submit">Login</button>
</form>
</>
);
}
export default SignUpForm;
You also can try useEffect
const initialFormState = {
email: '',
password: '',
}
const SignUpForm = () => {
const [formValues, setFormValues] = useState(initialFormState);
const [validationErrors, setValidationErrors] = useState<string>([]);
const handleChange = () => {
// handle the change implementation updating the formValues ...
}
//introduce useEffect here
useEffect(() => {
if (validationErrors.length > 0) {
console.log('validation errors!!');
return;
}
doLogin();
}, [validationErrors]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
doValidationOnSubmit();
};
const doValidationOnSubmit = () => {
Object.entries(formValues).forEach(([inputName, value]) => {
if (formValues[inputName] === '') {
setValidationErrors((oldValidationErrors) => [...oldValidationErrors, `${inputName} is not valid`]);
}
});
}
const doLogin = () => {
// do login logic
}
return (
<>
<form onSubmit={handleSubmit}>
<input type="email" name="email" onChange={handleChange} />
<input type="password" name="email" onChange={handleChange} />
<button type="submit">Login</button>
</form>
</>
);
}
export default SignUpForm;
If it does not work for your case, you can delay using the latest state with setTimeout. With this approach, it will put the task to get the latest state to the end of the call stack queue (you can check this document)
const initialFormState = {
email: '',
password: '',
}
const SignUpForm = () => {
const [formValues, setFormValues] = useState(initialFormState);
const [validationErrors, setValidationErrors] = useState<string>([]);
const handleChange = () => {
// handle the change implementation updating the formValues ...
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
doValidationOnSubmit();
setTimeout(() => {
if (validationErrors.length > 0) {
console.log('validation errors!!');
return;
}
doLogin();
})
};
const doValidationOnSubmit = () => {
Object.entries(formValues).forEach(([inputName, value]) => {
if (formValues[inputName] === '') {
setValidationErrors((oldValidationErrors) => [...oldValidationErrors, `${inputName} is not valid`]);
}
});
}
const doLogin = () => {
// do login logic
}
return (
<>
<form onSubmit={handleSubmit}>
<input type="email" name="email" onChange={handleChange} />
<input type="password" name="email" onChange={handleChange} />
<button type="submit">Login</button>
</form>
</>
);
}
export default SignUpForm;
I want to get the value when I change it with onChange and created a name and contact number by using the value and setContacts, this app does not cause error but it does not work, Where is the problem? Thanks.
Each new contact in an object has an id, a name and a phone number
const AddUSer = () => {
const {contacts, setcontacts}=useState([]);
const { userName, setUSerName } = useState("");
const { userPhone, setUserPhone } = useState("");
const setName = (e) => {
const value = e.target.value;
return setUSerName(value);
};
const setPhone = (e) => {
const value = e.target.value;
return setUserPhone(value);
};
const handleNewcontact = () => {
const allcontacts = [...contacts];
const newContact = {
id: Math.floor(Math.random() * 1000),
fullName: userName,
phone: userPhone,
};
allcontacts.push(newContact);
setcontacts(allcontacts);
setUSerName("");
setUserPhone("");
}
};
return (
<div className="container">
<form>
<label>Name</label>
<input className="form-control" onChange={(e) => setName} />
<label>Phone</label>
<input className="form-control" onChange={(e) => setPhone} />
<button
onClick={handleNewcontact}
className="btn btn-primary mt-3 mb-4"
>
Save
</button>
</form>
</div>
);
};
export default AddUSer;
You are not passing the event to the function. You can either do
onChange={(e) => setName(e)}
onChange={(e) => setPhone(e)}
but better:
onChange={setName}
onChange={setPhone}
try this. the values are consoled when the user clicks the submit button.
const AddUSer = () => {
const [contact, setContact] = useState({id: '', userName:'', userPhone:''});
function handleNewContact(event) {
setContact({
...contact, id: Math.floor(Math.random() * 1000),
[event.target.name]: event.target.value
});
}
function handleSubmit(event) {
event.preventDefault();
console.log(contact);
}
return (
<div className="container">
<form>
<label>Name</label>
<input className="form-control" name='userName'
onChange={handleNewContact} />
<label>Phone</label>
<input className="form-control" name='userPhone'
onChange={handleNewContact} />
<button
onClick={handleSubmit}
className="btn btn-primary mt-3 mb-4"
>
Save
</button>
</form>
</div>
);
};
export default AddUSer;
I have recently started learning react and I have a component that has a login form:
import Joi from "joi";
import { useState } from "react";
const Login = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const loginSchema = Joi.object().keys({
username: Joi.string().required(),
password: Joi.string().required(),
});
const [errors, setErrors] = useState({ username: null, password: null });
const handleSubmit = (form) => {
form.preventDefault();
validate();
};
const validate = () => {
const { error } = loginSchema.validate(
{ username, password },
{ abortEarly: false }
);
if (!error) return null;
for (const item of error.details) {
let key = item.path[0];
let value = item.message;
setErrors({ ...errors, [key]: value });
}
return errors;
};
return (
<div className="container">
<div>
<h1>Login</h1>
</div>
<div className="card">
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
name="username"
type="text"
className="form-control"
id="username"
placeholder="Enter username"
value={username}
onChange={(username) => setUsername(username.target.value)}
/>
{errors.username && (
<div className="alert alert-danger">{errors.username}</div>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
name="password"
type="password"
className="form-control"
id="password"
placeholder="Password"
value={password}
onChange={(password) => setPassword(password.target.value)}
/>
{errors.password && (
<div className="alert alert-danger">{errors.password}</div>
)}
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
{/* Delete later */}
<h4>{JSON.stringify(errors)}</h4>
</div>
</div>
</div>
);
};
export default Login;
Here, I am trying to set the values of username and password in the errors object, but for some reason, only the password property gets set, the username property remains null. I have used object destructuring but it still doesn't set the value, please help.
State updates are not reflected immediately in React, and since you loop over your data to set the errors object multiple times, it only sets in the last key due to batching in event handlers and the fact that state updates are affected by closures.
You should instead create an updated objected and set it in state once
let newErrors = {};
for (const item of error.details) {
let key = item.path[0];
let value = item.message;
newErrors = { ...newErrors, [key]: value };
}
setErrors(prev => ({...prev, ...newErrors}));
I think the problem comes from this code
for (const item of error.details) {
let key = item.path[0];
let value = item.message;
setErrors({ ...errors, [key]: value });
}
Here you are calling setErrors in a for loop with the same errors object so only the last field is taken into account.
Here is a possible solution :
if (!error) return null;
const newErrors = error.details.reduce((acc, item) => {
const key = item.path[0];
const value = item.message;
acc[key] = value;
return acc;
}, {...errors});
setErrors(newErrors);
I am having a simple form that has firstName and lastName.
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
For this I am trying to add validation.
The validation rules are,
Both fields should accept only text
First name is required and should have at least 4 characters.
If Last name field has value, then it needs to be at least 3 characters.
Things I have tried to achieve this,
components/utils.js
export function isLettersOnly(string) {
return /^[a-zA-Z]+$/.test(string);
}
components/basic_details.js
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
return;
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
On handle input field, I am making the validation to check whether the input has value but I am unable to get the point how to catch the actual validation error and display below respective input box.
Kindly please help me to display the validation message on the respective fields.
Working example:
I suggest adding an errors property to the form data in form_context:
const [formValue, setFormValue] = useState({
basicDetails: {
firstName: '',
lastName: '',
profileSummary: '',
errors: {},
},
...
});
Add the validation to basic_details subform:
const ErrorText = ({ children }) => (
<div style={{ color: 'red' }}>{children}</div>
);
const BasicDetails = () => {
const [value, setValue] = React.useContext(FormContext);
const { basicDetails } = value;
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: 'Can have only letters.',
},
},
}));
return;
}
switch (name) {
case 'firstName': {
const error = value.length < 4 ? 'Length must be at least 4.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
case 'lastName': {
const error = value.length < 3 ? 'Length must be at least 3.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
default:
// ignore
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
return (
<>
<br />
<br />
<div className="form-group col-sm-6">
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.firstName && (
<ErrorText>{basicDetails.errors.firstName}</ErrorText>
)}
<br />
<br />
<div className="form-group col-sm-4">
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.lastName && (
<ErrorText>{basicDetails.errors.lastName}</ErrorText>
)}
<br />
</>
);
};
Lastly, check the field values and errors to set the disabled attribute on the next button in index.js. The first !(value.basicDetails.firstName && value.basicDetails.lastName) condition handles the initial/empty values state while the second condition handles the error values.
{currentPage === 1 && (
<>
<BasicDetails />
<button
disabled={
!(
value.basicDetails.firstName && value.basicDetails.lastName
) ||
Object.values(value.basicDetails.errors).filter(Boolean).length
}
onClick={next}
>
Next
</button>
</>
)}
This pattern can be repeated for the following steps.
First you must be getting converting controlled component to uncontrolled component error in your console. For controlled component it is always preferred to use state to set value for the input. And with onChange handler you set the state. I will try to put into a single component so you would get the idea and apply your case
import React, {useState} from 'react';
import {isLettersOnly} from './components/utils'; // not sure where it is in your folder structure
const MyInputComponent = ({value, ...props}) => {
const [inputValue, setInputValue] = useState(value || ''); // input value should be empty string or undefined. null will not be accepted.
const [error, setError] = useState(null);
const handleChange = event => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setError('Invalid Input');
}
setInputValue(value);
}
return (
<>
<input
value={inputValue}
onChange={handleChange}
{...props}
/>
{error && (
<span className={"error"}>{error}</span>
)}
</>
)
}
export default MyInputComponent;
This is a very rudimentary component. just to show the concept. You can then import this component as your input field and pass necessary props like name, className etc from parent.
import React from 'react';
import MyInputComponent from 'components/MyInputComponent';
const MyForm = (props) => {
return props.data && props.data.map(data=> (
<MyInputComponent
name="lastName"
className="form-control"
value={data.lastName}
));
}