Hello Fonk: Rock-solid form validation

The need...

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:

  • Some validations can be synchronous or asynchronous.
  • Some validations can be at field level or at record level.
  • Some field validations may involve other fields in the form.
  • You may want to reuse common validation rules throughout your project (and even share them across different projects).
  • You need to ensure that your validations are rock-solid and can be unit tested.
  • You may want to reuse validations both on the client and on the server side.
  • You shouldn't need to mount the user interface just to test if your validations are working as expected.
  • You must provide the best user experience to your users.
  • You may not want to be a captive customer of a specific UI library or framework validation solution.

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).

TL;DR;

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/

The idea

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?

intro

Benefits that we obtain from this isolation:

  • We know which validation rules are applying to form and fields just by taking a look at a single decalaration file.
  • We can reuse and promote common validation rules (validators) to libraries.
  • We don't need to mount a component to test form validations.
  • We can port these validations to any other framework / library (vanilla js, React, Vuejs...)
  • We could reuse these validations even on the server side (nodejs based).

When we started implementing this library, we faced the following challenges:

  • Validations could be synchronous or asynchronous (e.g. hit a rest api and use the result of those ajax calls to validate a given field).
  • A given field validation may need additional information, not just the value of the field to be validated. It could be just another field in the form record (e.g. a repeat-your-password field needs the original password field), or additional context information (e.g. in a max-length validation what is the maxlength value).
  • There are field and record level validations.
  • You want to fire a validation when a given field changes, but you may also want to fire all field and record validations once the user hits the submit button.
  • You may want to override error messages thrown by the validators (e.g. internationalization support).
  • You just want to keep this framework agnostic and as a micro library (please do not add extra weight to our app :))

Keeping all this in mind we have built Fonk.

The whole picture

Fonk is a microlibrary focused on form validation. It implements the following features:

  • It allows us to decalaratively define the validation of a given form (looking at a single declaration file we can check which validations are applied to which field, plus record validations).
  • It provides support for Field, Record and Form Validation.
  • It provides support for syncrhonous and asynchronous validations.
  • It allows passing context information to validations (field values, record values, custom arguments).
  • It provides support for internazionalization.
  • It's written in plain vanilla javascript, no dependencies.
  • It's pluggable, we have written adaptors for libraries like React Final Form, or Formik.
  • It's extensible: a lot of validators are implemented and it's pretty straightforward to build your own validation rule and plug it into fonk.
  • It offers Typescript definitions for the library.

Fonk is divived into three parts:

fonk overview

Give it a try

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):

example form

We want to validate:

  • Firstname field is mandatory.
  • Lastname field is mandatory.
  • Age field is mandatory, numeric, and should be above 18.

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.

Validators

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:

  • A field is required.
  • A field is numeric.
  • A field is a valid email address.
  • A field is a valid IBAN.
  • A password field matches with a repeat password field.
  • A user field belongs to an existing user (checking asynchronously against a rest api).

You can define:

  • Field Validators, applying to a single field:

    • They have access to the current field value.
    • They have access to the current form record values.
    • They accept context parameters (custom settings).
    • You can define synchronous and asynchronous validators.
    • They support internationalization (customize error messages).
  • Record Validators - global form validation rules:

    • They have access to the current form record values.
    • They accept context parameters (custom settings).
    • You can define synchronous and asynchronous validators.
    • They support internationalization (customize error messages).

Fonk already offers a set of built-in validators:

  • Required: check if a field has been informed (field is not null, undefined or empty string), this can be applied to fields of type string or number.
  • Pattern: check if a field matches with a given regular expression.
  • MinLength: check if a string field has a minimum length.
  • MaxLength: check if a string field has a maximum length.
  • Email: check if a string field is a well-formed email address.

You can find more info in this link

Furthermore, you can find a set of pluggable validators that handle areas like:

  • Numeric values (min, max, range).
  • String values (lower case, upper case, match field...).
  • Bank (credit card, IBAN, BIC).
  • Dates (prior, after, range).
  • Others (url, sanitize...).

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;
};
  • Great! But what if I want to use this validator for any given country prefix (e.g. GE, FR, ...)? Then we can make use of \custom args_:
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:

Form Validation Schema

A Form Validation Schema allows you to synthesize all the form validations into a single object definition:

  • It's just a simple JavaScript object.
  • It contains two root entries: field and record
  • Under field property, we place a child property per field name that we want to validate, each field property will hold an array of validators to be applied to that field (order matters; these validators will be executed sequentially).
  • Validators can be added in two flavors: for simple ones you can just add the validator function to the array, for the ones that need extra context information you can use object notation (inform custom arguments).,
  • You can define record level validations as well (just add the section record to the validator) and include a child entry per set of record validations, each entry will contain an array of record validators.

In a nutshell, you only need to create one object and:

  • Under a property named field include all the field validations (add an entry per form field and include a validators array that applies to that validator)
  • Under a property named record include all the global validations that apply to a given form.

For instance, let's define the following form:

  • We have got a field called 'product', this field is required.
  • We have got a field called 'discount', this field is required.
  • We have got a field called 'price', this field is required.
  • We have got a field called 'isPrime' that tells you if the user is paying a subscription to get free shipping.
  • We have got a record validation, before the user hits submit you should check that price minus discount is greater than 20 USD, or if the user has contracted prime services to include freeshipping.

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]
  }
};

Validation Engine

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:

  • Call validateField on the OnBlur OnChange events.
  • Call validateForm on the submit event.

If you are using React Final Form (using fonk-finalform adaptor):

  • You can call validateField on Field >> validate event, check this example.
  • You can call validateRecord on Form >> validate event (usually you won't call this, but rather delegate it to validateForm). Check this example.
  • You can call validateForm on the Form >> validate.

If you are using Formik (using fonk-formik adaptor):

  • You can call validateField on Field >> validate, check this example
  • You can call validateForm on Formik >> validate, check this example

Integrating with other libraries / frameworks

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?

Wrapping up

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.

Next posts in this series

In the next posts of this series we will cover the following topics in detail:

  • Adding Form validation: Fonk + React Final Form.
  • Adding Form validation: Fonk + Formik.
  • Adding Form validation: Vuejs.

About Basefactor

We are a team of Front End Developers. If you need coaching or consultancy services, don't hesitate to contact us.

Doers/

Location/

C/ Pintor Martínez Cubells 5 Málaga (Spain)

General enquiries/

info@lemoncode.net

+34 693 84 24 54

Copyright 2018 Basefactor. All Rights Reserved.