Realtime Form Validation
Last week, I went over some Techniques for Form Validation, including using CSS for visual feedback as well as the Constraint Validation API. I also discussed the benefits of validating inputs in realtime, and so this week I built out this simple registration form, demonstrating realtime validation.
There are three key features of this form -
- The requirements for each input are clearly displayed even before the user starts typing
- As the user starts typing on the field and fulfils each requirement, they are given real time feedback on their success (or failings)
- The feedback is presented in such a way that the user will not submit the form without knowing of the errors in their input
If you'd like to see a walkthrough of how I built this out, you can watch the video below.
And here is the JavaScript behind the realtime validation -
/* ----------------------------
CustomValidation prototype
- Keeps track of the list of invalidity messages for this input
- Keeps track of what validity checks need to be performed for this input
- Performs the validity checks and sends feedback to the front end
---------------------------- */
function CustomValidation() {
this.invalidities = [];
this.validityChecks = [];
}
CustomValidation.prototype = {
addInvalidity: function(message) {
this.invalidities.push(message);
},
getInvalidities: function() {
return this.invalidities.join('. \n');
},
checkValidity: function(input) {
for ( var i = 0; i < this.validityChecks.length; i++ ) {
var isInvalid = this.validityChecks[i].isInvalid(input);
if (isInvalid) {
this.addInvalidity(this.validityChecks[i].invalidityMessage);
}
var requirementElement = this.validityChecks[i].element;
if (requirementElement) {
if (isInvalid) {
requirementElement.classList.add('invalid');
requirementElement.classList.remove('valid');
} else {
requirementElement.classList.remove('invalid');
requirementElement.classList.add('valid');
}
} // end if requirementElement
} // end for
}
};
/* ----------------------------
Validity Checks
The arrays of validity checks for each input
Comprised of three things
1. isInvalid() - the function to determine if the input fulfills a particular requirement
2. invalidityMessage - the error message to display if the field is invalid
3. element - The element that states the requirement
---------------------------- */
var usernameValidityChecks = [
{
isInvalid: function(input) {
return input.value.length < 3;
},
invalidityMessage: 'This input needs to be at least 3 characters',
element: document.querySelector('label[for="username"] .input-requirements li:nth-child(1)')
},
{
isInvalid: function(input) {
var illegalCharacters = input.value.match(/[^a-zA-Z0-9]/g);
return illegalCharacters ? true : false;
},
invalidityMessage: 'Only letters and numbers are allowed',
element: document.querySelector('label[for="username"] .input-requirements li:nth-child(2)')
}
];
var passwordValidityChecks = [
{
isInvalid: function(input) {
return input.value.length < 8 | input.value.length > 100;
},
invalidityMessage: 'This input needs to be between 8 and 100 characters',
element: document.querySelector('label[for="password"] .input-requirements li:nth-child(1)')
},
{
isInvalid: function(input) {
return !input.value.match(/[0-9]/g);
},
invalidityMessage: 'At least 1 number is required',
element: document.querySelector('label[for="password"] .input-requirements li:nth-child(2)')
},
{
isInvalid: function(input) {
return !input.value.match(/[a-z]/g);
},
invalidityMessage: 'At least 1 lowercase letter is required',
element: document.querySelector('label[for="password"] .input-requirements li:nth-child(3)')
},
{
isInvalid: function(input) {
return !input.value.match(/[A-Z]/g);
},
invalidityMessage: 'At least 1 uppercase letter is required',
element: document.querySelector('label[for="password"] .input-requirements li:nth-child(4)')
},
{
isInvalid: function(input) {
return !input.value.match(/[\!\@\#\$\%\^\&\*]/g);
},
invalidityMessage: 'You need one of the required special characters',
element: document.querySelector('label[for="password"] .input-requirements li:nth-child(5)')
}
];
var passwordRepeatValidityChecks = [
{
isInvalid: function() {
return passwordRepeatInput.value != passwordInput.value;
},
invalidityMessage: 'This password needs to match the first one'
}
];
/* ----------------------------
Check this input
Function to check this particular input
If input is invalid, use setCustomValidity() to pass a message to be displayed
---------------------------- */
function checkInput(input) {
input.CustomValidation.invalidities = [];
input.CustomValidation.checkValidity(input);
if ( input.CustomValidation.invalidities.length == 0 && input.value != '' ) {
input.setCustomValidity('');
} else {
var message = input.CustomValidation.getInvalidities();
input.setCustomValidity(message);
}
}
/* ----------------------------
Setup CustomValidation
Setup the CustomValidation prototype for each input
Also sets which array of validity checks to use for that input
---------------------------- */
var usernameInput = document.getElementById('username');
var passwordInput = document.getElementById('password');
var passwordRepeatInput = document.getElementById('password_repeat');
usernameInput.CustomValidation = new CustomValidation();
usernameInput.CustomValidation.validityChecks = usernameValidityChecks;
passwordInput.CustomValidation = new CustomValidation();
passwordInput.CustomValidation.validityChecks = passwordValidityChecks;
passwordRepeatInput.CustomValidation = new CustomValidation();
passwordRepeatInput.CustomValidation.validityChecks = passwordRepeatValidityChecks;
/* ----------------------------
Event Listeners
---------------------------- */
var inputs = document.querySelectorAll('input:not([type="submit"])');
var submit = document.querySelector('input[type="submit"');
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener('keyup', function() {
checkInput(this);
});
}
submit.addEventListener('click', function() {
for (var i = 0; i < inputs.length; i++) {
checkInput(inputs[i]);
}
});
As always, there are improvements that can be made and since I built and recorded this I've already had some ideas for how to improve the form. For example -
- Give feedback when a user focuses out of an invalid input
- Use a custom tooltip in place of the in-built browser tooltips for more consistent and reliable behaviour across browsers
If you have more ideas, or feedback on the implementation, do leave a comment below.