LcFormValidation

A framework agnostic, async form validation library

GitHub v2.0.0

LcFormValidation is a small JavaScript library that provides you form validation without any framework nor library attachment so you can easily integrate it in your stack. Validate your viewmodel either on client or server side with Node.js.

LcFormValidation is licensed under MIT software license being an open-source component of Lemoncode.

  • Heavily JavaScript based (no HTML attributes).
  • Support for sync / async validations.
  • Support for field and global form validations.
  • Customizable multi trigger / event validation.
  • Create extensible validators.
  • Lightweight library, just 29.1KB minified (8KB gzipped).
  • Client / server side validation.
  • It uses native Promises to handle validations (polyfill is included).

Download (link this)

Using npm

npm install --save lc-form-validation

Installation (link this)

Using Required.js/AMD

require(['lc-form-validation'], function(lcFormValidation) {
  var createFormValidation = lcFormValidation.createFormValidation;
  // ...
}

Using ES6 modules

const { createFormValidation } = require('lc-form-validation');

// harmony syntax:
import { createFormValidation } from 'lc-form-validation';

Using script tag

<script>
  var createFormValidation = window['lc-form-validation'].createFormValidation;
</script>

Examples (link this)

React examples (link this)

Simple form (ES6, TypeScript)

A simple form with fullname and password fields. Applied validations:

  • both fullname and password fields are mandatory (required validator + custom validator).

Signup form (ES6, TypeScript)

A sign up form with username, password and confirm password fields with the next validation constraints:

  • username is mandatory and has to not exist on GitHub (required validator + custom validator).
  • password is mandatory and has to be minimum 4 characters length (minLength validator).
  • confirm password is mandatory and has to be the same value as password field (custom validator).

Quiz form (ES6, TypeScript)

A simple quiz with three options where there has to be at least one checked option to pass the validation (custom global validation).

Vue.js examples (link this)

Custom Validator (Typescript)

A form about recipes and ingredients with the next validation constraints:

  • Name field is mandatory (required validator).
  • Ingredients field has custom array validator (Should have at least one ingredient).

jQuery examples (link this)

Shopping form (ES6)

A little shopping form where the user has to select a product with a version and optionally apply a discount code and enter its NIF. Validations applied:

  • The brand and product fields are mandatory (required validator).
  • The discount code is optional but needs to have a valid format if fulfilled (pattern validator).
  • NIF field is mandatory with a valid format (required validator + custom validator).

Get started (link this)

Overview (link this)

Let's say we want to add validation support for a login form with next constraints:

  • login is mandatory and has to be a valid email address.
  • password is mandatory.
Login form

To implement the form validation you just only need an object literal to declare the proper field validation constraints for the two fields:

// validation constraints definition
const validationConstraints = {
  fields: {
    login: [
      { validator: Validators.required },
      { validator: Validators.email }
    ],
    password: [
      { validator: Validators.required }
    ]
  }
};

Then create an instance of a FormValidation:

// create a form validation instance
const formValidation = createFormValidation(validationConstraints);

Finally apply validation when your data needs to be validate (e.g. a submit event handler):

// e.g. viewModel value: { login: 'johndoe@example.com', password: 'jdoe123' }
function onSave(viewModel) {
  formValidation
    .validateForm(viewModel)
    .then(validationResult => {
      if(validationResult.succeeded) {
        // we can now send it to server to validate credentials
      } else {
        // display validation errors on UI
        displayFormErrors(validationResult);
      }
    })
    .catch(error => {
      // Oops, an error happened...
      handleError(error.message);
    });
}

Validation constraint (link this)

Validation constraint is just an object literal passed with two main properties:

fields

An object containing all fields keys that need validations. Each field will have an array of FieldValidationConstraint objects, each one with three properties:

  • validator A function that will validate the field and return a FieldValidationResult or a promise wrapping it if the validator is async
  • eventsFilter (optional) An object containing one o more event names, that will be used as a filter when the validator needs to be triggered on a specific event, and a boolean value (true to use the filter). The default value is: { onChange: true }.
  • customParams (optional) An object with some values needed by the validator (e.g. a date format).
fields: {
  <fieldName>: [
    {
      validator: <validatorFunction>, // function(value, viewModel, customParams)
      eventsFilter: {
        <eventName>: <boolean>,
        ...
      },
      customParams: { ... }
    }
  ]
}

global

The global property will hold an array of validation functions that will validate the entire viewModel:

global: [
  <globalValidatorFunction>, // function(viewModel)
  ...
]

Validating form (link this)

In order to validate a single field or the entire form you have to use the methods validateField and validateForm of the instance returned by createFormValidation described in the API section.

Validators (link this)

LcFormValidation includes a set of basic validation rules to use in common scenarios. You can also write your own validator functions if you need some specific domain validators.

Note: All validators come with a default message. If you need to override it you cand do it by hand when you catch the FieldValidationResult. For i18n support, please see this section.

Validators.required (link this)

The required validator will check the value is not empty. This validator is useful when you need a fulfilled text field or a radio / checkbox button marked. These values are considered empty:

  • null
  • undefined
  • false
  • Empty string ""
  • Whitespace only string " " (it can be disabled with customParams.trim set to false).
import { createFormValidation, Validators } from 'lc-form-validation';
const validationConstraints = {
  fields: {
    firstName: [
      { validator: Validators.required }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

// Example of a successful validation
formValidation
  .validateField(null, 'firstName', 'John')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.type); // 'REQUIRED'
    console.log(validationResult.key); // 'firstName'
    console.log(validationResult.errorMessage); // ''
  });

// Example of a unsuccessful validation
formValidation
  .validateField(null, 'firstName', '')
  .then(validationResult => {
    console.log(validationResult.succeeded); // false
    console.log(validationResult.type); // 'REQUIRED'
    console.log(validationResult.key); // 'firstName'
    console.log(validationResult.errorMessage);
    // 'Please fill in this mandatory field.'
  });

Validators.minLength (link this)

The minLength validator will check if the length of the string passed as value is equals or greater than the length supplied in customParams.length.

Note: empty values such as "", null or undefined will be marked as valid values. If you need to check for non-empty values you may use this validator along with required validator.

Important! You must always providecustomParams.length and it must be a number. In case it is not provided an error will be thrown in the catch block of the returned promise.

import { createFormValidation, Validators } from 'lc-form-validation';

const validationConstraints = {
  fields: {
    username: [
      {
        validator: Validators.minLength,
        customParams: { length : 4 }
      }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

// Example of a successful validation
formValidation
  .validateField(null, 'username', 'jasmine')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.type); // 'MIN_LENGTH'
    console.log(validationResult.key); // 'username'
    console.log(validationResult.errorMessage); // ''
  });

// Example of a unsuccessful validation
formValidation
  .validateField(null, 'username', 'jax')
  .then(validationResult => {
    console.log(validationResult.succeeded); // false
    console.log(validationResult.type); // 'MIN_LENGTH'
    console.log(validationResult.key); // 'username'
    console.log(validationResult.errorMessage);
    // 'The value provided must have at least 4 characters.'
  });

Validators.maxLength (link this)

The maxLength validator will check if the length of the string passed as value is lesser or equals than the length supplied in customParams.length.

Note: empty values such as "", null or undefined will be marked as valid values. If you need to check for non-empty values you may use this validator along with required validator.

Important! You must always providecustomParams.length and it must be a number. In case it is not provided an error will be thrown in the catch block of the returned promise.

import { createFormValidation, Validators } from 'lc-form-validation';

const validationConstraints = {
  fields: {
    password: [
      {
        validator: Validators.maxLength,
        customParams: { length : 8 }
      }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

// Example of a successful validation
formValidation
  .validateField(null, 'password', 'test1234')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.type); // 'MAX_LENGTH'
    console.log(validationResult.key); // 'password'
    console.log(validationResult.errorMessage); // ''
  });

// Example of a unsuccessful validation
formValidation
  .validateField(null, 'password', 'loooooongp@s$w0rd')
  .then(validationResult => {
    console.log(validationResult.succeeded); // false
    console.log(validationResult.type); // 'MAX_LENGTH'
    console.log(validationResult.key); // 'password'
    console.log(validationResult.errorMessage);
    // 'The value provided is too long. Length must not exceed 8 characters.'
  });

Validators.pattern (link this)

The pattern validator will check if the value matches a regular expresion given in customParams.pattern option. This regular expression can be either a RegExp instance or a string. It's important to mention that flags in string patterns are not supported.

If you need flags in string patterns you can use the RegExp constructor.

Important! You must always providecustomParams.pattern. In case it is not provided an error will be thrown in the catch block of the returned promise.

import { createFormValidation, Validators } from 'lc-form-validation';

const CODE_PATTERN = /^LC\d{3}$/; // or "^LC\d{3}$"
const validationConstraints = {
  fields: {
    code: [
      {
        validator: Validators.pattern,
        customParams: { pattern: CODE_PATTERN }
      }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

// Example of a successful validation
formValidation
  .validateField(null, 'code', 'LC001')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.type); // 'PATTERN'
    console.log(validationResult.errorMessage); // ''
  });

// Example of a unsuccessful validation
formValidation
  .validateField(null, 'code', '003a')
  .then(validationResult => {
    console.log(validationResult.succeeded); // false
    console.log(validationResult.type); // 'PATTERN'
    console.log(validationResult.key); // 'code'
    console.log(validationResult.errorMessage);
    // 'Please provide a valid format.'
  });

Validators.email (link this)

The email validator will check if the given string value is a valid email address. LcFormValidation's email validator uses a RegExp provided by emailregex.com that follows RFC 5322 Official Standard since email address validation is hard to implement and there is not an unique source of truth. If this email validator doesn't meet your requirements you can write your custom validator.

Note: empty values such as "", null or undefined will be marked as valid values. If you need to check for non-empty values you may use this validator along with required validator.

import { createFormValidation, Validators } from 'lc-form-validation';

const viewModel = null;
const validationConstraints = {
  fields: {
    email: [
      { validator: Validators.email }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

// Example of a successful validation
formValidation
  .validateField(viewModel, 'email', 'johndoe@example.com')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.type); // 'EMAIL'
    console.log(validationResult.key); // 'email'
    console.log(validationResult.errorMessage); // ''
  });

// Example of a unsuccessful validation
formValidation
  .validateField(null, 'email', 'invalidValue')
  .then(validationResult => {
    console.log(validationResult.succeeded); // false
    console.log(validationResult.type); // 'EMAIL'
    console.log(validationResult.key); // 'email'
    console.log(validationResult.errorMessage);
    // 'Please enter a valid email address.'
  });

Implementing a custom validator (link this)

Writing a FieldValidationFunction

To create a field validator you only need to provide a function that accepts first the value of the field and optionally the entire view model and a plain object with extra parameters. These parameters are injected when the validator is called. Your field validator must always return a FieldValidationResult (or a wrapper promise for async validations) that returns a FieldValidationResult. The signature for a synchronous field validation function is:

function myValidator(value: any, vm: any, customParams: any): FieldValidationResult

Example of a synchronous field validator

This example shows a validator that needs to check if a given date is before today using Moment.js:

function isDatePriorToCurrentDate(value, vm, customParams) {
  const yesterday = moment().substract(1, 'days').startOf('day');
  const selectedDate = moment(value);
  const isValid = selectedDate.isSameOrBefore(yesterday);
  const errorMessage = 'The selected day must be before today.';
  const validationResult = new FieldValidationResult();

  validationResult.succeeded = isValid;
  validationResult.type = 'NOT_PRIOR_CURRENT_DATE';
  validationResult.errorMessage = isValid ? '' : errorMessage;
  return validationResult;
}

Asynchronous field validator

To write an asynchronous field validation function you only needs a validation function that returns a promise with the FieldValidationResult. The signature for an asynchronous field validator is:

function myValidator(value: any, vm: any, customParams: any):Promise<FieldValidationResult>

Example of an asynchronous field validator

This example validates if the given user does not exist in GitHub:

function userExistsOnGitHubValidator(value, vm, customParams) {
  const errorMessage = `The username "${value}" already exists`;
  const validationResult = new FieldValidationResult();
  validationResult.type = 'GITHUB_USER_EXISTS';

  return new Promise(resolve => {
    fetch(`https://api.github.com/users/${value}`)
      .then(result => {
        // Status 200, meaning user exists, so the given user is not valid
        validationResult.isValid = false;
        validationResult.errorMessage = errorMessage;
        resolve(validationResult);
      })
      .catch(error => {
        if(error.status === 404) {
          // User does not exists, so the given user is valid
          validationResult.isValid = true;
          validationResult.errorMessage = '';
          resolve(validationResult);
        } else {
          // Unexpected error
          reject(error);
        }
      });
  });
}

Note: This is a basic example, consider using helpers to modularize your code and follow the Single Responsability Principle.

i18n support (link this)

If you need internationalization support you will need to implement a custom service that parse the FieldValidationResult `type` (and optionally `key`) and return the proper message. See this little example:

Create a i18n service (or use yours):

// ./i18n/es_ES.js
export default {
  'CUSTOMERFORM.PATTERN.phone': 'Please, enter a valid phone number.'
};

// ./i18n/es_ES.js
export default {
  'CUSTOMERFORM.PATTERN.phone': 'Por favor, introduzca un número de teléfono válido.'
};

// ./i18n/kr_KO.js
export default {
  'CUSTOMERFORM.PATTERN.phone': '유효한 전화 번호를 입력합니다.'
};

// ./i18n/i18n.js
import en_US from './i18n/en_US';
import es_ES from './i18n/es_ES';
import kr_KO from './i18n/kr_KO.js';

const SUPPORTED_LOCALES = {
  en_US,
  es_ES,
  kr_KO,
};

// Accept a locale and return the translation function
export function i18n(locale) {
  if (!SUPPORTED_LOCALES[locale]) {
    throw new Error(`Locale ${locale} is not supported`);
  }

  // Accept a dictionary key and return the message or null;
  return function (key) {
    return SUPPORTED_LOCALES[locale][key] || null;
  }
}

Note: This is a very basic example. You may have your dictionaries in JSON files and request them when the user changes the language.

Create a form validation instance that uses the service

import { i18n } from './i18n/i18n';
import { createFormValidation, Validators } from 'lc-form-validation';

// get language and invoke service
const locale = localStorage.getItem('app.i18n.lang'); // "es_ES"
const translate = i18n(locale); //

// Define validation constraints
const customerFormValidationConstraints = {
  fields: {
    phone: [
      {
        validator: Validators.pattern,
        customParams: { pattern: /^\d{3}((((?:)\s?)\d{2}){3}|(((?:)\s?)\d{3}){2})$/ }
      },
    ],
  }
};

// create a FormValidation instance
const customerFormValidation = createFormValidation(customerFormValidationConstraints);

// Validate field
customerFormValidation
  .validateField(null, 'phone', '698 52 01 47')
  .then((validationResult) => {
    if (!validationResult.succeded) {
      // set translation
      const { key, type } = validationResult;
      const i18nKey = getI18nKey(type, key);
      validationResult.errorMessage = translate(i18nKey);
      // "Por favor, introduzca un número de teléfono válido."

      // Handle unsuccessful validation
      handleUnsuccessfulValidation(validationResult);
    }
  })
  .catch((error) => {
    // handle unexpected error
  });

// Get dictionary key for CUSTOMERFORM namespace
function getI18nKey(type, field) {
  return `CUSTOMERFORM.${type}.${field}`;
}

function handleUnsuccessfulValidation(validationResult) { /* .... */ };

API (link this)

FieldValidationResult (link this)

FieldValidationResult is a simple class with three main properties:

  • succeeded boolean - The result of a validation constraint.
  • type string - An identifier to associate the validation result with a validator, it is useful for checking out what validation did not get passed when multiple validators on a field are applied.
  • errorMessage string - A message describing why the field did not get validated. This field can be declared as empty for a successful validation.

A fourth read-only property is added when it is returned by validateForm or validateField named key that will hold the field name to distinguish which field the validation was made.

Note: You can use a plain object as validation result without extending the FieldValidationResult class.

Example of a FieldValidationResult

{
  succeeded: false,
  errorMessage: 'Please, fill in this mandatory field',
  type: 'REQUIRED',
  key: 'lastName'
}

FormValidationResult (link this)

FormValidationResult is an global validation result returned by validateForm. It has three main properties:

  • succeeded boolean - The result of the entire form validation.
  • fieldErrors { [key: string]: FieldValidationResult }; - An object of all field errors given as a result of validate every field. This property contains every field validation result, so it will be filled even if all validation passed.
  • formGlobalErrors Array<FieldValidationResult> - A list of all global errors given as a result of executing every global validation function. This property can be empty if there are no errors.

Example of a FormValidationResult

{
  succeeded: true,
  fieldErrors: {
    emailAddress: {
      succeeded: true,
      errorMessage: '',
      type: 'EMAIL',
      key: 'emailAddress'
    }
  },
  formGlobalErrors: []
}

validateForm (link this)

formValidation.validateForm(vm: any): Promise<FormValidationResult>;

It validates the entire viewModel passed as argument triggering all field and global validations previously defined in validationConstraints. Field validations are proxied to validateField and global validations are executed.

When all validations are executed they're merged in a FormValidationResult and returned in a promise.


Example:

// Define a validator
function areAllFieldsCheckedValidation(vm) {
  const validationResult = new FieldValidationResult();
  const errorMessage = 'All fields must be checked';
  const isValid = areAllChecked(vm);
  validationResult.succeeded = validationResult;
  validationResult.type = 'ALL_CHECKED';
  validationResult.errorMessage = isValid ? '' : errorMessage;

  return validationResult;
}

function isAllChecked(vm) {
  return Object.keys(vm).every(field => vm[field].checked === true);
}

// Define the form validation constraints
const validationConstraints = {
  global: [areAllFieldsCheckedValidation]
}

// Create an instance of FormValidation
const formValidation = createFormValidation(validationConstraints);
const viewModel = {
  fieldA: { value: 'A', checked: true },
  fieldB: { value: 'B', checked: true },
  fieldC: { value: 'C', checked: true },
};

// Apply validation
formValidation
  .validateForm(viewModel)
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.fieldErrors); // {}
    console.log(validationResult.formGlobalErrors);
    // [{ errorMessage: '', type: 'ALL_CHECKED', key: '_GLOBAL_FORM_', succeeded: true }]
  })
  .catch(error => {
    // handle error
  });

validateField (link this)

validateField(vm: any, key: string, value: any, eventsFilter?: ValidationEventsFilter): Promise<FieldValidationResult>;

It validates a value against all constraints for a field previously defined in validationConstraints. It will trigger every field validation function sequentially and evaluate its FieldValidationResult, if the validation result is not successful it will stop executing the next field validation function, so it will always return the first error.

It accepts four arguments:

  • vm any - The entire viewModel. This can be optionally passed as null if your field validation does not needs to do checks againsts your viewModel.
  • key string - The name of the field that requires validation. This key has to be the same as defined in your ValidationConstraints.
  • value any - The value to validate against the field validation constraints.
  • eventsFilter (optional) Object - An object with the signature { <eventName>: boolean } describing the event (or events) to filter what validation functions are triggered. Only the validations matching the filter will be executed. If you need to trigger a validation function multiple times you can add more event filters. The default value is: { onChange: true }. You can also define your custom event names.

Note: Events defined in eventsFilter object are not directly attached to DOM, their purpose is only to filter validation functions.

Important! Since every field validation function is executed sequentially the order you define the field validation functions matters.

Example - single validation:

// Define a validator
function firstUpperCaseValidator(value) {
  const validationResult = new FieldvalidationResult();
  const errorMessage = 'First name's first letter must be uppercase';
  const isValid = value.charAt(0).toUpperCase() === value.charAt(0);
  validationResult.succeeded = isValid;
  validationResult.errorMessage = isValid ? '' : errorMessage;
  validationResult.type = 'FIRSTNAME';

  return validationResult;
}

// Define the field validation constraints
const validationConstraints = {
  fields: {
    firstName: [
      { validator: Validators.required },
      { validator: firstUpperCaseValidator }
    ]
  }
};

// Create an instance of FormValidation
const formValidation = createFormValidation(validationConstraints);

// Apply validation
formValidation
  .validateField(null, 'firstName', 'John')
  .then(validationResult => {
    console.log(validationResult.succeeded); // true
    console.log(validationResult.errorMessage); // ''
    console.log(validationResult.type); // 'FIRSTNAME'
    console.log(validationResult.key); // 'firstName'
  })
  .catch(error => {
    // handle error
  });

isValidationInProgress (link this)

formValidation.isValidationInProgress(): boolean;

It checks if a form or a field validation is currently in progress by returning true or false.

Example:

const validationConstraints = {
  fields: {
    firstName: [
      { validator: Validators.required }
    ],
    lastName: [
      { validator: Validators.required }
    ],
  }
};

const formValidation = createFormValidation(validationConstraints);
const viewModel = {
  firstName: 'John',
  lastName: 'Doe',
};

formValidation
  .validateform(viewModel)
  .then(validationResult => {
    console.log(formValidation.isValidationInProgress()); // false (validation finished)
  });

console.log(formValidation.isValidationInProgress()); // true (validation started)

Using eventsFilter to filter validations (link this)

By default { onChange: true } is applied when the eventsFilter object is not passed. You can pass the name of a event or a identifier to track when certain validations are triggered..

Important! If you filter a validation using a non registered event name the FieldValidationResult will be undefined.

Let's take a look at the next example. Suppose we need to apply a validation to signup a new user and the username with the next constraints:

  • The username is mandatory.
  • The username can only contains lowercase letters and numbers.
  • The username must not exist in the database.

We can apply the required and format validation when the user types and apply the database verification when the input foucs out. The resulting validation constraints would look like this:

const validationConstraints = {
  fields: {
    username: [
      { validator: Validators.required },
      {
        validator: Validators.pattern,
        customParams: { pattern: /^[a-z\d]+$/ },
        eventsFilter: { onChange: true, onBlur: true },
      },
      {
        // your custom validator that queries the database
        validator: usernameDoesNotExistsValidator,
        eventsFilter: { onBlur: true }
      }
    ]
  }
};

const formValidation = createFormValidation(validationConstraints);

Then attach the proper events to your form input and validate the field. For a jQuery implementation it would look like this:

$('#txtUsername').on({
  keyup: function() {
    formValidation
      .validateField(null, 'username', $(this).val())
      .then(function(validationResult) {
        if (validationResult.succeeded) {
          $('#usernameErrorMessage').hide().val('');
        } else {
          $('#usenameErrorMessage').val(validationResult.errorMessage).show();
        }
      });
  },
  blur: function() {
    formValidation
      .validateField(null, 'username', $(this).val(), { onBlur: true }) // apply event filter
      .then(function(validationResult) {
        if (validationResult.succeeded) {
          $('#usernameErrorMessage').hide().val('');
        } else {
          $('#usenameErrorMessage').val(validationResult.errorMessage).show();
        }
      });
  }
});

Note: This is a basic example, you could move this to a more mantenible helper instead of a huge object.

isFormPristine (link this)

formValidation.isFormPristine(): boolean;

It checks if the form was validated at least once using validateForm or validateField. It returns true or false.

Example:

const validationConstraints = {
  fields: {
    firstName: [
      { validator: Validators.required }
    ],
    lastName: [
      { validator: Validators.required }
    ],
  }
};

const formValidation = createFormValidation(validationConstraints);
const viewModel = {
  firstName: 'John',
  lastName: 'Doe',
};

console.log(formValidation.isFormPristine()); // true
formValidation
  .validateform(viewModel)
  .then(validationResult => {
    console.log(formValidation.isFormPristine()); // false
  });

isFormDirty (link this)

formValidation.isFormDirty(): boolean;

This method is the counterpart of isFormPristine. It will return false when then form was never validated, and true if it was.

const validationConstraints = {
  fields: {
    firstName: [
      { validator: Validators.required }
    ],
    lastName: [
      { validator: Validators.required }
    ],
  }
};

const formValidation = createFormValidation(validationConstraints);
console.log(formValidation.isFormDirty()); // false
formValidation
  .validateField(null, 'firstName', 'John')
  .then(validationResult => {
    console.log(formValidation.isFormDirty()); // true
  });

Change Log (link this)

This library follows Semantic Versioning system. Every release is documented on the Releases section.