Using npm
npm install --save lc-form-validation
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.
npm install --save lc-form-validation
require(['lc-form-validation'], function(lcFormValidation) {
var createFormValidation = lcFormValidation.createFormValidation;
// ...
}
const { createFormValidation } = require('lc-form-validation');
// harmony syntax:
import { createFormValidation } from 'lc-form-validation';
<script>
var createFormValidation = window['lc-form-validation'].createFormValidation;
</script>
A simple form with fullname and password fields. Applied validations:
required
validator + custom validator).A sign up form with username, password and confirm password fields with the next validation constraints:
required
validator + custom validator).minLength
validator).
A simple quiz with three options where there has to be at least one checked option to pass the validation (custom global validation).
A form about recipes and ingredients with the next validation constraints:
required
validator).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:
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.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 is just an object literal passed with two main properties:
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: { ... }
}
]
}
The global property will hold an array of validation functions that will validate the entire viewModel:
global: [
<globalValidatorFunction>, // function(viewModel)
...
]
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.
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.
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
""
" "
(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.'
});
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.'
});
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.'
});
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.'
});
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.'
});
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
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;
}
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>
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.
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) { /* .... */ };
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.
{
succeeded: false,
errorMessage: 'Please, fill in this mandatory field',
type: 'REQUIRED',
key: 'lastName'
}
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.{
succeeded: true,
fieldErrors: {
emailAddress: {
succeeded: true,
errorMessage: '',
type: 'EMAIL',
key: 'emailAddress'
}
},
formGlobalErrors: []
}
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.
// 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(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.
// 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
});
formValidation.isValidationInProgress(): boolean;
It checks if a form or a field validation is currently in progress by returning true
or false
.
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)
eventsFilter
to filter validations
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:
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.
formValidation.isFormPristine(): boolean;
It checks if the form was validated at least once using validateForm
or validateField
. It returns true
or false
.
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
});
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
});
This library follows Semantic Versioning system. Every release is documented on the Releases section.