Async Form Validation

Last modified by Marius Dumitru Florea on 2022/09/13

When working with HTML forms a recurring need is to be able to validate the form data. This is normally done both:

  • on the client-side (when the user inputs the data or before the data is submitted) and
  • on the server-side (after the data is submitted).

On the client-side, the validation can be done both:

  • synchronously (e.g. when checking if the mandatory fields are filled and if the field values match the expected format)
  • and asynchronously, when the JavaScript code doesn't have all the information needed for the validation so it has to make one or more HTTP requests to the server-side (e.g. to check if the typed page name exists already)

When a form is validated asynchronously we usually want to:

  • postpone the submit until all validations are executed (until there are no pending validations)
    • allow the user to trigger the form submit even if there are pending validations, but disable the form while waiting for them to be executed
  • prevent the submit if there are failed validations (e.g. by disabling the submit button)
  • be able to abort / replace a previous validation for a specific field when the user changes its value
    • re-enable the submit button when a failed validation is replaced, if there are no other failed validations
  • be able to delay the validation in order to give the user the chance to type (we don't want to validate after each keystroke because HTTP requests are expensive)

This can be achieved using the xwiki-form-validation-async JavaScript module that provides a jQuery plugin. Suppose you have the following HTML form:

<form>
 <!-- The fieldset is needed in order to be able to disable the entire form easily. -->
  <fieldset>
   <!-- This is the field that needs to be validated asynchronously. -->
    <input type="text" name="title" value="" />
   <!-- This is where we display the validation error. -->
    <span class="xErrorMsg hidden"></span>
    <input type="submit" value="Submit" />
  </fieldset>
</form>

You can implement asynchronous validation like this:

require(['jquery', 'xwiki-form-validation-async'], function($) {
 const titleInput = $('input[name=title]');
 const titleError = titleInput.next('.xErrorMsg');

 const validateTitle = () => {
   if (!titleInput.val()) {
     return Promise.reject('Please enter the title.');
    } else {
     return new Promise((resolve, reject) => {
       // Perform the asynchronous validation, calling resolve() or reject('error message') when done.
       ...
      });
    }
  };

  titleInput.on('input', () => {
   // Hide the last error message whenever the user changes the value.
   titleError.addClass('hidden');
   // Show a visual indicator while the validation is in progress.
   titleInput.addClass('loading');
   // Schedule the asynchronous validation after 500ms. The namespace is used to prevent replacing validations added by
   // other modules.
   titleInput.validateAsync(validateTitle, /* delay: */ 500, /* namespace: */ 'myModule').catch(error => {
     // Show the error message. Note that this code is executed only if the validation has not become outdated since it
     // was scheduled.
     titleError.removeClass('hidden').text(error);
    }).finally(() => {
     // Remove the visual indicator once the validation is done. Note that this code is executed only if the validation
     // has not become outdated since it was scheduled.
     titleInput.removeClass('loading');
    });
  });
});

The validateAsync function can be called in multiple ways:

// Schedule a validation after 500ms, specifying the namespace.
$('#myFormField').validateAsync(() => Promise.resolve(), 500, 'myModule')

// You can omit the namespace if you know for sure that other modules don't add validations to the same field.
$('#myFormField').validateAsync(() => Promise.resolve(), 500)

// You can also pass directly a Promise, instead of a function returning a Promise. This means the validation is
// scheduled right away (for the next event cycle). This is useful if you want to force a quick validation when the
// submit is triggered.
$('#myFormField').validateAsync(Promise.resolve(), 'myApp')

// Same, without the namespace.
$('#myFormField').validateAsync(Promise.resolve())

Implementation Notes

Here are a few details regarding how the xwiki-form-validation-async module is implemented:

  • It captures the submit event on the document object and prevents the default event behavior, also stopping its propagation, while there are pending and failed validations. This happens on the event capturing phase, so before most of the other submit event listeners are called, effectively blocking them. It then re-triggers the submit event once all validations are fulfilled, which executes the other submit event listeners.
  • It expects the form content to be wrapped in a fieldset tag. It won't fail if the fieldset is missing, but the form won't be disabled while a submit is pending (due to pending validations). The risk in such case is that the user may trigger another submit (after changing the data), which may lead to invalid data being submited.
  • The validation key is computed based on the field id/name and the provided namespace. If you call validateAsync on a form field that doesn't have an id/name then you may overwrite an existing validation without being aware of it. The same can happen if you don't provide the namespace and multiple modules add validations for the same form field.
Tags:
   

Get Connected