Why can't Form Validation be nice?
Tags: angular, react, html, javascript
Published: 2020-01-01 22:23:03

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*?