Form Validation Best Practice
Background
I’ve been working heavily on forms recently, and gathered my thoughts on what I think is a good way to do the form validation.
Form Validation Approach
You can see forms everywhere, when you doing online shopping, when you apply for a colledge, when you pay you bills online… … As a developer, we build these forms, we maintain these forms. So it’s a good investement if we can think this through and gain some deeper understanding of it.
In my opinion, the way to do the form validation is:
- Use the form to collect the data(aka, user inputs)
- Use a separate code to run the validation. The validation result will pass back to the form
- The form will display the validation result to user.
The benefits of using separate code to do the validation are:
-
the logic can be reused
Say we doing the same form in web page and also in mobile client, if we use separated validation code, this validation logic can be shared.
-
the form is easy to manage
Instead of put every logic into the form itself, we make the form simply to react the data it’s getting. The fundamental idea is that UI is a function to based on the data:
UI = fn(Data)
Pass in Value vs. Pass in Control
When interacting with valiation code, one decision you have to make is whether we pass in the value or we pass in the control(widget).
My opinion on this is always use the value.
If we pass the control to the valiation code, the code has a dependency on the control. And you may expect the validation code also handles how to show the errors, which in my opinion is bad because the validation code should only does the validation, not how the UI responses to the errors, especially if the validation logic is shared among different platforms(e.g.: web, mobile etc.).
So I recommend to use value, not the controls, as parameter for validation.
Fluent API1
A fluent API means you can chain the methods instead of doing them line by line. Here’s an simple example:
// non fluent api
let result = SQL.query("xxx");
let filteredResult = result.filter(xxx);
let mappedResult = filteredResult.map(xxx);
// fluent api
let result = SQL.query("xxx").filter(xxx).map(xxx);
That’s the reason why fluent API is also called chaining - you can chain methods together.
The reason to use, more importantly, design, a fluent API in form validation is to do composition - compose the rules together.
A good example for this is the npm package YUP. Here’s a quick demo on the fluent API it provides:
let yup = require("yup");
let schema = yup.object().shape({
name: yup.string().required(),
age: yup.number().required().positive().integer(),
email: yup.string().email(),
website: yup.string().url(),
});
As you can see from the above example, the validation rules for age
includes:
- it’s a number
- it’s required
- it’s greater than 0
- it’s an integer
All the above rules can be written in a simple statement:
yup.number().required().positive().integer()
Final Thoughts
In summary, for form validation, my opinions are:
- use form to collect user input
- use separate code to do the validation
- the validation should accept the values, not the controls, as input
- the validation should use fluent API for composition of rules
- use the form to show the validation result
Happy coding!