Managing a Form can be darn simple. Just let the user fill in some fields, submit it to the server and if there are any errors notify them and let the user start over again. Is that a good approach? The answer is no, you don't want users to get frustrated waiting for a server round trip to get some form validation result.
Why don't I want users to get frustrated? Because that impacts your business directly; a frustrated client won't complete a purchase, instead they will check your competitor's solution to get a boost up in his productivity, or they'll even sue you for faulty software... long story short, you will end up losing money.
If a user feels comfortable using your application, chances of complete bookings, sales, or processes are way higher.
Now here comes the interesting part... How can I enhance the level of usability of my Forms? You can ask a User Experience expert, and he might give you the following tips (depending on the professional consulted, mileage applies):
We can even go one step further and add some gamification techniques to this form, e.g. booking.com marks your fields in green once you touched them and they are valid.
On the other hand, as a developer you must ensure that a form is stable and easy to maintain:
The approach to follow is to industrialize and standardize processes:
Before starting from scratch, you can check if there already are some solutions implemented for this challenge. In this case we will make use of:
Why searching for already built-in material?
Why look for already built-in material?
Managing Form State (holding field information, check if a control has been touched, if the user has clicked the submit button, who owns the current focus...) can be tedious and prone to errors. We can get help from Formik to handle these challenges for us.
Form validation can get complex (synchronous validations, asynchronous validations, record validations, field validations, internationalization, schemas definitions...). To cope with these challenges we will leverage this into Fonk and Fonk Formik adaptor for a Forkmik seamless integration.
The demo that we are going to implement: link
Github repository: link
If you want to dig into the details keep on reading :).
If you want to implement a form with a superb User Experience, you have to take care of many variables:
You can try to build a solution to tackle these issues on your own, but it will cost you time and money... why not use a battle-tested solution to handle all this complexity? Formik is a great library.
Formik works in a nutshell:
It exposes two main components:
You can find a more detailed explanation in this link.
On the other hand, handling form validation on your own can be tempting, but you have to cover scenarios like:
Of course you can start implementing your own thing, but you will waste a lot of precious time reinventing the wheel. Why not take advantage of a validation library that takes care of all this complexity for you?
We have chosen Fonk. Here's why:
Cool, so how does it work?
The idea behind Fonk is to encapsulate form validation management and expose four methods to the form ui (in this case Formik):
Fonk is divided into three main parts:
You can find more information about Fonk in this link plus official documentation in this link
Formik and Fonk integrate seamlessly, there is an specific extension fonk-formik that returns the validation error information in the format expected by Formik
How does it work under the hood:
Initialization:
Firing validations at form level:
Showing error information:
You can fire validations at field level as well.
That was cool! but should I have to stick to Formik and React forever? Formik is a great choice, but you don't have to stick to it. You can reuse Fonk validations in plain React application, or use React Final Form, or even Vue or plain vanilla JavaScript application.
All the theory is great, but SHOW ME THE CODE !!
Let's implement a Bank Transfer form:
We will start from a boiler plate code that will have:
We will start from a boiler plate code that we have already implemented:
You can find all this examples source code available in the following repository
We will take as starting point the example 00 Boiler plate
The first step is to setup Formik and build the form layout.
Let's start by installing Formik
npm install formik --save
Let's define a Formik form:
./src/playground.jsx
import React from "react";
import { Formik } from "formik";
export const Playground = () => {
return (
<div>
<h1>Formik and Fonk</h1>
<h2>Wire transfer form</h2>
<Formik onSubmit={values => {}}>
{props => {
const {
values,
touched,
errors,
dirty,
handleChange,
handleSubmit
} = props;
return <form onSubmit={handleSubmit}></form>;
}}
</Formik>
</div>
);
};
Time to define the initial data:
./src/playground.jsx
...
<Formik
onSubmit={(values) => {}}
+ initialValues={{
+ account: '',
+ name: '',
+ integerAmount: 0,
+ decimalAmount: 0,
+ reference: '',
+ email: '',
+ }}
>
InitialValues listens for changes, if you update this value later one (e.g. data coming from a fetch call) it will automatically update the data in the form.
Now that we got the form let's place some fields, inside the render prop:
./src/playground.jsx
- import { Formik } from 'formik';
+ import { Field, Formik } from 'formik';
...
return (
<form onSubmit={handleSubmit}>
+ <Field name="account">
+ {({ field }) => (
+ <div>
+ <label>Beneficiary IBAN:</label>
+ <input {...field} />
+ </div>
+ )}
+ </Field>
+ <Field name="name">
+ {({ field }) => (
+ <div>
+ <label>Beneficiary fullname:</label>
+ <input {...field} />
+ </div>
+ )}
+ </Field>
+ <div>
+ <label>Amount of wire:</label>
+ <Field name="integerAmount" type="number">
+ {({ field }) => <input {...field} className="amount-field" />}
+ </Field>
+ <strong>.</strong>
+ <Field name="decimalAmount" type="number">
+ {({ field }) => (
+ <>
+ <input {...field} className="amount-field" />
+ <label>EUR</label>
+ </>
+ )}
+ </Field>
+ </div>
+ <Field name="reference">
+ {({ field }) => (
+ <div>
+ <label>Reference:</label>
+ <input {...field} />
+ </div>
+ )}
+ </Field>
+ <p>
+ If you want to send a notice to the beneficiary, inform the e-mail
+ </p>
+ <Field
+ name="email">
+ {({ field }) => (
+ <div>
+ <label>Beneficiary Email:</label>
+ <input {...field} />
+ </div>
+ )}
+ </Field>
</form>
)}
Things to take into consideration:
Let's add some code to handle the submit button (we will make a console.log showing the field values).
./src/playground.jsx
...
...
<Formik
- onSubmit={() => {}}
+ onSubmit={values => {
+ console.log({ values });
+ }}
initialValues={{
account: '',
name: '',
integerAmount: 0,
decimalAmount: 0,
reference: '',
email: '',
}}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
...
<Field name="email">
{({ input }) => (
<div>
<label>Beneficiary Email:</label>
<input {...input} />
</div>
)}
</Field>
+ <div className="buttons">
+ <button type="submit">Submit</button>
+ </div>
</form>
)}
/>
Let's test this:
We made great progress, but there's one problem: we can hit submit but we won't validate anything on the client's side, behavior which will frustrate the final user.
Let's start our validation journey
We want to add client side validation support in our form. Let's start by installing fonk and fonk-formik extension.
npm install @lemoncode/fonk @lemoncode/fonk-formik --save
Now let's define an empty form validationSchema and create an instance of Fonk validation engine passing as parameter the already created validation schema as parameter.
./src/form-validation.js
import { createFormikValidation } from "@lemoncode/fonk-formik";
const validationSchema = {};
export const formValidation = createFormikValidation(validationSchema);
We've got everything we need, so let's wire up this validation engine instance into Formik:
import React from 'react';
import { Field, Formik } from 'formik';
+ import { formValidation } from './form-validation';
...
<Formik
onSubmit={values => {
console.log({ values });
}}
+ validate={(values) => formValidation.validateForm(values)}
initialValues={{
It's time to create some validations.
In our bank transfer form we want the following fields to be required:
Let's add these constraints to our form validation schema:
./src/form-validation.js
+ import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
const validationSchema = {
+ field: {
+ account: [Validators.required],
+ name: [Validators.required],
+ integerAmount: [Validators.required],
+ decimalAmount: [Validators.required],
+ reference: [Validators.required],
+ email: [Validators.required],
+ },
};
export const formValidation = createFormikValidation(validationSchema);
Let's display the error information inline in the form components:
./src/playground.jsx
import React from 'react';
- import { Field, Formik } from 'formik';
+ import { Field, Formik, ErrorMessage } from 'formik';
import { formValidation } from './form-validation';
...
return (
<form onSubmit={handleSubmit}>
<Field name="account">
{({ field }) => (
<div>
<label>Beneficiary IBAN:</label>
<input {...field} />
</div>
)}
</Field>
+ <ErrorMessage component="span" name="account" />
<Field name="name">
{({ field }) => (
<div>
<label>Beneficiary fullname:</label>
<input {...field} />
</div>
)}
</Field>
+ <ErrorMessage component="span" name="name" />
<div>
<label>Amount of wire:</label>
<Field name="integerAmount" type="number">
- {({ field }) => <input {...field} className="amount-field" />}
+ {({ field }) =>
+ <div className="amount-field">
+ <input {...field} className="amount-field" />
+ <ErrorMessage component="span" name="integerAmount" />
+ </div>
+ }
</Field>
<strong>.</strong>
<Field name="decimalAmount" type="number">
{({ field }) => (
<>
+ <div className="amount-field">
<input {...field} className="amount-field" />
+ <ErrorMessage component="span" name="decimalAmount" />
+ </div>
<label>EUR</label>
</>
)}
</Field>
+ <ErrorMessage component="span" name="decimalAmount" />
</div>
<Field name="reference">
{({ field }) => (
<div>
<label>Reference:</label>
<input {...field} />
</div>
)}
</Field>
+ <ErrorMessage component="span" name="reference" />
<p>
If you want to send a notice to the beneficiary, inform the
e-mail
</p>
<Field name="email">
{({ field }) => (
<div>
<label>Beneficiary Email:</label>
<input {...field} />
</div>
)}
</Field>
+ <ErrorMessage component="span" name="email" />
<div className="buttons">
<button type="submit">Submit</button>
</div>
</form>
);
}}
What are we doing here? Display the error message only if an error has been reported for the given field and component has been 'touched' (touched means: user set focus on the component and jumped into another component, lost focus).
Now if we run the sample we can check if we get error messages as soon as we enter content in the field and fields are touched
Fair enough, but I need to check for other business rules Yup, let's move forward.
Fonk has some built-in validators (email, pattern, ...). Let's validate that the beneficiary email field is a valid email
./src/form-validation.js
...
const validationSchema = {
field: {
account: [Validators.required],
name: [Validators.required],
integerAmount: [Validators.required],
decimalAmount: [Validators.required],
reference: [Validators.required],
email: [
Validators.required,
+ Validators.email
],
},
};
...
Note down: we don't need to add anything else on the UI code, everything is already wired up. We only need to worry about adding our business rules (validators) to the ValidationSchema.
What about the [IBAN](https://en.wikipedia.org/wiki/InternationalBankAccountNumber) number validation? Do I have to write my own validator?_ Fortunately Fonk has an ecosystem of validators that can save us some time coding, in this case we have an IBAN validator at our disposal, let's install it:
npm install @lemoncode/fonk-iban-validator --save
And let's use it in our schema
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
+ import { iban } from '@lemoncode/fonk-iban-validator';
const validationSchema = {
field: {
account: [
Validators.required,
+ iban,
],
...
},
};
...
Why aren't these validators enclosed in the library? We prefer the approach of exposing microlibraries and letting you use them as in a buffet (no need to add extra noise if it's not necessary).
Let's jump into another interesting validator, the use cases:
There is another third party validator available fonk-range-number-validator
npm install @lemoncode/fonk-range-number-validator --save
Let's add them to our ValidationSchema
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
import { iban } from '@lemoncode/fonk-iban-validator';
+ import { rangeNumber } from '@lemoncode/fonk-range-number-validator';
const validationSchema = {
field: {
...
integerAmount: [
Validators.required,
+ {
+ validator: rangeNumber,
+ customArgs: {
+ min: {
+ value: 0,
+ inclusive: true,
+ },
+ max: {
+ value: 10000,
+ inclusive: true,
+ },
+ },
+ },
],
decimalAmount: [
Validators.required,
+ {
+ validator: rangeNumber,
+ customArgs: {
+ min: {
+ value: 0,
+ inclusive: true,
+ },
+ max: {
+ value: 99,
+ inclusive: true,
+ },
+ },
+ },
],
...
},
};
...
As you can see this validator accepts custom params, which allows us to implement flexible business rules.
Just to wrap up this section, let's end with disabling wire transfers. If the IBAN country code belongs to France (imagine that there are some temporary technical issues and you cannot perform that operation on the server side), you can easily implement this using the built-in pattern validator (RegEx):
./src/form-validation.js
...
const validationSchema = {
field: {
account: [
Validators.required,
iban,
+ {
+ validator: Validators.pattern,
+ customArgs: {
+ pattern: /^(?!FR)/i,
+ },
+ message: 'Not available transfers to France',
+ },
],
...
Note down: we are placing this validation at the end of the validators array for the IBAN field, doing so we ensure that we only run this validation once the field has been informed and it's a well-formed IBAN.
The final result is looking great; we have coded a lot of business rules without having to worry about the UI.
Where can I find a complete list of validators already implemented? click here to check the list of built-in validators and here to check the third party validators
Ok, I have seen that there are lot of built-in and third party validations, but sooner or later I will face a validation rule not covered by this buffet. Can I build a custom one? Of course you can! Let's take the case of disabling wire transfers for a given country as a startimg point, but let's add an extra requirement: What if we want to pass a list of countries prefix? E.g. there are temporary issues for IBAN numbers belonging to Germany and France, so we want to pass an array of country prefixes. It's time to create our own validator:
We will implement a very basic one. For a complete guide about implementing custom synchronous validators click on this link
./src/custom-validators/country-black-list.validator.js
import { Validators } from "@lemoncode/fonk";
export const countryBlackList = ({ value, customArgs }) => {
const { countries } = customArgs;
const countriesRegExp = countries.reduce(
(regex, country, i) => (i === 0 ? `(${country})` : `${regex}|(${country})`),
""
);
const pattern = new RegExp(`^(?!${countriesRegExp})`, "i");
const { succeeded } = Validators.pattern.validator({
value,
customArgs: { pattern }
});
return {
type: "COUNTRY_BLACK_LIST",
succeeded,
message: succeeded ? "" : "This country is not available"
};
};
Let's instantiate it in our schema (for instance let's disable France and Spain)
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
import { iban } from '@lemoncode/fonk-iban-validator';
import { rangeNumber } from '@lemoncode/fonk-range-number-validator';
+ import { countryBlackList } from './custom-validators';
const validationSchema = {
field: {
account: [
Validators.required,
iban,
- {
- validator: Validators.pattern,
- customArgs: {
- pattern: /^(?!FR)/ig,
- },
- message: 'Not available transfers to France',
- },
+ { validator: countryBlackList, customArgs: { countries: ['FR', 'ES'] } },
],
...
What would happen if we get the list from a rest api when the form component is mounted? That's an interesting topic, you can add a rule once a component has been mounted and update the associated validation schema.
Let's first remove the rule from the ValidationSchema:
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
import { iban } from '@lemoncode/fonk-iban-validator';
import { rangeNumber } from '@lemoncode/fonk-range-number-validator';
- import { countryBlackList } from './custom-validators';
- const validationSchema = {
+ export const validationSchema = {
field: {
account: [
Validators.required,
iban,
- { validator: countryBlackList, customArgs: { countries: ['FR', 'ES'] } },
],
...
Let's add it to the schema after a fetch call to getDisabledCountryIBANCollection is completed.
./src/playground.jsx
import React from 'react';
import { Form, Field } from 'formik';
- import { formValidation } from './form-validation';
+ import { formValidation, validationSchema } from './form-validation';
+ import { getDisabledCountryIBANCollection } from './api';
+ import { countryBlackList } from './custom-validators';
export const Playground = () => {
+ React.useEffect(() => {
+ getDisabledCountryIBANCollection().then(countries => {
+ const newValidationSchema = {
+ ...validationSchema,
+ field: {
+ ...validationSchema.field,
+ account: [
+ ...validationSchema.field.account,
+ {
+ validator: countryBlackList,
+ customArgs: {
+ countries,
+ },
+ },
+ ],
+ },
+ };
+ formValidation.updateValidationSchema(newValidationSchema);
+ });
+ }, []);
return (
<div>
<h1>React Final Form and Fonk</h1>
<h2>Wire transfer form</h2>
<Form
...
Implementing custom synchronous validations is great, but what about asynchronous ones? Let's check how to do this by coding an example: In this case once the user has entered a valid IBAN, we want to check against the server if that IBAN number belongs to a blacklist. We have the following fake simulation of a rest api call already implemented:
./src/api.js
const mockIBANBlackList = ["BE71 0961 2345 6769"];
export const isIBANInBlackList = iban =>
Promise.resolve(mockIBANBlackList.includes(iban));
We can write our own custom validator:
./src/custom-validators/iban-black-list.validator.js
import { isIBANInBlackList } from "../api";
export const ibanBlackList = ({ value }) =>
isIBANInBlackList(value).then(isInBlackList => ({
type: "IBAN_BLACK_LIST",
succeeded: !isInBlackList,
message: isInBlackList ? "This IBAN is not allowed" : ""
}));
And add it to our schema:
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
import { iban } from '@lemoncode/fonk-iban-validator';
import { rangeNumber } from '@lemoncode/fonk-range-number-validator';
+ import { ibanBlackList } from './custom-validators';
export const validationSchema = {
field: {
account: [
Validators.required,
iban,
+ ibanBlackList,
],
...
That's all, no need to update the UI, no need to know whether the validator is synchronous or asynchronous.
Let's check how far we have gone:
Well, we've had a great time adding field validations, but there are validations that are tied up to the whole record we are editing rather than to a given field. For instance, let's face this scenario: You are not allowed to transfer more than 1000 € to Switzerland using this form (for instance: you have to go through another form where some additional documentation is required).
The best place to fire this validation is at record level.
Record validation functions accept the whole form record info as input parameter, and return the result of the validation (it accepts both flavours sync and promise based). Let's check the code for this validator:
./src/custom-validators/switzerland-transfer.validator.js
import { Validators } from "@lemoncode/fonk";
const isSwitzerlandAccount = value => {
const pattern = /^CH/i;
const { succeeded } = Validators.pattern.validator({
value,
customArgs: { pattern }
});
return succeeded;
};
export const switzerlandTransfer = ({ values }) => {
const succeeded =
!isSwitzerlandAccount(values.account) ||
Number(values.integerAmount) < 1000 ||
(Number(values.integerAmount) === 1000 &&
Number(values.decimalAmount) <= 0);
return {
type: "SWITZERLAND_TRANSFER",
succeeded,
message: succeeded
? ""
: "Not allowed to transfer more than 1000 € in Swiss account"
};
};
In order to set it up in our form validation schema there is a section called record where we can add our validation:
./src/form-validation.js
import { Validators } from '@lemoncode/fonk';
import { createFormikValidation } from '@lemoncode/fonk-formik';
import { iban } from '@lemoncode/fonk-iban-validator';
import { rangeNumber } from '@lemoncode/fonk-range-number-validator';
- import { ibanBlackList } from './custom-validators';
+ import { ibanBlackList, switzerlandTransfer } from './custom-validators';
export const validationSchema = {
field: {
...
},
+ record: {
+ switzerlandTransfer: [switzerlandTransfer],
+ },
};
export const formValidation = createFinalFormValidation(validationSchema);
Now let's add some plumbing in the UI code to display the record error message (it will only be displayed once IBAN and Amount fields have been touched).
./src/playground.jsx
...
<Formik
...
<form onSubmit={handleSubmit}>
...
</Field>
+ {errors.recordErrors && errors.recordErrors.switzerlandTransfer && (
+ <span>{errors.recordErrors.switzerlandTransfer}</span>
+ )}
<div className="buttons">
<button type="submit">Submit</button>
</div>
</form>
)}
/>
</div>
);
};
Let's check this out in action:
Working with HTML plain vanilla controls is good enough if you want to learn how the libraries work without adding additional noise, but in real life projects you use a set of components libraries, advanced custom components... How can we integrate these components with Formik + Fonk?
Below you will find the same example as built before but now using Material UI
Example material, GitHub repository
Full example working codesandbox
If you want to learn more about Formik:
If you want to learn more about Fonk:
Standarizing Form State Management + From Validation can add a lot of value to your solution, benefits that you get:
We are a team of Front End Developers. If you need training, 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.