Recipe 7.10
Validating a Field Asynchronously
You can perform asynchronous tasks to validate fields. Your validation function should return a Promise
and set a custom validity message when the result is ready.
In the form’s submit handler, await the Promise
before checking the form’s validity. If the asynchronous validation failed, a custom validity message will be set and the error message will be shown.
Demo
Code
JavaScript
const form = document.querySelector('#signup-form');
/**
* Calls an API to validate that a password meets strength requirements.
* @param form the form containing the password field
*/
async function validatePasswordStrength(form) {
const { password } = form.elements;
const response = await fetch(`/api/password-strength?password=${password.value}`);
const result = await response.json();
if (result.status === 'error') {
password.setCustomValidity(result.error);
} else {
password.setCustomValidity('');
}
}
/**
* Updates the async validation when focus is lost.
* Do this on blur rather than input to prevent sending a request on each keystroke.
*/
form.elements.password.addEventListener('blur', async event => {
const password = event.target;
const errorElement = document.getElementById('password-error');
if (password.dataset.shouldValidate) {
await validatePasswordStrength(form);
if (password.checkValidity()) {
errorElement.textContent = '';
password.classList.remove('border-danger');
}
}
});
/**
* Adds the necessary event listeners to an element to participate in form validation.
* It handles setting and clearing error messages depending on the validation state.
* @param element The input element to validate
*/
function addValidation(element) {
const errorElement = document.getElementById(`${element.id}-error`);
/**
* Fired when the form is validated and the field is not valid.
* Sets the error message and style, and also sets the shouldValidate flag.
*/
element.addEventListener('invalid', () => {
element.classList.add('border-danger');
errorElement.textContent = element.validationMessage;
element.dataset.shouldValidate = true;
});
/**
* Fired when user input occurs in the field. If the shouldValidate flag is set,
* it will re-check the field's validity and clear the error message if it becomes valid.
*/
element.addEventListener('input', () => {
if (element.dataset.shouldValidate) {
if (element.checkValidity()) {
element.classList.remove('border-danger');
errorElement.textContent = '';
}
}
});
/**
* Fired when the field loses focus, applying the shouldValidate flag.
*/
element.addEventListener('blur', () => {
// This field has been touched, it will now be validated on subsequent
// `input` events.
element.dataset.shouldValidate = true;
});
}
addValidation(form.elements.username);
addValidation(form.elements.password);
form.addEventListener('submit', async event => {
event.preventDefault();
await validatePasswordStrength(form);
console.log(form.checkValidity());
});
HTML
<form id="signup-form" novalidate>
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input required type="text" id="username" name="username" class="form-control">
<div class="error-message text-danger" id="username-error"></div>
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<div><small>Passwords must be at least 8 characters and must contain at least one number</small></div>
<input required type="password" id="password" name="password" class="form-control">
<div class="error-message text-danger" id="password-error"></div>
</div>
<button class="btn btn-primary">Submit</button>
</form>