Vanilla JavaScript Form Validation example

Bunny Validation with all sub-components is only 5.6kb gzipped and it includes rich functionality support already like:

Without these components Validation Core itself is only 2kb, which includes:

Bunny Validation is an HTML5 native form validation library. It uses native attributes like required, type="email" and custom attributes like data-ajax="/check-email/{value}" or maxfilesize="1".

Old src/bunny.validate is deprecated and will be removed in 1.0. Use newest version instead: src/Validation.

Live demo

Try test@test.com for ajax validation
  • Try uploading non-images even if they have .png/.jpg extension
  • Images < 500x500px will be invalid
  • And try uploading images larger then 1MB

1. Including scripts

Because of modern web app development trends Bunny Validation for rich use-case support and UX uses by default:

It is recommended to compile all the app scripts used on the page or a module into one file via rollup.js or anything else. All what you need to do is just to do one import:

1
import { Validation } from 'bunnyjs/src/Validation';

Or dists are available. Download them and put before </body>.

1
2
3
4
5
<script src="https://unpkg.com/bunnyjs@0.14.19/dist/ajax.min.js"></script>
<script src="https://unpkg.com/bunnyjs@0.14.19/dist/file.min.js"></script>
<script src="https://unpkg.com/bunnyjs@0.14.19/dist/image.min.js"></script>
<script src="https://unpkg.com/bunnyjs@0.14.19/dist/element.min.js"></script>
<script src="https://unpkg.com/bunnyjs@0.14.19/dist/validation.min.js"></script>

Finally, just add 1 line of code:

1
2
3
4
5
// init form validation
Validation.init(document.form[0]);

// or set second argument to true to add also inline validation - inputs will be instantly validated on 'change' event
Validation.init(document.form[0], true);

2. HTML requirements and configuration

Configuration stored in ValidationConfig object and injected into ValidationUI.config. ValidationUI is an object which creates error messages and searches for DOM elements related to input, like input container (input group, form group) or label. ValidationUI injected into Validation.ui.

Configuration properties (by default Bootstrap 4 classes used):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const ValidationConfig = {

// div/node class name selector which contains one label, one input, one help text etc.
classInputGroup: 'form-group',
// class to be applied on input group node if it has invalid input
classInputGroupError: 'has-danger',

// label to pick textContent from to insert field name into error message
classLabel: 'form-control-label',

// error message tag name
tagNameError: 'small',
// error message class
classError: 'text-help',

// query selector to search inputs within input groups to validate
selectorInput: '[name]'

};

The most important things to note here:

To change form group class, for example, just append a new value to ValidationConfig.classInputGroup = 'app-input-container';

To change the behaviour of how to create, where to insert error messages and other DOM operations extend functions in ValidationUI. For example, to change place where to insert error message this method can be changed:

1
2
3
4
5
6
7
8
9
/**
* DOM algorithm - where to insert error node/message
*
* @param {HTMLElement} inputGroup
* @param {HTMLElement} errorNode
*/

insertErrorNode(inputGroup, errorNode) {
inputGroup.appendChild(errorNode);
},

Browse source code on GitHub to view all the methods.

3. Translations and AJAX error message

Each validator has own validation error message and all lang string are stored in ValidationLang object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Bunny Form Validation default Translations (EN)
*
* object key = validator method name
* may use additional parameters in rejected (invalid) Promise
* each invalid input will receive {label} parameter anyway
* ajax error message should be received from server via JSON response in "message" key
*/

export const ValidationLang = {

required: "'{label}' ir required!",
email: "'{label}' should be a valid e-mail address!",
tel: "'{label}' is not a valid telephone number!",
maxLength: "'{label}' length must be < '{maxLength}'",
minLength: "'{label}' length must be > '{minLength}'",
maxFileSize: "Max file size must be < {maxFileSize}MB, uploaded {fileSize}MB",
image: "'{label}' should be an image (JPG or PNG)",
minImageDimensions: "'{label}' must be > {minWidth}x{minHeight}, uploaded {width}x{height}",
maxImageDimensions: "'{label}' must be < {maxWidth}x{maxHeight}, uploaded {width}x{height}",
requiredFromList: "Select '{label}' from list",
confirmation: "'{label}' is not equal to '{originalLabel}'",
minOptions: "Please select at least {minOptionsCount} options"

};

ValidatorLang is injected into Validation.lang property.

The easiest way to change lang string on some page/controller/route is to assign a new value to Validation.lang.required = 'new value'.

For complete translations new lang object can be created, imported and injected via Validation.lang = CustomValidationLang.

For AJAX validations - server should return a response in JSON format. If response contains message root key and it is NOT empty then input will be invalid and message from response will be used. To translate AJAX validation server should handle it itself.

4. Validator list

Validator key How to use Additional params for error message Comment
required add “required” attribute Works also with files, selects and checkboxes
email type=”email”
tel type=”tel” Input must be a telephone number
maxLength maxlength=”N” {maxLength} N - number of characters
minLength minlength=”N” {minLength} N - number of characters
maxFileSize maxfilesize=”N” {maxFileSize}, {fileSize} N - number in MBs, can be float
image accept=”image/jpeg, image/png” {signature} If accept has “image” somewhere, file input is image
minImageDimensions mindimensionx=”XxY” {width}, {height}, {minWidth}, {minHeight} X - width, Y - height
maxImageDimensions maxdimensions=”XxY” {width}, {height}, {maxWidth}, {maxHeight} X - width, Y - height
requiredFromList requiredfromlist=”ID” OR there is input with ID = name + “_id” ID - hidden input ID, this validator is useful for Google Place and other autocompletes
minOptions minoptions=”N” on custom autocompletes, tag selectors, etc. N - number of min options to select from autocomplete, tag selector, etc. It looks for non-empty hidden inputs within same input container (input group)
confirmation ID = original input.name + “_confirmation”
ajax data-ajax=”URL” URL - ajax URL where {value} will be replaced with input.value, server should return JSON. If there is non-empty “message” key - input is invalid

5. Custom validators

Each validator is a simple function which:

All validators are stored in ValidatorValidators object and injected into Validator.validators.

Each ValidatorValidators object property name = validator name = ValidatorLang property name.

The order of properties defined in ValidatorValidators is important. For example, first one required is defined. Each input is checked against each validator in defined order. When validator was rejected, input is marked as invalid and further validators on this input are not executed.

Example with custom validator which uses BunnyDate to check for valid date:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Validation.lang.date = 'Invalid date';

Validation.validators.date = input => {
return new Promise((valid, invalid) => {
if (input.id === 'day') {
const date = BunnyDate.create(
document.getElementById('year').value,
document.getElementById('month').value,
input.value
);
if (date) {
document.getElementById('birth_date').value = BunnyDate.toSqlDate(date);
valid();
} else {
invalid();
}
} else {
valid();
}
});
};

6. Manual section validation and input check

In modern web apps and complicated forms with sections, tabs and steps there is a need of validating not the whole form but only part of it.

Validation.validateSection(node) does this job. It returns Promise with resolve only which accepts only one argument - true if all inputs within this section are valid or array of invalid inputs otherwise.

When form validation is in progress - validateSection() should not be called during this time or Error will be thrown.

Because of that and from UX point of view all the buttons which should validate section - MUST be disabled and maybe some spinner added. Example below is taken from init() src:

1
2
3
4
5
6
7
8
9
10
11
Validation.validateSection(node).then(result => {
[].forEach.call(submitBtns, submitBtn => {
submitBtn.disabled = false;
});
if (result === true) {
form.submit();
} else {
// section invalid, result is array of invalid inputs
Validation.focusInput(result[0]);
}
})

Validation.focusInput() can be used to focus invalid input, to scroll to it and also put cursor to the end if it is text input.