Form validation system in vanilla JavaScript
I've been implementing lately a validation management system on the forms. I believe that creativity is one of the best parts of being a developer, actually I do not believe it only, I feel it as well.
I don't want to write a too-long post, I'd like to jump straightaway to the point, but also, I'd like to give you an understanding of why validation management on our forms is essential.
1. Admission
In this blog post, I’m going to walk you through a validation system written in pure JS. It is going to be a tutorial, step by step introduction to building a custom form validation system.
In this tutorial, I’m not going to show you how to configure and set-up webpack and how to import other files. We’re going to create our simple validation system in one file to show you and give you an idea on how to create a simple form validation mechanism.
If you want to use webpack for it and use code splitting, go ahead, it is welcomed because that’s what I’d do.
2. Validation errors management
2.1 HTML form
<html>
<head>
<title>Validation state mananagement</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<form class="form-1" method="POST">
<div class="group">
<label for="username" class="block-element">Username:</label>
<input type="text" class="form-control input-1" name="username" placeholder="Your username">
<span class="text-danger hide">Username is too short or contains special characters.</span>
</div>
<div class="group">
<label for="username" class="block-element">Password:</label>
<input type="password" class="form-control input-1" name="password">
<span class="text-danger hide">The password has to consist of one upper case letter, one number and one special character.</span>
</div>
<button class="js-submit-user btn btn-2" tabindex="0">Submit</button>
</form>
<script src="./validation.js"></script>
</body>
</html>
Above you can see our simple HTML code. I don’t think there is something more to explain in this step, but let me go through it quickly.
In our body, we’ve got the form element that has a class named “form-1”, and it uses the “POST” method to send the data to the server. Inside of the form, we’ve got two divs having classes named “group”. Our “group” class is going to help us to separate those two inputs and its labels from each other.
The last element in the form is a button. This button is going to submit our form to the server. As you can see, it has three classes. I suspect that you already know what “btn” and “btn-2” class does. We’re going to use our “btn” class as a root class for our button and “btn-2” class is going to be our extension of this class.
Another exciting approach I started using some time ago is naming classes by adding “js” at the beginning of the name. Why? It implies that this button has a JavaScript functionality attached to itself. So we know then that we can find this functionality by looking for “js-submit-user” class in our JS code.
Great, I have explained an HTML part, let’s go to the most important one – JavaScript code.
2.2 JavaScript code
We’re going to kick off this part from creating a submit form function in our “validation.js” file. This function is initializing when we open the page. Which means that event listener is attached to our button straightaway.
Firstly, we’re going to add two lines of code at the top of our file. We’re going to need those lines of code while writing additional functions.
const validationState = new Set();
const loginForm = document.forms[0];
In brief, our “validationState” is a new set which is uniquely storing our input names and acts as our state. Our “loginForm” variable has assigned our form, where we’ve got our inputs and button. We’re going to use those two later on.
Let’s focus on writing our function that is going to submit the form.
// The function submits the form
function submitForm() {
const submitButton = document.getElementsByClassName('js-submit-user')[0];
submitButton.addEventListener('click', function(event) {
event.preventDefault();
manageState().validateState();
});
}
Let’s take a look at our function and what is happening here. We’re initializing constant variable named “submitButton”, and we are assigning to it our submit form button.
Next step is to make sure that we’re attaching a click event to our button. When the event happens after clicking on the button, it doesn't send the form because of the “preventDefault” mechanism. We’re using it to validate the form instead of sending it straight away to the server.
The second function call, which is “manageState().validateState()” will be introduced in the next steps of the tutorial.
Well, we’ve got our function to submit the form, it will not work until we invoke the function, we’re going to do it after creating another necessary function.
// Attaching 'keyup' event to the login form.
// Using event delegation
function attachKeyUpEvent() {
loginForm.addEventListener('keyup', function(event) {
const nodeName = event.target.nodeName;
const inputProps = event.target;
if(nodeName === 'INPUT') {
validateForm(inputProps);
}
});
}
Our “attachKeyUpEvent” function is allowing us to get every single character typed in our inputs to validate our values at the same moment. Let’s jump into our function.
We are attaching the “keyup” event to our “loginForm” variable that we declared on the top of our file. Remember? This variable stores our form that contain two inputs and one button. Why are we attaching “keyup” event to the form, if we have to catch typed characters from inputs? That’s the good thing; we’re using event delegation, which is allowing us to recognize what has been changed.
Whatever we do in our form that qualifies to be considered by “keyup” event, we’re going to get it in our parameter called, “event”. Our “event” parameter is going to be an object, where one of the properties is named “target”.
As you can see, in the scope of our event listener, I’m declaring two constant variables. The first one, “nodeName” is getting the node name of our element, and the second one represents the target property of the event, named “inputProps”. In the next stage, we’re checking if our node name equals input.
Which means that we are checking if the event listener reacted to changes in our input, and if it did, we’re going to pass these input properties to the function “validateForm” as a parameter.
The next step is to initialize those two functions; we’re going to do it in a flexible and organized way.
function init() {
attachKeyUpEvent();
submitForm();
}
document.addEventListener('DOMContentLoaded', init);
When our HTML code is loaded, we’re going to invoke our “init” function that is going to invoke other two functions necessary to make our validation system work properly. If you are beginner, and you didn’t see ‘DOMContentLoaded’ event before, I’ll explain it to you in a brief way.
This event is going to be fired once the entire content of the page loads, it allows you to attach JavaScript functionality to your DOM elements when they’re in the right place. I hope it helps let’s move ahead.
Below you can see what we’ve got so far:
function attachKeyUpEvent() {
loginForm.addEventListener('keyup', function(event) {
const nodeName = event.target.nodeName;
const inputProps = event.target;
if(nodeName === 'INPUT') {
validateForm(inputProps);
}
});
}
function submitForm() {
const submitButton = document.getElementsByClassName('js-submit-user')[0];
submitButton.addEventListener('click', function(event) {
event.preventDefault();
manageState().validateState();
});
}
function init() {
attachKeyUpEvent();
submitForm();
}
document.addEventListener('DOMContentLoaded', init);
The first part of the tutorial is behind us. We’re going to go through implementation more in-depth. Next step is to write and explain our “manageState” function that invokes in “submitForm” function.
// Collection of functions for managing state
function manageState() {
return {
addToState: (inputData) => {
const action = 'removeClass';
const { inputProps, inputName } = inputData;
validationState.add(inputName);
manipulateValidationMsg({ inputProps, action });
},
removeFromState: (inputData) => {
const action = 'addClass';
const { inputProps, inputName } = inputData;
validationState.delete(inputName);
manipulateValidationMsg({ inputProps, action})
},
validateState: () => {
if(validationState.size > 0) {
return false;
}
if(validationState.size === 0) {
validationRules().emptyFields();
return true;
}
}
}
};
For now, we’re going to skip functions other than “validateState”. As you can see, I have used closures here. Why did I do it? I did it because I wanted to keep the “manageState” function consist, organized and what is the most important to make sense of its name if you look at the function it contains only functions related to the state.
Let’s focus on “validateState” function. This function is being triggered in “submitForm” function, which means that when we click on our button, this function will be invoked and its functionality.
We’re going to check if our validation state has some fields with validation errors by checking if the size of our validation state is larger than zero and if so, it will not allow us to send the form by only returning false. Another step is, if our validation state doesn’t have any validation errors, the size will be zero, which means that the JavaScript engine will visit the scope of this if statement scope.
In the scope, it will invoke “emptyFields” function, which validates if our form contains fields with no values inside, if so, it will not allow us to send the form, if not, it will return the true and allow us to send the form.
I will introduce the functionality of “emptyFields” function in the next steps.
It’s time to jump to the primary function where everything starts. I’m going to explain the functionality of “validateForm” function placed in the scope of “attachKeyUpEvent”.
The first thing is, we’re passing a parameter which is our “event.target” properties we assigned to our variable called “inputProps”. Let’s have a look at “validateForm” function.
// Function receives an input with its properties
function validateForm(inputProps) {
const inputName = inputProps.name;
const verifyInputName = {
'username': validationRules().username,
'password': validationRules().password
};
return verifyInputName[inputName](inputProps)
}
I know that you’re probably wondering, why am I passing all of the properties if I’m using only name attribute from all of them. I’m passing all of the properties, in case if I want to extend this function and use different properties from this object one day and also to use other properties while passing them to one of the validation rules’ functions.
In the scope, we’ve got two constant variables. The first one, “inputName” has assigned the name of the input which was added to it as an attribute on the HTML element. If you come back to our HTML code, you’ll see that each input has its name attribute. The second one is an object literal which contains a key, named in the same way as each input’s name attribute. The value of the key is referencing to functions with specific validation rules for each input.
As you can see, we’re returning a result of a function from “validationRules” scope. By passing “inputProps” parameter to one of the functions, we’re sending entire input event object to it. Let’s have a look at “validationRules” closure.
// Validation rules for each field in our form.
function validationRules() {
return {
username: (inputProps) => {
const usernameValidationRule = /[A-Za-z0-9]{6,}/;
const inputValue = inputProps.value;
const inputName = inputProps.name;
const isInputValid = usernameValidationRule.test(inputValue);
isInputValid ? manageState().removeFromState({inputProps, inputName}) : manageState().addToState({inputProps, inputName});
return true;
},
password: (inputProps) => {
const passwordValidationRule = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+=!*()@%&]).{8,10}$/g;
const inputValue = inputProps.value;
const inputName = inputProps.name;
const isInputValid = passwordValidationRule.test(inputValue);
isInputValid ? manageState().removeFromState({inputProps, inputName}) : manageState().addToState({inputProps, inputName});
return true;
},
emptyFields: () => {
const formInputElems = [...loginForm.elements].filter(item => item.nodeName === 'INPUT');
for(const inputProps of formInputElems) {
const inputName = inputProps.name;
const inputValue = inputProps.value;
if(!inputValue) {
manageState().addToState({inputProps, inputName});
}
}
}
}
}
Above we’ve got our closure, where we’re having functions responsible for validation of particular inputs — the first one which is called “username”. As the name suggests, it is responsible for validating our username input. I will describe this function from a deeper perspective, and I won’t be going through the “password” function because it works in the same way as “username” one. I will go through “emptyFields” one for sure.
In our “username” function we’re declaring 4 variables. The first one “usernameValidationRule” is storing our regex expression, which is allowing the user to create the username from uppercase, lowercase characters and numbers. Also, the username has to have at least 6 characters to be valid.
Moving forward, we’ve got “inputValue” and “inputName”, I think I don’t have to explain those two as the name and assignment suggests everything. The last one – “isInputValid” is having assigned the result of the regex test, which is going to be true or false as a Boolean type of value.
isInputValid ? manageState().removeFromState({inputProps, inputName}) : manageState().addToState({inputProps, inputName});
Above ternary condition is the last operation we perform in our function. It checks if “isInputValid” is true if it is, it’s going to remove it from the state, and if it’s false, it will add our input name to the state. As you can see, we’re using there “manageState” function, which is having “removeFromState” and “addToState” child functions. We’re passing two parameters as one object to mentioned functions. We’re passing “inputProps”, which is an event object of the input and also “inputName” which is the name attribute and its value. We will come back to those two functions, once I explain “emptyFields” function placed in our “validationRules”.
Our first variable in the scope of “emptyFields” is “formInputElems”. It stores all of the elements from our form that are inputs. We’re using the spread operator to convert our form to an array of elements because, in the first place, our form is not a valid array. Next step is to filter all of the elements that are inputs.
Next thing, we go through all of the elements we filtered and assignee the name attribute and the value to separated variables. The last step is to check if the input is empty or not. If it doesn’t have any value, we will add it to the state and throw an error; if it does, we will ignore it. That was the brief explanation of “emptyFields” function; we’re going to move to “manageState” scope and take a look at its child functions.
addToState: (inputData) => {
const { inputProps, inputName } = inputData;
validationState.add(inputName);
manipulateValidationMsg(inputProps);
},
removeFromState: (inputData) => {
const action = ‘hide’;
const { inputProps, inputName } = inputData;
validationState.delete(inputName);
manipulateValidationMsg({ inputProps, action})
},
Let’s talk about the above functions. The first one – “addToState”, has a destructive assignment at the top of the scope. The parameter we’re sending – “inputData” is an object containing two properties. It contains inputProps and inputName. The next step is to add our input name on the blacklist of invalid inputs. We’re adding our input to validationState set and storing it there until the value of the input is valid. The next step is a little bit different. We do have to manipulate our validation messages, that’s where “manipulateValidationMsg” function comes into play. Take a look at the function code down below:
// Function manipulates validation messages by toggling them
function manipulateValidationMsg(validationData) {
const { inputProps, action } = validationData;
const elementValidationMsg = inputProps.nextElementSibling;
const validationMsgClasses = elementValidationMsg.classList;
const removeClass = () => {
validationMsgClasses.remove('hide');
};
const addClass = () => {
validationMsgClasses.add('hide');
};
return action === 'hide' ? addClass() : removeClass();
}
We’ve got our function to manipulate validation messages. We’ve got two properties in our destructive assignment, inputProps and action. I will explain what “action” property is when I move on to “removeFromState” function. In the next step, we declare a variable, and we’re assigning to it, next sibling of our input.
What does it mean? It means that we’ve got a specific pattern to follow when we build our form. It means that our validation message has to be next to our input and be its sibling to work correctly. Moving forward, we’ve got our validation message class list, which is necessary to have, to hide and show our message. In the next step, we’re declaring two functions, responsible for removing and adding “hide” class to our message element.
In the end, we’re checking if our action equals ‘hide’, if it does, we add hide class, which means that the value of our input is valid, if not we remove the class, which has the opposite meaning. Fantastic, I think we’ve got it covered. I hope you understand it so far.
Now is the moment to jump straightaway to “removeFromState” function. I will not be going through this function as it has the same code as “addToState” function apart from one detail, which is our “hide” class and “action” parameter.
Our action variable in a “removeFromState” function scope, has assigned ‘hide’ class, which is being sent to the function that manipulates our validation messages. We’re using it to verify if we’re going to add hide class or remove it from our validation element. As easy as that, you can find an entire solution in my GitHub repository.
Validation State Management Github
If you’ve got any suggestions, opinions, please add a comment down below. I do hope that you’ve learnt something new and found it useful. If you didn’t like it, please tell me about it, constructive criticism is much appreciated.
Until next time!
This tutorial was initiall written on: Robert Wozniak - Dev blog