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

Passwords must be at least 8 characters and must contain at least one number

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>
Web API Cookbook
Joe Attardi