At first sight validating a form can be seen as a trivial task... Uh? You only need to check that some fields comply with some rules and that's all, where's the mystery? Well, you need to take into account the following topics:
Taking all these assumptions into account for real projects introduces a big risk of ending up with spaghetti code mixing up concerns (component logic and form validations).
Fonk is a library written in plain vanilla javascript which allows you to define a form validation in a declarative way, and lets you implement reusable field and record validation rules.
Live Example: https://codesandbox.io/s/github/lemoncode/fonk/tree/master/examples/react-final-form/js/validate-field
Getting started: https://lemoncode.github.io/fonk-doc/getting-started
Documentation: https://lemoncode.github.io/fonk-doc/
When you design the validation plumbing for a complex form it's usually tied up to the user interface layer, and what's worse it's hard to check which validations are really applying to the forms and fields without going through reading a big bunch of code, event handlers...
Why not isolate validation into a separate layer?
Benefits that we obtain from this isolation:
When we started implementing this library, we faced the following challenges:
Keeping all this in mind we have built Fonk.
Fonk is a microlibrary focused on form validation. It implements the following features:
Fonk is divived into three parts:
Let's get our hands wet: in this example we are defining the validation schema for a given form (in this case we will integrate it with React Final Form library):
We want to validate:
The validation schema for this form:
import { Validators } from "@lemoncode/fonk";
import { createFinalFormValidation } from "@lemoncode/fonk-final-form";
import { isNumber } from "@lemoncode/fonk-is-number-validator";
import { minNumberValidator } from "./custom-validators";
const validationSchema = {
field: {
firstName: [
{
validator: Validators.required.validator,
message: "Required"
}
],
lastName: [
{
validator: Validators.required.validator,
message: "Required"
}
],
age: [
{
validator: Validators.required.validator,
message: "Required"
},
isNumber.validator,
{
validator: minNumberValidator,
customArgs: { min: 18 }
}
]
}
};
// If you are not using Final Form, just call createFormValidation
export const formValidation = createFinalFormValidation(validationSchema);
Here we just define the validations that apply to each field, setting up an array of rules (validators) per field, which are executed in sequential order. As soon as a validation rule in the queue fails, the field validation process stops (just for the specific field validation in progress) and the validation message error is reported to the form.
We use this validation schema to set up the Fonk ValidationEngine (the function expose is named createFinalFormValidation, but in case you use plain vanilla version it would be createFormValidation).
This engine allows us to execute field, record and form validations and it returns a promise that includes the result of the validation.
A validator is just a function that implements a validation rule.
Cool, but what the hell is a validation rule? A validation rule is just isolated logic to check that a given field or record complies with a given restriction, for instance:
You can define:
Field Validators, applying to a single field:
Record Validators - global form validation rules:
Fonk already offers a set of built-in validators:
You can find more info in this link
Furthermore, you can find a set of pluggable validators that handle areas like:
You can find the full list in this link
If none of these validators suit your needs, or you need to validate custom domain logic, you can always build your own validators.
Custom validators, interesting. What does a custom validator look like? Let's go for a simple one: check if the first two characters of a given field are 'ES', the minimal implementation could be someting like:
Note this validator has been created for learning purposes, we could perform the same validation by using the Pattern built-in validator.
const validatorType = "MY_IBAN_COUNTRY_CODE_VALIDATOR";
export const myValidator = fieldValidatorArgs => {
const { value } = fieldValidatorArgs;
const validationResult = {
succeeded: false,
type: validatorType,
message: "IBAN does not belong to Spain"
};
if (value && value[0] === "E" && value[1] === "S") {
validationResult.succeeded = true;
validationResult.message = "";
}
return validationResult;
};
Why are we returning succeeded true if the value is empty/ null / undefined? We don't cover this case in the validator (e.g. the field may not be mandatory and empty string could be a valid entry). To check if a field has been informed, just use the Required validator (place it as the first element in the field validators array, just to short-circuit other validators in case the field has not been informed and is required).
That was fine, but what if we want to allow consumers of this validator to customize the error message returned (globally for any usage of the validator, or just for the entry being defined in the validation Schema)? We could do something like:
const validatorType = 'MY_IBAN_VALIDATOR';
let defaultMessage = 'IBAN does not belong to Spain';
export const setErrorMessage = message => (defaultMessage = message);
export const myValidator = fieldValidatorArgs => {
- const { value } = fieldValidatorArgs;
+ const { value, message = defaultMessage } = fieldValidatorArgs;
const validationResult = {
succeeded: false,
type: validatorType,
- message: defaultMessage,
+ message,
};
if (value && value[0] === 'E' && value[1] === 'S') {
validationResult.succeeded = true;
validationResult.message = '';
}
return validationResult;
};
const validatorType = 'MY_IBAN_VALIDATOR';
let defaultMessage = 'IBAN does not belong to Spain';
export const setErrorMessage = message => (defaultMessage = message);
+ const hasValidCountryCode = (value, customArgs) =>
+ value &&
+ value[0] === customArgs.countryCode[0] &&
+ value[1] === customArgs.countryCode[1];
export const myValidator = fieldValidatorArgs => {
- const { value, message = defaultMessage } = fieldValidatorArgs;
+ const { value, customArgs, message = defaultMessage } = fieldValidatorArgs;
+ // In your case you may feed default values to customArgs or throw
+ // an exception or a console.log error
+ if (!customArgs.countryCode || customArgs.countryCode.length !== 2) {
+ throw `${validatorType}: error you should inform customArgs countryCode prefix (2 characters length)`;
+ }
const validationResult = {
succeeded: false,
type: validatorType,
message,
};
- if (value && value[0] === 'E' && value[1] === 'S') {
+ if (hasValidCountryCode(value, customArgs)) {
validationResult.succeeded = true;
validationResult.message = '';
}
return validationResult;
};
Let's check how we can include this validator into a Validation Schema (in this case we will check for EN prefix and override the error message):
In the next section you will learn more about form validation schemas.
import { createFinalFormValidation } from "@lemoncode/fonk-final-form";
import { ibanValidator } from "./custom-validators";
const validationSchema = {
field: {
account: [
{
validator: ibanValidator,
customArgs: {
countryCode: "EN",
message: "Not a UK IBAN"
}
}
]
}
};
// Using Final Form, to use plain vanilla solution, call createFormValidation
export const formValidation = createFinalFormValidation(validationSchema);
You can find the full live example in this link
More info:
A Form Validation Schema allows you to synthesize all the form validations into a single object definition:
In a nutshell, you only need to create one object and:
For instance, let's define the following form:
Let's first define a custom record validator to validate the free shipping scenario:
// A record validator receives an object in the args with
// all the record values and optionally the custom message
const freeShippingRecordValidator = ({ values }) => {
const succeeded = values.isPrime || values.price - values.discount > 20;
return {
succeeded,
message: succeeded
? ""
: "Subscribe to prime service or total must be greater than 20USD",
type: "RECORD_FREE_SHIPPING"
};
};
Let's check how to define these rules in a Validation Schema:
const validationSchema = {
field: {
product: [Validators.required.validator],
discount: [Validators.required.validator],
price: [Validators.required.validator]
},
record: {
freeShipping: [freeShippingRecordValidator]
}
};
So far so good... but how can I trigger these validations?
Let's evaluate the following schema definition:
const validationSchema = {
field: {
product: [Validators.required.validator],
discount: [Validators.required.validator],
price: [Validators.required.validator]
},
record: {
freeShipping: [freeShippingRecordValidator]
}
};
Right after defining the validation schema we are making the following call:
// Using Final Form, to use plain vanilla solution, call createFormValidation
export const formValidation = createFinalFormValidation(validationSchema);
This call creates an instance of Fonk validation engine setting up the validationSchema that we have previously created.
If we want to execute the validations we can make the following calls:
Validating a single field
validateField
// assuming the form contains a variable called values that contains the record information
// it won't be needed in this case though (it's an optional parameter)
formValidation.validateField("product", value, values).then(result => {
if (result.succeed) {
console.log("Field Validation succeeded");
} else {
console.log(result.message);
}
});
If you are using FonkFinalForm adaptor, returned values have a different structure (check this link for more information).
If you are using FonkFormik adaptor, validation errors are reported via throw exception (check this link for more information).
validateRecord
Executing a record level validation:
// assuming the form contains a variable called values that contains the record information
formValidation.validateRecord(values).then(result => {
if (result.succeeded) {
console.log("Record Validation succeeded");
} else {
console.log(result.freeshipping.message);
}
});
If you are using FonkFinalForm adaptor, returned values have a different structure (check this link for more information).
If you are using FonkFormik adaptor, validation errors are reported via throw exception (check this link for more information).
validateForm
Before submitting your form it's a good idea to fire all field and record validations. Is there a way to perform all these calls in one go? The answer is yes, the Fonk validation engine exposes a method for that: validateForm.
// assuming the form contains a variable called values that contains the record information
formValidation.validateForm(values).then(result => {
if (result.succeeded) {
console.log("Form Validation succeeded");
} else {
console.log(result);
}
});
If you are using FonkFinalForm adaptor, returned values have a different structure (check this link for more information).
If you are using FonkFormik adatpro, validation errors are reported via throw exception (check this link for more information).
Ok, cool... on to the next question. Where should I call these methods?
If you are using a plain vanilla javascript solution or just pure React or Vuejs:
If you are using React Final Form (using fonk-finalform adaptor):
If you are using Formik (using fonk-formik adaptor):
Fonk is written in plain vanilla Javascript, which means that it can be easily integrated with other frameworks and libraries.
So far we have got support for plain javascript, react + final forms, react + formik, and we are working on creating examples for Vuejs.
We are looking for volunteers willing to help build adaptors for other libraries and form state managers. Will you join us?
Breaking complexity into smaller pieces and following the separation of concerns principle, allows us to industrialize our software development cycle, and hence create rock-solid applications.
In the next posts of this series we will cover the following topics in detail:
We are a team of Front End Developers. If you need coaching or consultancy services, don't hesitate to contact us.
C/ Pintor Martínez Cubells 5 Málaga (Spain)
info@lemoncode.net
+34 693 84 24 54
Copyright 2018 Basefactor. All Rights Reserved.