loading...

Why can't Form Validation be nice?

ronnewcomb profile image Ron Newcomb ・4 min read

I've used five different ways of doing form validation now, two in React and two in Angular and one with just jQuery, and I hate them all.

Why is form validation, especially in the age of Typescript, so long-winded, so convoluted, so many lines of code? Why isn't form validation nice? It's just a few boolean functions, right? So why does it require 50 lines of code for a simple email-and-password form, let alone anything complex?

A model has properties that are string, number, boolean, Date, enum, and arrays or sub-objects also comprised of same. But HTML forms largely have only one type, that of string. So a form validation library must perform model mapping to turn non-strings into strings on form init, and then map them all back to non-strings on blur (or even more often) to validate. So form validation implies a layer of model mapping, even though 95% of the time it's so straightforward it basically goes unused.

Model mapping itself is a bit of a headache. Although a native datatype, dates lack a HTML input element dedicated to them so we'll always need some custom datepicker component that works who-knows-how. Enums have two elements, the dropdown and the radio button set, even though javascript lacks a native enum type. And then you get a work ticket wanting to represent an expiration date with a lone checkbox element which "means 60 days into the future if checked, null if unchecked, or preserving the same value it had on form init no matter how many times the box is unchecked then rechecked, unless it was left unchecked on submit in which case undefined is fine."

Form fields don't map to validation functions 1-to-1. The same field will have multiple constraints like required and range and length, but combining them into a single function limits re-use. But if we library-ize them, then our fields must work with the standard functions as well as custom functions, and syntax is almost always nicer for one set than the other.

Form fields need outside information to validate. AngularJS learned this lesson the hard way when validating a field depended on the value in another field. (Angular2+ then explicitly added ways for cross-field validation.) Some validations require an async call to see what the valid values even are. For example, the province/state field relies on the value of the country field above it, but there are a whole lot of provinces in the world so wait on fetching the list until after country is chosen.

Mapping itself can throw surprise validation errors of the is-it-plugged-in variety. How many times have we tried to use a new DatePicker but it fails either on the initial date-to-string conversion, or the string-to-Date submit conversion? It's a silently understood validation rule that a date must be a valid date, a number a valid number, yet we're surprised when a datepicker passes all its Required and Less-Than-Expiration rules, but fails anyway on an unwritten rule because of parsing.

The final format of the Errors object can cause lines of code. If Errors is an array of keywords then showing lastName's Required message involves a lengthy array.find invocation. If Errors is an object then asking how many involves a nested Object.keys invocation. Nested, because the Errors object can't have a simple flat structure. Multiple fields can fail Required, and one field can fail all of its validations simultaneously.

When it comes to showing the error messages there's also several fine ways of doing so. Add or remove CSS classes that control a div's visibility, or pass something from Errors to a component as in <Err show={errors.lastName.required}>Last Name is required</Err>. Sometimes a pre-existing form validation library doesn't intersect well with a pre-existing UI Elements kit and that causes a lot of boilerplate: the validation gives an Errors object but the UI kit wanted it to toggle a particular classname on a particular element, so we have to glue it together.

And I haven't even mentioned forms which have an array of stuff in them, like an array of addresses. Validation functions need to know if their result and the field they are attached to are irrelevant because the user deleted the third address. Dangling references annoy.

Many nice solutions for mapping fail when the model has a sub-object, even one as simple as three properties year/month/day.

But when I see every single <input/> element on every single form in the whole app possess a filled-in name, type, id, value, onBlur, onChange, onInit and various data-* properties, sixteen React Hooks or a page of Angular's type-unsafe FormBuilder, regexes embedded within HTML, and onSubmit pointed to the validation library's onSubmit handler which then takes another onSubmit handler as a parameter which is our actual onSubmit handler, which can fail form validation after submission because the server said so and how do we even, I just gotta ask: why can't form validation be nice?

Posted on by:

Discussion

markdown guide
 

I recommend trying Vest: It is a data validation library formed like assertion (familiar format) and it has easy examples for vanilla Javascript and React to get started.

GitHub logo ealush / vest

Vest - validation testing

Vest

Vest - Validation Testing

npm version Build Status Known Vulnerabilities

What is Vest?

Vest is a validations library for JS apps that derives its syntax from modern JS frameworks such as Mocha or Jest. It is easy to learn due to its use of already common declarative patterns.

The idea behind Vest is that your validations can be described as a 'spec' or a contract that reflects your form or feature structure. Your validations run in production, and they are framework agnostic - meaning Vest works well with React, Angular, Vue, or even without a framework at all.

Example code:

// validation.js
import { validate, test, enforce } from 'vest'
const validation = (data) => validate('NewUserForm', () => {
    test('username', 'Must be at least 3
 

Vest won't help at all in this case. It's purely a lightweight test framework.

Case in point, if you look at their examples on Git, you have to then re-implement your validation rules (again) with the matches methods.

 

Haven't heard of that one but I'll check it out, thx.

 
 

Formik's mess was what prompted this post, particularly the bit about the onSubmit.

 

You should include code snippets about the issues you’re describing to have a better picture

 

Also, what about localized number input format validation and formatting? Hell.

 

Actually my employer lost a client over this recently. Yeah.

 

Can't use it for super high volume forms, but since you're going to have all of the same validation on the server, just keep simple validations presentation side and have your server side validation kick back detailed messages for the heavier stuff. A compromise on UX but its perfectly viable for many forms.

 

What about React Hook Form? I find it elegant enough when combined with a UI lib.

 

UI and Validations are completely different concerns. I would stay away from anything that's tied directly into one UI framework or another.

Presentation is one thing, and validation stands on its own, and you really don't want to mix the two together. Your presentation layer should only be using the output of your validations, nothing more.

I wrote a framework named Vest (also mentioned in another comment), which takes this approach into practice and separates your UI from your validation logic. You might like it.

Here are two very different examples using react hooks:
stackblitz.com/edit/vest-react-sup...
stackblitz.com/edit/vest-react-reg...

Link to Vest:
github.com/ealush/vest

 

I've read it over twice now, but near as I can tell, it doesn't have anything to do with Forms. It's model-validation which 1) Yup does the same job more concisely, and 2) most devs (read: me) hate writing unit tests so much that the similar syntax is actually a minus :)

The annoying part of forms validation is wiring up the actual input and form event handlers without loads of boilerplate, and mapping -- changing an known-good model to/from something the webbrowser elements understand.

Vest seems more similar to Yup than anything else.

Hey, sorry for replying late.

I can see where you're coming from. Yup and other schema validation libraries are great, and there is nothing much you can do with Vest that you can't do with them. They may even do it more concisely.

That's not my argument for using Vest.

You are right, though. Vest is not a perse form validation library, but it is also not a strictly data-model validation library.
It is not a data model validation library since it does not validate the structure of your data. You do not provide a schema to be validated, but instead describe different tests for your data - and the difference between the two is quite large.

If I had to put describe Vest in a sentence I would say something like: "App logic data validations".
The reason for this distinction is simple: Vest does a little more than just validate your data. It also gives you the tools and structure to handle your validations in your feature itself.

An inherent part of every Vest test is the error message displayed to the user upon failure, and since you can write multiple tests for the same field, you can present different error messages per field.

Vest gives you the ability to only validate some fields, for example - only the field that the user currently interacts with.

There are quite a few more features, but the general idea is this - it is not only about the data, but how your validations interact with your feature.

"The annoying part of forms validation is wiring up the actual input and form event handlers without loads of boilerplate, and mapping -- changing an known-good model to/from something the webbrowser elements understand."

So no, Vest is not strictly a form validations library, but working with it on multiple features I did notice the ease of linking a form field to validation test (or tests), and when done correctly, you only need a couple of lines to make your validations work with your inputs and form fields.

I encourage you to take a second look at this file (index.js)
stackblitz.com/edit/vest-vanilla-s...

That's really all it takes to make your form validations work with Vest, it's even easier when using a framework such as React or Vue.

 

It's not bad at all, but I'm starting to see a lot of components - not necessarily forms - start with a paragraph of Hooks for every little thing. Seems no one wants to use a single setState anymore, but now wants to use a separate hook for every string or number?

RHF starts with 3 hooks...

Also I really hate repeating
onBlur={handleBlur}
onChange={handleChange}
on every single element. RHF and Formik both require this boilerplate.

 

with RxCompForm you got ReactiveForm validation with FormGroup and FormArray in a modern ES6 vanilla like environment. If you know Angular you already know how to use it.

 

Great article , Here is one of those solutions to make it nice

dev.to/ronaldhove/a-custom-form-bu...

 

Haven't heard of that one either. Just from reading the landing page you linked me, I was struck by:

1) Overriding css seems hacky; I must use !important?! Srsly? How about it not use it's own classes at all if something's passed in?

2) it paints the DOM structure for literally the whole form. The approach satisfies me as an engineer but I have a feeling it won't mesh well when you have a pure designer (HTML/CSS) on the team. Or if designers made pretty input box elements or whatever in a library; can it somehow use that, not knowing the internal structure of proprietary ui components?

3) I don't see how it knows to compare passwords in the last example. Convention over configuration?

4) formControlName seems redundant between having both title and type around. (Granted, type is rarely unique, but sometimes it is, and title is almost certainly unique.) Yeah I know that's Angular's not the library's. Icon seems like it should be part of the CSS class, not in the code, and could also hang off of the type attribute.

5) passing the text of the submit button seems highly over-specific, but I understand it's making the whole form from scratch.

6) unclear why every AbstractControl must clutter the controller.

7) IIRC the validators array can also take a lambda accepting.. the value? the whole form? ... but I seem to remember Angular having a bad time if the lambda depended upon an outside var. Like it wouldn't re-render or something. Have to check my old Stackoverflow posts. Still, it's mostly good.

8) async doLogin didn't return its promise, so the form would know when teh server is done? (Oh, it's used as an event handler.)

It's got potential as a nice solution as long as #2 isn't a practical problem. Even then I nitpick the syntax as too verbose. Maybe every FormField object should add a property for the AbstractControl so they aren't running around loose in the controller. Maybe the FormField[] and the errors[] could be under a single umbrella object to avoid cluttering the controller, or, each FormField has its own errors. Maybe FormField itself could be slimmed down with reasonable defaults for formControlName and removing icon or other CSS completely. Using one's own CSS should never be so clunky. Maybe the umbrella object can have a prop for a Promise-returning submit function so it knows when to re-run validation on server return in case of server-side validation failing.

The final comment in one example regarding "we know fields X and Y are at indexes 0 and 1" screams "hacky magic numbers" to me.

Maybe #2 could be addressed by configuration: inform the library of the shape of your datepicker, of your styled inputs and validation errors, by way of some sort of template passed to forRoot(), so the form it dynamically creates matches your employer's chosen standard.

Thanks for the suggestion.

 

Thanks for the feedback , you have some great points I appreciate the time you took going into detail with each one.

This is a new project I made while working on a very simple app so I will have to make the form builder more configurable to meet other people's needs

To address some of your points

1) It actually does work like that , when you pass in css classes it uses those instead , When explaining how it works , I was leaning more towards explaining the usage example , I will change the the docs to be more clear on that

2) Probably another case of better docs , the form builder uses mostly ion-components , so DOM structure should be customisable a very decent amount.

3) When your FormFeild object array contains the controllers password and confirm_password then the form builder knows its a case of password validation

4) type is the input type i.e

        <input type="text">

title is for the label , and formControlName is used by angular formbuilder when building the actual form , these could be very different

8) If you read the the scenario description , I make the assumption that there is a service api that implements a login function that returns a promise, doLogin just calls that function and when it fails the catch block handles that error. This is for the purpose of describing a use case for setting individual field errors

 

I love the Yii2 form validation. It's a PHP server side framework that outputs the client side code automatically for me and has a lot of control over all the weird cases like only being required if X=6 and z is set.

But as I mostly create and interact with REST APIs I don't get to use it too often.

I've just started playing with React and wondering if there's a good form validation system to go with material-ui or if I should just roll my own for my basic needs.