Form validation with vanilla JS using data attributes on form elements

damirpristav
14 min readJul 3, 2020

This post is from my blog.

You can also check the video on youtube.

Create html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form Validation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Form Validation</h1>
</header>
<div class="container">
<form class="form" data-form>
<div class="form__group" data-formgroup>
<label for="name">Full name</label>
<input
type="text" id="name" name="name"
data-validate data-required data-required-message="Name is required"
>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group" data-formgroup>
<label for="email">Email</label>
<input
type="text" name="email" id="email"
data-validate data-required
data-email data-email-message="Email is not valid"
>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group" data-formgroup>
<label for="password">Password</label>
<input
type="password" name="password" id="password"
data-validate data-required
data-minlength="6" data-maxlength="16"
data-match-with="confirmPassword"
>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group" data-formgroup>
<label for="confirmPassword">Confirm Password</label>
<input
type="password" name="confirmPassword" id="confirmPassword"
data-validate data-required
data-match="password" data-match-message="Passwords must be equal"
>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group form__group--radio" data-formgroup>
<p>Gender</p>
<label>
<input type="radio" name="gender" value="female"
data-validate data-error-message="Gender is required">
<span>Female</span>
</label>
<label>
<input type="radio" name="gender" value="male" data-validate>
<span>Male</span>
</label>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group" data-formgroup>
<label for="difficulty">Difficulty</label>
<select name="difficulty" id="difficulty" data-validate data-required>
<option value="">Select difficulty</option>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
</select>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group" data-formgroup>
<label for="image">Image</label>
<input type="file" name="image" id="image"
data-validate data-required data-maxfilesize="1024"
data-allowed-types="jpg,png" data-allowed-types-message="Only jpg and png formats allowed"
>
<div class="form__error" data-formerror></div>
</div>
<div class="form__group">
<label for="description">Description</label>
<textarea name="description" id="description"></textarea>
</div>
<div class="form__group form__group--checkbox" data-formgroup>
<label>
<input type="checkbox" name="terms" id="terms"
data-validate data-error-message="You must accept our Terms and Conditions"
>
<span>Terms and Conditions</span>
</label>
<div class="form__error" data-formerror></div>
</div>
<button type="submit" class="btn">Submit</button>
</form>
</div>
<script src="form.js"></script>
</body>
</html>

To validate form add data-form attribute to form element. Next, add data-formgroup to each wrapper div. Each div has a label element, input element and a div for error message.

At this point nothing will be validated because we need to add few more attributes to the input element. First one is data-validate, this tells javascript to include this element to validation process. Now we need to add attributes to tell js what kind of validation it needs to check.

data-required, data-required-message

This one will tell js that input value is required. Every attribute also has corresponding attribute for error message. To add it just add -message to the attribute name attribute. For example for data-required we can add data-required-message=”This field is required”. If not added default error message will be used, this will be added in javascript.

data-minlength, data-minlength-message

This one is used to tell js that the input value length cannot be smaller than the number you put in this attribute, for example data-minlength=”5″. For error message use data-minlength-message. Again this attribute is optional, if not set default error message will be used.

data-maxlength, data-maxlength-message

This one check if the input value length is greater than the number you put in the attribute.

data-email, data-email-message

data-email checks if the input value is the valid email address.

data-match, data-match-with, data-match-message

This attributes are used on passwords. For example when you have registration form you will probably add confirm password field to allow user to repeat the password.

On original password field you can add data-match-with attribute, and the value of this attribute must be the value of the confirm password input name attribute. And on the confirm password input you must add match-with attribute with the value of the name attribute of the original password input. And for the error message you can add data-match-error to the confirm password input.

radio buttons

If you want to validate radio buttons, you must add data-validate to each radio button in a group, and for error message add data-error-message to the first radio button in the group.

checkbox

To validate checkbox add data-validate attribute to checkbox and for error message you can add data-error-message.

NOTE: for radio buttons and checkboxes you don’t need to add data-required attribute.

file input, data-maxfilesize, data-allowed-types

For file inputs you can add data-maxfilesize attribute, it will check if the file size is greater than the value of this attribute, and in this it will show the error. For error you can use data-maxfilesize-message.

And if you want to validate file type you can add data-allowed-types attribute. Value of this attribute is the file extension. To add multiple values separate them with comma, and no space after the comma. For example: data-allowed-types=”jpg,png,pdf”. For error message use data-allowed-types-message attribute.

And that’s it for the attributes. One more thing, each error div must have data-formerror attribute.

Create CSS

@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');:root {
--primary-color: #009e6c;
--error-color: #e70f0f;
}
* {
box-sizing: border-box;
margin: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 1.5;
}
header {
background-color: var(--primary-color);
color: #fff;
text-align: center;
padding: 50px 0;
margin-bottom: 50px;
}
.container {
max-width: 600px;
margin: 0 auto;
padding-bottom: 50px;
}
.form {
border: 1px solid #eee;
padding: 40px;
}
.form__group {
margin-bottom: 20px;
}
.form__group label,
.form__group p {
display: block;
font-size: 14px;
margin-bottom: 5px;
}
.form__group--radio label,
.form__group--checkbox label {
display: inline-flex;
align-items: center;
margin-right: 15px;
}
.form__group--radio label span,
.form__group--checkbox label span {
margin-left: 5px;
}
.form__group input[type="text"],
.form__group input[type="file"],
.form__group input[type="password"],
.form__group select,
.form__group textarea {
display: block;
width: 100%;
padding: 10px;
font-size: 14px;
border: 1px solid #eee;
outline: 0;
transition: box-shadow .3s ease;
}
.form__group input[type="text"]:focus,
.form__group input[type="file"]:focus,
.form__group input[type="password"]:focus,
.form__group select:focus,
.form__group textarea:focus {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.form__group input[type="text"].error,
.form__group input[type="file"].error,
.form__group input[type="password"].error,
.form__group select.error,
.form__group textarea.error {
border-color: var(--error-color);
}
.form__error {
color: var(--error-color);
font-size: 12px;
padding: 5px 0;
display: none;
}
.btn {
display: inline-flex;
align-items: center;
padding: 10px 20px;
background-color: var(--primary-color);
color: #fff;
border: 0;
outline: 0;
cursor: pointer;
}

CSS is very simple. I just added a bunch of styles to style the form. The most important thing is to hide error div by default and to add some class to input element when error is visible, in this example this is error class.

JS part

The first thing we must do in js is to get all form elements with data-form attribute, to check if they exist on page and then loop through them.

// Get elements
const forms = document.querySelectorAll('form[data-form]');
// Check if form elements exist
if(forms.length > 0) {
// Loop through forms
for(let form of forms) {
// Get all inputs with data-validate attribute
const inputs = form.querySelectorAll('[data-validate]');
// Submit form
form.addEventListener('submit', submitForm.bind(form, inputs));

// Loop through inputs
inputs.forEach(input => {
// Add input event to all inputs and listen to inputChange function
input.addEventListener('input', inputChange);
});
}
}

After that, for each form we can get form elements which needs to be validated, we can get them by [data-validate] attribute. We can then add event listener for input event to validate each input when the value of the input is changed. And also add event listener for submit event type on form, so that when submit button is clicked we can validate the inputs as well.

Now the functions we need:

// Input change
function inputChange() {
const input = this;
validateInput(input);
}
// Validate input
function validateInput(input) {
// Get the value and error element
const value = input.value;
const errorEl = input.closest('[data-formgroup]').querySelector('[data-formerror]');
// Declare error variable and assign null by default
let error = null;
// Check in input has data-required attribute and if the value is empty, and if the input is not radio or checkbox
if((input.type !== 'radio' || input.type !== 'checkbox') && input.dataset.required !== undefined && value === '') {
error = input.dataset.requiredMessage ? input.dataset.requiredMessage : 'This field is required';
input.classList.add('error');
}
// Check if input is checkbox and it is not checked
if(input.type === 'checkbox' && !input.checked) {
error = input.dataset.errorMessage ? input.dataset.errorMessage : 'This field is required';
}
// Check if input is radio
if(input.type === 'radio') {
// Get all radio inputs in a group
const radios = input.closest('[data-formgroup]').querySelectorAll('input[type="radio"]');
let isChecked = false;
let errorMsg = '';
// Loop through radios and check if any radio is checked and if it is checked set isChecked to true
radios.forEach(radio => {
if(radio.checked) {
isChecked = true;
}
if(radio.dataset.errorMessage) {
errorMsg = input.dataset.errorMessage;
}
});
if(!isChecked) {
error = errorMsg !== '' ? errorMsg : 'This field is required';
}
}
// Check if input has data-minlength attribute and if value length is smaller than this attribute, if so show the error
if(!error && input.dataset.minlength !== undefined && value.length < +input.dataset.minlength) {
error =
input.dataset.minlengthMessage ? input.dataset.minlengthMessage : `Please enter at least ${input.dataset.minlength} characters`;
input.classList.add('error');
}
// Check if input has data-maxlength attribute and if value length is greater than this attribute, if so show the error
if(!error && input.dataset.maxlength !== undefined && value.length > +input.dataset.maxlength) {
error =
input.dataset.maxlengthMessage ? input.dataset.maxlengthMessage : `Only ${input.dataset.maxlength} characters allowed`;
input.classList.add('error');
}
// Check if input has data-email attribute and if email is not valid
if(!error && input.dataset.email !== undefined && !validateEmail(value)) {
error =
input.dataset.emailMessage ? input.dataset.emailMessage : 'Invalid email address';
input.classList.add('error');
}
// Check if input has data-match attribute and if value is not equal to the value of the element with name attribute equal to this data-match attribute
if(!error && input.dataset.match !== undefined && value !== input.closest('[data-form]').querySelector(`[name="${input.dataset.match}"]`).value) {
error =
input.dataset.matchMessage ? input.dataset.matchMessage : 'Fields are not the same';
input.classList.add('error');
}
// Check if input has data-match-with attribute
if(input.dataset.matchWith !== undefined) {
// Get the input that has a name attribute equal to value of data-match-with attribute
const inputToMatch = input.closest('[data-form]').querySelector(`[name="${input.dataset.matchWith}"]`);
// Get the error element of that input
const inputToMatchError = inputToMatch.closest('[data-formgroup]').querySelector('[data-formerror]');
// If values are equal remove error class from input and hide error element
if(value === inputToMatch.value) {
inputToMatch.classList.remove('error');
inputToMatchError.style.display = 'none';
}else { // Add error class to input and show error element
inputToMatch.classList.add('error');
inputToMatchError.style.display = 'block';
inputToMatchError.innerText = inputToMatch.dataset.matchMessage || 'Fields are not the same';
}
}
// Check if input is file input and if has data-maxfilesize attribute and if file size is greater than the value of this data-maxfilesize attribute
if(!error && input.type === 'file' && input.dataset.maxfilesize !== undefined && input.files[0].size > +input.dataset.maxfilesize * 1024) {
error =
input.dataset.maxfilesizeMessage ? input.dataset.maxfilesizeMessage : 'File is too large';
input.classList.add('error');
}
// Check if input is file input and if it has data-allowed-types attribute and if file type is not equal to one of the values in data-allowed-type attribute
if(!error && input.type === 'file' && input.dataset.allowedTypes !== undefined && !input.dataset.allowedTypes.includes(input.files[0].type.split('/')[1])) {
error =
input.dataset.allowedTypesMessage ? input.dataset.allowedTypesMessage : 'Invalid file type';
input.classList.add('error');
}

// If there is no error remove error class from the input, remove message from error element and hide it
if(!error) {
input.classList.remove('error');
errorEl.innerText = '';
errorEl.style.display = 'none';
} else { // If there is error set error message and show error element
errorEl.innerText = error;
errorEl.style.display = 'block';
}
return error;
}
// Submit form - on submit btn click
function submitForm(inputs, e) {
e.preventDefault();
const errors = [];

inputs.forEach(input => {
const error = validateInput(input);
if(error) {
errors.push(error);
}
});
if(errors.length === 0) {
console.log('form can be submitted...');
}
}
// Validate email
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}

First off all we need inputChange function. In this function we get the input by this keyword and then we can just call validateInput function and pass the input as the argument.

For submitForm we do the same thing, except we pass all inputs with bind to loop through inputs and call validateInput on each input element. We can also create errors array and if validateInput returns error and not null we add this error to array. This way we can check after forEach if errors array is empty and only in that case we can submit the form.

Now the most important function, validateInput.

First we get the value of the input. And then the error div element and set the error variable to null by default.

// Get the value and error element
const value = input.value;
const errorEl = input.closest('[data-formgroup]').querySelector('[data-formerror]');
// Declare error variable and assign null by default
let error = null;

In the first if statement we check if the input type is not radio or checkbox and if input has data-required attribute and if it is empty. In this case we set the error to error message that was passed to data-required-message attribute or we use default error message if there is no data-required-message attribute on the input and we add error class to the input.

// Check in input has data-required attribute and if the value is empty, and if the input is not radio or checkbox
if((input.type !== 'radio' || input.type !== 'checkbox') && input.dataset.required !== undefined && value === '') {
error = input.dataset.requiredMessage ? input.dataset.requiredMessage : 'This field is required';
input.classList.add('error');
}

In the second if statement we check if the input type is checkbox and if the checkbox is not checked. If this is true we set the error variable to data-error-message attribute value or we use default error message.

// Check if input is checkbox and it is not checked
if(input.type === 'checkbox' && !input.checked) {
error = input.dataset.errorMessage ? input.dataset.errorMessage : 'This field is required';
}

In the next if statement we check if the input type is radio. If it is we get all the radio buttons in this group, set the isChecked variable to false and errorMsg to empty string.

Then we loop through radio buttons and we set isChecked to true if radio is checked and we set the errorMsg to error message(only if radio has data-error-message attribute). After the forEach we check if isChecked is false, so if none of the radio buttons is checked, then we set the error to errorMsg or default message.

// Check if input is radio
if(input.type === 'radio') {
// Get all radio inputs in a group
const radios = input.closest('[data-formgroup]').querySelectorAll('input[type="radio"]');
let isChecked = false;
let errorMsg = '';
// Loop through radios and check if any radio is checked and if it is checked set isChecked to true
radios.forEach(radio => {
if(radio.checked) {
isChecked = true;
}
if(radio.dataset.errorMessage) {
errorMsg = input.dataset.errorMessage;
}
});
if(!isChecked) {
error = errorMsg !== '' ? errorMsg : 'This field is required';
}
}

NOTE: in the following if statements we will always first check if the error is not set, because if it is we don’t want to show multiple error messages.

Next if statement is for data-minlength attribute. Here we check if data-minlength attrbute is added to the input and if the length of the input value is smaller than the value of data-minlength attribute. In this case show error message and add error class to input.

// Check if input has data-minlength attribute and if value length is smaller than this attribute, if so show the error
if(!error && input.dataset.minlength !== undefined && value.length < +input.dataset.minlength) {
error =
input.dataset.minlengthMessage ? input.dataset.minlengthMessage : `Please enter at least ${input.dataset.minlength} characters`;
input.classList.add('error');
}

Next one is for data-maxlength attribute. If this attribute is on the input and if the value length is greater than the value of this attribute then we need to show error message.

// Check if input has data-maxlength attribute and if value length is greater than this attribute, if so show the error
if(!error && input.dataset.maxlength !== undefined && value.length > +input.dataset.maxlength) {
error =
input.dataset.maxlengthMessage ? input.dataset.maxlengthMessage : `Only ${input.dataset.maxlength} characters allowed`;
input.classList.add('error');
}

Next one is data-email. This one checks if the email is valid and if not error message is shown. I am using validateEmail function here to check it is valid.

// Check if input has data-email attribute and if email is not valid
if(!error && input.dataset.email !== undefined && !validateEmail(value)) {
error =
input.dataset.emailMessage ? input.dataset.emailMessage : 'Invalid email address';
input.classList.add('error');
}

Now let’s validate passwords. First we check if input has data-match attribute and if the input value is not equal to the value of the input with name attribute which has same value as the data-match attribute. In this case we know that they are not equal and we can show the error message.

// Check if input has data-match attribute and if value is not equal to the value of the element with name attribute equal to this data-match attribute
if(!error && input.dataset.match !== undefined && value !== input.closest('[data-form]').querySelector(`[name="${input.dataset.match}"]`).value) {
error =
input.dataset.matchMessage ? input.dataset.matchMessage : 'Fields are not the same';
input.classList.add('error');
}

Above if statement is for confirm password field. And on original password field we must add data-match-with attribute. Now we can check if the data-match-with attribute is set, and if so we first get the input with name attribute value equal to data-match-with attribute value and we also get the error div of this input. Now we can check if values of this inputs are equal, if they are we can remove error message and error class otherwise we add error message and error class to the inputToMatch.

// Check if input has data-match-with attribute
if(input.dataset.matchWith !== undefined) {
// Get the input that has a name attribute equal to value of data-match-with attribute
const inputToMatch = input.closest('[data-form]').querySelector(`[name="${input.dataset.matchWith}"]`);
// Get the error element of that input
const inputToMatchError = inputToMatch.closest('[data-formgroup]').querySelector('[data-formerror]');
// If values are equal remove error class from input and hide error element
if(value === inputToMatch.value) {
inputToMatch.classList.remove('error');
inputToMatchError.style.display = 'none';
}else { // Add error class to input and show error element
inputToMatch.classList.add('error');
inputToMatchError.style.display = 'block';
inputToMatchError.innerText = inputToMatch.dataset.matchMessage || 'Fields are not the same';
}
}

In the next if statement we check if the input type is file, and if it has data-maxfilesize attribute and if the size of the selected file is greater than the value of data-maxfilesize attribute, in this case we show the error.

// Check if input is file input and if has data-maxfilesize attribute and if file size is greater than the value of this data-maxfilesize attribute 
if(!error && input.type === 'file' && input.dataset.maxfilesize !== undefined && input.files[0].size > +input.dataset.maxfilesize * 1024) {
error =
input.dataset.maxfilesizeMessage ? input.dataset.maxfilesizeMessage : 'File is too large';
input.classList.add('error');
}

Next if statement is also for the file input. We need to check if the input type is file, if input has data-allowed-types attribute and if the value of the data-allowed-types attribute does not include a value of the input type. I am splitting the input type value because this by default returns something like “image/png” and I am only interested in “png”. If all of this is true we can show error message.

// Check if input is file input and if it has data-allowed-types attribute and if file type is not equal to one of the values in data-allowed-type attribute
if(!error && input.type === 'file' && input.dataset.allowedTypes !== undefined && !input.dataset.allowedTypes.includes(input.files[0].type.split('/')[1])) {
error =
input.dataset.allowedTypesMessage ? input.dataset.allowedTypesMessage : 'Invalid file type';
input.classList.add('error');
}

Last thing to do is to check if error is set, if it is not we remove error class and hide error message otherwise we set the content of the error div to error message and then show the error. We don’t need to add error class here because we are doing this in other is statements. And at the end of the validateInput function we return the error(either null or error message) to use this value in submitForm function.

And that’s all we need to do. As you can see this is actually not hard. And you can easily add new validators inside(new if statements).

--

--