JavaScript In Practice - Let's Write Own Form Validator

When developing the front side of a web application, we usually don’t use the full power of Javascript that we know exists. This is because nowadays we have Angular, React, etc., which encapsulate “low level” elements of JS, and with minimal code allow us to create beautiful functionalities. For that reason, most developers limit only a few topics of JS, and know some topics only in theory.

This article’s main purpose is to let you get a feel for the “turbo” mode of JS, and use some pure JS topics in practice.

Use this link to download the source code.

We will build PJFormValidator (Pure Javascript Form Validator ) which is going to be a validation module for our application. Below you can find some functionalities of PJFormValidator:

  1. Validator any HTML in a clear manner
  2. Multiple language support (for now, it supports three languages, but you can easily extend it)
  3. Effortless rule adding

Our validator will mostly use the below topics of JS:

  1. Object construction
  2. Object configuration
  3. Callbacks
  4. OOP encapsulation
  5. Clean code practices

Before starting to write, let's prepare an HTML form. For validating form inputs, we will use “class” es.

For example, if any of the inputs has an “email” class, our validator will check if this inputs is email or not. PFFormValidator by default supports the below rules:

  1. ‘required’ -> Checks if it is empty or not. If empty then throw an empty exception
  2. ‘min-len’ -> Checks the minimum length of the input
  3. ‘max-len’ -> Checks the maximum length of the input
  4. ‘num-min’ -> the minimum value of input should be ‘x’
  5. ‘num-max’ -> The maximum value of input should be ‘x’
  6. ‘number’ -> Should be a number, otherwise an error
  7. ‘email’ -> Should be email, otherwise an error

The above classes are extendable. So, for example, min-len-6 mins your input length should be a minimum of six symbols.

There should be a data-validation-error attribute in every input. This attribute will provide the possibility to “select” where to render error messages.

This is what our HTML will look like:

<form id="validatable-form" class="form-center">
        <div class="form-group">
            <label>Please enter your name</label>
            <input type="text" class="form-control required min-len-2 max-len-16 mx-m-4" data-validation-error="sp-1">
            <div id="sp-1"></div>
        </div>
        <div class="form-group">
            <label>Please enter your surname</label>
            <input type="text" class="form-control required min-len-2 max-len-16" data-validation-error="sp-2">
            <div id="sp-2"></div>
        </div>
        <div class="form-group">
            <label>Please enter your age</label>
            <input type="text" class="form-control number required num-min-8 num-max-19" data-validation-error="sp-3">
            <div id="sp-3"></div>
        </div>
        <div class="form-group">
            <label>Please enter your email</label>
            <input type="text" class="form-control required email" data-validation-error="sp-4">
            <div id="sp-4"></div>
        </div>
        <button id="btn_validate" class="btn btn-success">Validate</button>
    </form>

For validating our form, we will use the validator object. It is a special method with the name validatorForm(), which will validate the form using its id.

By default, the validator will render errors in English. If you want to switch to another supported language, you will need to call the configLanguage() method with the language name parameter.

 //wait full page to be loaded
 document.addEventListener('DOMContentLoaded', function() {
     // when clicking to btn_validate ..
     document.getElementById('btn_validate').addEventListener('click', function(e) {
         //avoid default behaviour
         e.preventDefault();
         //change validation language to azerbaijani
         validator.configLanguage('az');
         //validate form with id = validatable-form.
         validator.validateForm('validatable-form');
     });
 })

Validator depends on configuration (validationConfig object).The purpose of validationConfig is to provide language support. Using addValidationConfig(), defined in this object, you can easily add a new rule for validation.

let validationConfig = {
    isLangExists: function(lang) {
        let isExists = false;
        for (let _lang in this.langs) {
            if (_lang == lang) {
                isExists = true;
                break;
            }
        }
        return isExists;
    },
    addValidationConfiguration: function(lang, configClass, errorMessage) {
        if (!this.isLangExists(lang))
            throw new Error('given language is not exists');
        this.langs[lang][configClass] = errorMessage;
    },
    getRuleValueByLang: function(lang, rulename) {
        return this.langs[lang][rulename];
    },
    langs: {
        az: {
            'required': 'Xana boş ola bilməz!!',
            'min-len-': 'Sözün uzunluğu minimum `x` olmalıdır',
            'max-len-': 'Sözün uzunluğu maksimum `x` olmalıdır',
            'num-min-': 'Ədədin minimum dəyəri `x` olmalıdır',
            'num-max-': 'Ədədin maksimal dəyəri `x` olmalıdır',
            'number': 'Ədəd olmalıdır',
            'email': "Email duzgun deyil"
        },
        ru: {
            'required': 'Поля должен быть заполнено',
            'min-len-': 'Минимальный длина слова должен быть `x`',
            'max-len-': 'Максимальная длина слова должен быть `x`',
            'num-min-': 'Минимальная значения число должен быть `x`',
            'num-max-': 'Максимальная значения число должен быть `x`',
            'number': 'Должен быть число',
            'email': "неправильно указан email"
        },
        en: {
            'required': 'Input cant be empty',
            'min-len-': 'Minimum length of the input must be `x`',
            'max-len-': 'Maximum length of the input must be `x`',
            'num-min-': 'Minimum size of number must be `x`',
            'num-max-': 'Maximum size of number must be `x`',
            'number': 'Must be only number!!',
            'email': "Email is not valid"
        }
    }
};

Say you want to add a new class for defining different validation mechanisms. In that case, you can add any class name (for example mx-m-5 ) and inject it into addValidationConfig:

validator.addValidationConfiguration('az', 'mx-m-', 'Hər hansı bir error mesajini buradan verəcəm');

When adding a new rule, there is no need to add numbers at the end of the classes. For example: for min-len-7, just add min-len.

validator.addValidationRule('mx-m-', function(originalValue, errorOn, errorMessage, ruleValue) {
    if (true) {
        this._sendErrorToElement('mx-m-', errorOn, errorMessage);
    }
});

Here are detailed steps for the validation algorithm:

  1. Send the id of the form to validate and retrieve the object.
  2. Read all inputs in this form.
  3. Clear all innerHTML-s of elements where you’re planning to render error messages.
  4. Read all classes of input.
  5. Check if there is a rule per class.
  6. If there is a rule, read a number from the class name ) min-len-6, read “6”.
  7. Execute the rule.

Here is the code for above steps:

this.validateForm = function(formId) {
    let formForValidate = document.getElementById(formId);
    if (formForValidate == null) {
        throw new Error('given form doesnt exist');
    } else {
        let inputsForValidate = this._getInputsForValidate(formForValidate);
        for (let input of inputsForValidate) {
            let errorOccuredOn = this._getValidationErrorElement(input);
            this._resetHtml(errorOccuredOn);
            let inputClAttributes = this._getAllClassAttributes(input);
            for (let inputClAttr of inputClAttributes) {
                let ruleName = this._getRule(inputClAttr);
                if (ruleName != null) {
                    let ruleValue = this._getRuleValue(inputClAttr, ruleName);
                    let formalizeErrorMessage = this._makeFormattedErrorMessage(inputClAttr, this._validationConfig.getRuleValueByLang(this._lang, ruleName));
                    this._rules.executeRule(ruleName, input.value, errorOccuredOn, formalizeErrorMessage, ruleValue);
                }
            }
        }
    }
}

We have ruleTypes as an object which stores all rules.

this._rules = {
    executeRule: function(ruleName, value, errorOn, formalizedErrorMessage, ruleValue) {
        this.ruleTypes[ruleName](value, errorOn, formalizedErrorMessage, ruleValue);
    },
    ruleTypes: {
        _createErrorContainer: function(key, errorMessage) {
            let errorSpan = document.createElement('span');
            errorSpan.className = `error-${key}`;
            errorSpan.innerText = errorMessage;
            return errorSpan;
        },
        _isNotaNumber: function(val) {
            return isNaN(Number(val));
        },
        _isEmpty: function(val) {
            return (!val || 0 === val.length);
        },
        _isValidEmail: function(email) {
            let pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            return pattern.test(email);
        },
        _IsTrue: function(callback) {
            if (callback()) return true;
            else return false;
        },
        _sendErrorToElement: function(className, errorOn, errorMessage) {
            let errorSpan = this._createErrorContainer(className, errorMessage);
            document.getElementById(errorOn).appendChild(errorSpan);
        },
        'number': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isNotaNumber(originalValue)) {
                this._sendErrorToElement('number', errorOn, errorMessage);
            }
        },
        'required': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue)) {
                this._sendErrorToElement('required', errorOn, errorMessage);
            }
        },
        'email': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue) || !this._isValidEmail(originalValue)) {
                this._sendErrorToElement('email', errorOn, errorMessage);
            }
        },
        'min-len-': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue) || this._IsTrue(() => originalValue.length < parseInt(ruleValue))) {
                this._sendErrorToElement('min-len', errorOn, errorMessage);
            }
        },
        'max-len-': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue) || this._IsTrue(() => originalValue.length > parseInt(ruleValue))) {
                this._sendErrorToElement('max-len', errorOn, errorMessage);
            }
        },
        'num-min-': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue) || this._IsTrue(() => parseInt(originalValue) < parseInt(ruleValue))) {
                this._sendErrorToElement('num-min', errorOn, errorMessage);
            }
        },
        'num-max-': function(originalValue, errorOn, errorMessage, ruleValue) {
            if (this._isEmpty(originalValue) || this._IsTrue(() => parseInt(originalValue) > parseInt(ruleValue))) {
                this._sendErrorToElement('num-max', errorOn, errorMessage);
            }
        }
    }
}

There are some functionalities that is not part of the rules. For security reasons, we used Object.defineProperties to avoid enumerating them.

Object.defineProperties(this._rules.ruleTypes, {
    "_createErrorContainer": {
        enumerable: false,
        configurable: false
    },
    "_isNotaNumber": {
        enumerable: false,
        configurable: false
    },
    "_isEmpty": {
        enumerable: false,
        configurable: false
    },
    "_isValidEmail": {
        enumerable: false,
        configurable: false
    },
    "_IsTrue": {
        enumerable: false,
        configurable: false
    },
    "_sendErrorToElement": {
        enumerable: false,
        configurable: false
    }
});

After validating, you will see the below validation messages in case of errors:

Use this link to download the source code.