I am working on a chat app with React 18 and Firebase.
In the src\pages\Register.jsx
component, I have a form that I validate with Simple Body Validator:
import React, { useState } from "react";
import FormCard from '../components/FormCard/FormCard';
import { make } from 'simple-body-validator';
export default function Register() {
const initialFormData = {
firstName: '',
lastName: '',
email: '',
password: '',
passwordConfirm: ''
};
const validationRules = {
firstName: ['required', 'string', 'min:3', 'max:255'],
lastName: ['required', 'string', 'min:3', 'max:255'],
email: ['required', 'email'],
password: ['required', 'confirmed'],
passwordConfirm: ['required'],
};
const validator = make(initialFormData, validationRules);
const [formData, setFormData] = useState(initialFormData);
const [errors, setErrors] = useState(validator.errors());
const handleChange = (event) => {
const { name, value } = event.target;
setFormData((prevFormData) => ({ ...prevFormData, [name]: value }));
};
const handleSubmit = (event) => {
event.preventDefault();
if (!validator.setData(formData).validate()) {
setErrors(validator.errors());
}
};
return (
<FormCard title="Register">
<form onSubmit={handleSubmit}>
<div className={`mb-2 form-element ${errors.has('firstName') ? 'has-error' : null}`}>
<label for="firstName" className="form-label">First name</label>
<input type="text" id="firstName" name="firstName" value={formData.firstName} onChange={handleChange} className="form-control form-control-sm" />
{ errors.has('firstName') ? (
<p className="invalid-feedback">{errors.first('firstName')}</p>
) : null }
</div>
<div className={`mb-2 form-element ${errors.has('lastName') ? 'has-error' : null}`}>
<label for="lastName" className="form-label">Last name</label>
<input type="text" id="lastName" name="lastName" value={formData.lastName} onChange={handleChange} className="form-control form-control-sm" />
{ errors.has('lastName') ? (
<p className="invalid-feedback">{errors.first('lastName')}</p>
) : null }
</div>
<div className={`mb-2 form-element ${errors.has('email') ? 'has-error' : null}`}>
<label for="email" className="form-label">Email address</label>
<input type="email" id="email" name="email" value={formData.email} onChange={handleChange} className="form-control form-control-sm" />
{ errors.has('email') ? (
<p className="invalid-feedback">{errors.first('email')}</p>
) : null }
</div>
<div className={`mb-2 form-element ${errors.has('password') ? 'has-error' : null}`}>
<label for="password" className="form-label">Password</label>
<input type="password" id="password" name="password" value={formData.password} onChange={handleChange} className="form-control form-control form-control-sm" />
{ errors.has('password') ? (
<p className="invalid-feedback">{errors.first('password')}</p>
) : null }
</div>
<div className={`mb-2 form-element ${errors.has('passwordConfirm') ? 'has-error' : null}`}>
<label for="password_repeat" className="form-label">Confirm Password</label>
<input type="password" id="passwordConfirm" name="passwordConfirm" value={formData.passwordConfirm} onChange={handleChange} className="form-control form-control form-control-sm" />
{ errors.has('passwordConfirm') ? (
<p className="invalid-feedback">{errors.first('passwordConfirm')}</p>
) : null }
</div>
<div className="pt-1">
<button type="submit" className="btn btn-sm btn-success fw-bold">Submit</button>
</div>
</form>
</FormCard>
);
}
If the form is invalid, once a submit is adempted, the validation error messages are displayed as expected:
If valid data is introduced into an invalid form, the validation error messages and classes do not disappear on input/change, as I believe it should.
The valid fields pass the validation only upon a new click of the submit button while I want the feedback to be instant, similar to the AngularJS or Angular2+ forms.
There is a sandbox with the code here. The registration route is /auth/register
.
I have noticed that the library you're using does not handle form states(e.g. dirty, pristine).
So, here would be my approach:
/* ... */
const initialFormData = {
firstName: "",
lastName: "",
email: "",
password: "",
passwordConfirm: ""
};
const [pristineFields, setPristineFields] = useState(() =>
Object.keys(initialFormData)
);
const handleChange = (event) => {
const { name, value } = event.target;
setFormData((prevFormData) => {
const newFormData = { ...prevFormData, [name]: value };
const newPristineFields = pristineFields.filter((f) => f !== name);
validator.setData(newFormData).validate();
const validationErrors = validator.errors();
// 'Forgetting' about pristine fields.
newPristineFields.forEach((f) => validationErrors.forget(f));
setErrors(validationErrors);
setPristineFields(newPristineFields);
return newFormData;
});
};
/* ... */
I have also added these lines in the submit
handler:
const handleSubmit = (event) => {
event.preventDefault();
if (!validator.setData(formData).validate()) {
setErrors(validator.errors());
} else {
console.log("all good");
}
// Validations are shown either way, so we could forget
// about any pristine fields at this point.
setPristineFields([]);
};
Here is the link to the updated CodeSandbox application.
const CONFIRM_PASSWORD_KEY = "passwordConfirm";
const PASSWORD_KEY = "password";
export default function Register() {
const initialFormData = {
firstName: "",
lastName: "",
email: "",
[PASSWORD_KEY]: "",
[CONFIRM_PASSWORD_KEY]: ""
};
...
Then, in the setFormData
's callback, I have added the logic for password confirmation:
...
setFormData((prevFormData) => {
const newFormData = { ...prevFormData, [name]: value };
const newPristineFields = pristineFields.filter((f) => f !== name);
validator.setData(newFormData).validate();
const validationErrors = validator.errors();
newPristineFields.forEach((f) => validationErrors.forget(f));
// Other cases can be handled here..
if (name === CONFIRM_PASSWORD_KEY) {
if (value !== newFormData[PASSWORD_KEY]) {
validationErrors.add(CONFIRM_PASSWORD_KEY, {
message: "Passwords do not match."
});
}
}
setErrors(validationErrors);
setPristineFields(newPristineFields);
return newFormData;
});
...