The frustrations of form validation

Recently I was tasked with redeveloping the registration process here at Crowdcube. My first, admittedly foolish, response was “that sounds straightforward”. Looking back on that now I realise that I was wrong — very wrong.

Aside from the social logins, the registration form consists of only five required inputs: first name, last name, email address, password and nickname. Setting up the initial HTML form for this was fairly simple; I could also post the form off to our dedicated API, and voila — registration worked!

The API took care of server-side validation, but I still wanted to add some client-side validation to make the registration experience nicer. Client-side validation has a lot of benefits, including:

  • An instant feedback loop makes the UI more responsive to user input
  • It catches input mistakes on-the-fly, when they happen
  • Allows you to display errors and warnings before the user hits the submit button

These benefits can all help to reduce friction and frustration. In theory, this can help to lower the drop-out rates during key processes such as registration.

The HTML5 constraints API

Being a diligent developer, I did some research into client side validation. There are various libraries and packages out there (such as Parsley JS), but each seems to have drawbacks, e.g. large file size, dependency on jQuery, etc.

However, the HTML5 constraints validation API had been recommended to me, so I wanted to check this out first. At first glance it seems to offer some benefits:

  • Validation is native, based on HTML attributes, so no extra JS is required
  • You can use JS to add custom validation (e.g. AJAX)
  • It has good modern browser support
  • A polyfill was available to help support IE9

At this point I was a very happy bunny. I went off and created a super small validation library that extended this API and also accepted methods for AJAX validation (I needed this to check our DB if email addresses and nicknames already existed), setting the validity of the inputs as and when required:

let emailElement = window.document.getElementById('input-email');
emailElement.setCustomValidity('Email address already exists');

Seems great, right?

I finished and tested the form; it worked in modern browsers, the polyfill patched IE9, I gave it a good bashing with valid and invalid inputs, the automated tests passed, and I was away. Or so I thought…

The registration form went live, but within a day we had a bunch of complaints that some people couldn’t register. Not good.

It turns out that there are some browser inconsistencies with the constraints validation API, my testing was clearly not thorough enough and there were edge cases that my automated tests did not pick up.

Git revert, git revert!

No need to panic kids; we use version control. Nuff said.

So what went wrong?

After some lengthy team investigation we found a few validation related quirks that were clearly obstacles to registration.

Checking for minimum lengths

The required attribute captured null entries, but it turns out that minlength is not a fully supported attribute when it comes to validation. Some modern browsers support it, but (at the time) Safari and Firefox did not set a tooShort invalidity status, as the property does not exist in the validation API. In this case, you could get through registration using these browsers by entering a single character for some inputs, e.g. password or nickname.

It is worth noting that the API caught this as a fallback, but the experience was obviously frustrating.

Deprecated

I received another kick in the teeth after launch: the polyfill became deprecated. Not wanting to continue with an unsupported plugin I checked out Hyperform: the recommended replacement.

I was pulling my hair out at this point, as it made a mess of my crafted forms. In fairness, it may be a great plugin, but for me it tried to do too much and take over my forms and UI. Admittedly, I wasn’t in the mood for this at that point, so didn’t pursue this as an option.

Email validation inconsistency

According to the constraints validation API me@gmail is a valid email address. “Technically” this is correct, although most mail senders do not accept these. We use MailChimp to send system emails, and they certainly don’t accept such email addresses. The problem I had here was that the client-side validation passed, but the API returned an emphatic “NO!”. You could not register with such an email address, even though the browser allowed the input.

This was the final straw.

The righteous path

At this point I had decided that I needed a new approach. I was getting nowhere fast and adding copious amounts of code to patch inconsistencies, when the whole point was to reduce the amount of code required.

After discussions with my fellow developers, I decided that a programmatic approach was required. I was no longer going to rely on the browser and their inconsistencies to perform validation for me; I was going to dictate what was validated, how and when.

I now have a small function that purely listens for input events using addEventListener, setting a debounce (using the lightweight just-debounce package) before validation is triggered. This prevents validation from firing too often, which can interrupt someone whilst they are typing and distract them from the task of filling in forms.

I can then pass in any validation as a callback function when the event is fired, using a great lightweight validation package called validator. I even use sexy ES6 promises (my favourite) to return meaningful error messages, which only get displayed when the promise has been fulfilled.

Here is a snippet of the final code:

HTML:

<input type="email" name="email" id="input-email" value="" />
<span id="error-email"></span>

JS:

import debounce from 'just-debounce';
import isEmail from 'validator/lib/isEmail';
import emailValidator from './email-validator'; // Local promised based validator methods for our API
// Get the DOM elements
let emailInputElement = window.document.getElementById('input-email');
let emailErrorElement = window.document.getElementById('error-email');
// These are the input events we listen for
let events = ['input', 'change', 'blur'];
// Email validation, fulfils a promise
let validateEmail = function () {
return new Promise((resolve, reject) => {
if (!emailInputElement.value) {
// Empty value
reject('Please enter your email address');
} else if (!isEmail(emailInputElement.value)) {
// Too short
reject('Please enter a valid email address');
}
        // AJAX check
emailValidator.exists().then(function () {
resolve();
}).catch((error) => {
reject(error);
});
});
};
// Loop each of our input events
events.forEach((event) => {
// Add event listeners
emailInputElement.addEventListener(event, debounce(function () {
// Set/unset the error message
validateEmail().then(() => {
emailErrorElement.innerText = "";
}).catch((error) => {
emailErrorElement.innerText = error;
});
}, 300));
});

As you can see, this is a relatively small amount of JavaScript, but is very powerful.

  • It allows you to perform any validation method
  • It waits until the user has stopped typing before performing any validation
  • Any error message is displayed based on the message as rejected by the validation promise

What I have learnt

I should have set out my testing much better; although these inconsistencies were edge cases and I couldn’t have predicted each one, I should have prepared for things going wrong.

The HTML constraints validation API shows some good signs, but I personally feel that it has too many weaknesses to be reliable in production. It also provides instant feedback, which sounds nice, but this can be visually jarring if validation messages keep flashing up during typing.

I‘m not so keen to rely on the browser for accurate feedback now that I know such obscure inconsistencies exist with what is a relatively young and experimental API. I feel much happier being programmatically in control of the events that happen and when and how to deal with them. This is a philosophy that I will carry forward into other areas.

What’s next?

I would like to turn this into a reusable library. It would be great if I could have a core function that handles listeners and debouncing, etc, accepting callbacks for custom validation.

I would then be able to hook this library into any form in the future and not have to rebuild validation every time, safe in the knowledge that it works exactly as I want it to.