Building Forms

I never planned on it but a large portion of my career has been building forms. At the peak of this I was working on a project with a form that contained somewhere around 100 total fields that were all conditional and interacted in some way with each other. Changing the value of 1 field could affect maybe 6 other fields and either them being present or their value.

Out of all these years and challenges there’s a pattern I’ve picked up, I don’t recall exactly where from, for how I represent my form fields now, a pairing of the raw value and the parsed value. If we were building a feedback form that needs things like name, email, and age then this would look like

-- Elm
{ name : ( String, Result String String )
, age : ( String, Result String Int )
, email : ( String, Result String Email )
}
// TypeScript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

interface Form {
  name: [string, Result<string, string>];
  age: [string, Result<number, string>];
  email: [string, Result<Email, string>];
};

I like this pattern because it allows me to track both the “raw” value of the input and the desired, or parsed, value. The two huge benefits are that the raw value gets passed directly into the input field, no converting to/from a string, and the parsed value can be used for displaying error messages as well as determining the validity of the entire form. If you happen to have a large form with interdependent fields, you don’t have to validate the raw value at every point, just use the already parsed value!

This can also extend to non-string inputs. If you wanted to turn multiple fields into a single value you can store the raw value as the combination of the multiple inputs and the parsed value van be just the result. This might look like

// TypeScript
interface Form {
  priorEmployers: [Array<{ name: string, phone: Phone }>, Result<string, Array<Employer>];
};

This allows you to surface any (or all) of the errors within the priorEmployers section up to the top form as well as display errors directly next to the employer section that’s causing the error.


Another way you could write this that might make a little more sense is

-- Elm
{ name : { raw : String, parsed : Result String String }
, age : { raw : String, parsed : Result String Int }
, email : { raw : String, parsed : Result String Email }
}
// TypeScript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

interface Form {
  name: { raw: string, parsed: Result<string, string> };
  age: { raw: string, parsed: Result<number, string> };
  email: { raw: string, parsed: Result<Email, string> };
};

I sometimes find this is a little more explicit about what each part corresponds to. With the tuple version you can guess quite easily since the second value is a Result but there’s still guessing involved. With this keyed approach it’s very explicit about what you’re doing. I don’t have much more to say on this but I’ll add a couple small examples at the bottom in case that helps.


Elm Example

https://ellie-app.com/d4FShD8PH6Ja1

React Example

Edit j8ir0