elm radio
Tune in to the tools and techniques in the Elm ecosystem.
Exploring a New Form API Design
We dive into some ideas for a new Form design, pulling in a lot of our favorite Elm techniques to make it safe and intuitive.
Published
July 18, 2022
Episode
#61
Parse, Don't Validate
etaque/elm-form
Codecs episode
Some similarities in the design for
how you build up Custom Type codecs with
elm-codec
Transcript
[00:00:00]
Hello, Jeroen.
[00:00:01]
Hello, Dillon.
[00:00:02]
I have to admit, I'm a little bit intimidated by our topic today.
[00:00:07]
Me too, a little bit.
[00:00:09]
I feel like people will have expectations and I'm not sure I'll be up to par, but we'll
[00:00:16]
see.
[00:00:17]
We'll see.
[00:00:18]
Yeah, forms are a formidable topic, if you will.
[00:00:22]
They are...
[00:00:23]
Ooh.
[00:00:24]
Nice.
[00:00:25]
It really is a complex problem space, I think.
[00:00:31]
And it is ambitious to...
[00:00:33]
I mean, working in a type safe world, it does put an extra burden on us to design all the
[00:00:42]
pieces to fit together neatly, especially when what we're doing is essentially data
[00:00:47]
intake and data processing and throw type safety into the mix.
[00:00:52]
And that's a lot of responsibility to make a really nice API for transforming things
[00:00:57]
in a way that we can parse, don't validate.
[00:01:01]
And at the same time, all these concerns are working together.
[00:01:06]
The logic for presenting the view, but that can be dependent on what you've parsed and
[00:01:14]
how you render individual fields and how you present them.
[00:01:18]
So it's going to parse into the appropriate thing and how you display errors for each
[00:01:23]
of those fields, how and when you display errors for those individual fields.
[00:01:28]
So that's sort of what I hoped we would get into today.
[00:01:33]
I don't have a particular conclusion about which form people should use, which form API.
[00:01:41]
Rather, I sort of hope we can explore some of these different design considerations and
[00:01:48]
really what I'm hoping to do is talk through some of my ideas for how the state of form
[00:01:55]
APIs could be improved.
[00:01:58]
That's really what I want to get into.
[00:02:00]
Yeah, let's do that.
[00:02:01]
I also don't have any specific package that I want to recommend because the one I'm using
[00:02:07]
at Humio is custom made and slightly different from the ones that I see on the Elm packages
[00:02:14]
registry.
[00:02:15]
I'll be curious to hear about that.
[00:02:19]
We'll see.
[00:02:20]
Yeah.
[00:02:21]
Cool.
[00:02:22]
Dillon, what is a form?
[00:02:24]
Oh, it is a form.
[00:02:26]
Well, I mean, I think, again, it is sort of these two different concerns.
[00:02:32]
How do you present something to the user that they can input data into and how do you take
[00:02:38]
that data and turn it into structured data that might have errors, also known as parsing.
[00:02:45]
Sometimes we call that...
[00:02:46]
Decoding, data dating, parsing.
[00:02:49]
Yes.
[00:02:50]
And, you know, ideally, I think that parse don't validate is really key in my mind to
[00:02:56]
an ideal form API because, sure, you can sort of treat everything as strings or low level
[00:03:03]
data and leave it at that and just tack on errors.
[00:03:07]
And that simplifies a lot of things, but it doesn't feel right.
[00:03:14]
It feels like we should be able to parse things into nicely structured data.
[00:03:19]
At the core, I think it's that and that data, then the purpose of it is usually to send
[00:03:25]
it to a server.
[00:03:27]
Not always.
[00:03:28]
Yeah.
[00:03:29]
You sort of front end only forms where we're not really persisting it to a back end or
[00:03:34]
interacting with some external service.
[00:03:37]
But usually that's what it's for.
[00:03:39]
Yeah.
[00:03:40]
So a form is a series of inputs that you can turn into either a list of errors because
[00:03:47]
the form is not complete or has problems or to something, let's say, final that you can
[00:03:54]
submit to a service, a server, another function, something like that.
[00:04:00]
Just a series of inputs that you want to handle in a nice way.
[00:04:04]
Yeah, absolutely.
[00:04:05]
And that you would like to make some nice interactions with, especially with regards
[00:04:10]
to validation errors and whatnot.
[00:04:14]
Yeah, absolutely.
[00:04:15]
You want to give really nice user feedback and you want that feedback to be meaningful,
[00:04:22]
precise and in sync with what your actual back end expects.
[00:04:27]
So that's like a little bit out of scope for this discussion, but maybe a bit of foreshadowing
[00:04:34]
for a future episode about how might you keep your form parsing logic in sync between your
[00:04:42]
front end and back end?
[00:04:43]
Well, wouldn't it be handy if we could run the same code on both sides and have that
[00:04:48]
be Elm code that so we know that they're in sync.
[00:04:52]
And yes, of course, I'm talking about the new Elm pages release, which we'll definitely
[00:04:57]
be talking about at some point.
[00:04:59]
Forthcoming Elm pages release.
[00:05:01]
Yes, yes.
[00:05:03]
It's not released yet.
[00:05:04]
So it's not new.
[00:05:05]
That's right.
[00:05:06]
That's right.
[00:05:07]
You have more work to do Dillon to release it.
[00:05:10]
So don't give them false hope like it's available today.
[00:05:14]
No, it isn't.
[00:05:15]
No, it isn't.
[00:05:16]
No, it's going to be awesome.
[00:05:18]
Yeah.
[00:05:19]
I've been thinking about forms a lot because it's pretty core to the goals I have for Elm
[00:05:26]
pages v3, especially being able to take input, user input and turn it into trusted validated
[00:05:37]
data.
[00:05:38]
And you want that feedback.
[00:05:40]
You want to share that feedback about what makes it invalid to the user.
[00:05:44]
And you want to be able to use that same logic to get it into a trusted form, you know, trusted
[00:05:51]
structured data that you can use to do things with, you know, with those assumptions that
[00:05:56]
you made with your parsing logic.
[00:05:58]
So if you if you say something is a valid username, then you need to be able to trust
[00:06:03]
that.
[00:06:04]
Before we dive into that, I want to know whether you think what a form API is for because when
[00:06:12]
I was working with React back in the day, it's not like I did it for a long time.
[00:06:18]
But when people said, hey, I'm looking for a form package, it felt more like people were
[00:06:24]
looking for how to create form UIs.
[00:06:30]
Like I want to say here is an input and it expects a number.
[00:06:36]
And based on this condition, it shows up or doesn't show up.
[00:06:40]
And it will look like this.
[00:06:41]
And I feel like the packages that you see in the Elm community, they don't have that
[00:06:47]
UI component.
[00:06:48]
They don't have a visual.
[00:06:50]
They only have that logic about validating, parsing, showing errors, but they don't have
[00:06:56]
that particular point of outputting or rendering a UI.
[00:07:02]
When you say rendering a UI, do you mean like rendering a field that's like a password input
[00:07:08]
type, so it masks the characters versus a text area?
[00:07:12]
Yeah, exactly.
[00:07:13]
And with a specific UI, it looks good and you can just put it in your project and use
[00:07:20]
it and you will be happy.
[00:07:23]
And it shows that I see.
[00:07:25]
Okay, right.
[00:07:26]
Well, yeah.
[00:07:27]
So I think first of all, starting with what would vanilla form handling look like in Elm
[00:07:34]
is a very good place to start.
[00:07:35]
I like that a lot.
[00:07:37]
For the problem of like presenting the form versus kind of parsing the input, that's a
[00:07:44]
great distinction.
[00:07:46]
There definitely are helpers in some of the common packages in the Elm ecosystem that
[00:07:51]
help with this.
[00:07:52]
There are a few considerations here.
[00:07:55]
So the different approaches you could take, you could completely leave it up to the user
[00:08:00]
and say, all I do is parse data.
[00:08:03]
I don't have any concern about how you present it.
[00:08:07]
That's up to the user.
[00:08:09]
So there are different approaches you could take this.
[00:08:13]
You could leave it completely up to the user to deal with presenting the forms.
[00:08:19]
And all you do is you parse the data from those forms.
[00:08:23]
You could sort of provide some helpers for presenting those forms.
[00:08:27]
That's kind of the approach that one of the more popular packages out there, Itake Emilian,
[00:08:36]
I think is his name.
[00:08:37]
I don't know how to pronounce that properly.
[00:08:39]
But this package Elm form is like a sort of decoder style API for parsing form input.
[00:08:48]
And it provides a few helpers for displaying fields.
[00:08:53]
Like nice looking fields, you mean?
[00:08:55]
It doesn't have any opinions on how they look.
[00:08:59]
And I kind of think that that makes sense because at the core, you have essentially
[00:09:06]
three different elements, maybe four.
[00:09:11]
You have inputs.
[00:09:13]
That's kind of the broadest one because a radio button is a type of input.
[00:09:18]
Text input is an input.
[00:09:19]
Password input is an input.
[00:09:20]
A data input is an input.
[00:09:22]
You have buttons.
[00:09:23]
Texts can be used to submit forms.
[00:09:26]
Actually you can create that using an input element.
[00:09:30]
So that's sort of just a special case.
[00:09:34]
And I mean, depending on how you're building your forms, if you're not necessarily concerned
[00:09:39]
about building it as a semantic HTML form, where it's actually like a valid form with
[00:09:45]
a button that would actually do something without JavaScript, then you could say like,
[00:09:50]
you know, that's not even a concern of the package.
[00:09:54]
Here's a message you can attach to it to submit or a function you could use to validate or
[00:09:59]
whatever and wire that up to whatever element you want.
[00:10:02]
I don't care.
[00:10:03]
I don't have an opinion on how you render that.
[00:10:06]
And then you've got a select element which can have dropdowns and you've got a text area.
[00:10:12]
So there aren't that many.
[00:10:13]
And a text area is really kind of like an input text.
[00:10:17]
It's just like really it seems like it should be a different input type.
[00:10:21]
There's probably some historical reason for why it's not.
[00:10:24]
You mean it should be the same or it should be a different one?
[00:10:27]
Because there are different ones.
[00:10:29]
I mean it should be like, why not have it be an input element with type equals text
[00:10:37]
area or something like that.
[00:10:39]
Like it's just a, at the end of the day, it's functionally equivalent to input type equals
[00:10:44]
text but it's presented differently but that's the same for input type equals tel for telephone
[00:10:50]
number or input type equals email.
[00:10:52]
It's like functionally the same as input type equals text.
[00:10:56]
You know email is maybe going to run a client side validation automatically unless you have
[00:11:03]
a no validate attribute which usually is actually what you want to do because you want to present
[00:11:08]
your own error messages however you want to present them and have more fine grained control
[00:11:13]
over what error messages to present and how and when to present them.
[00:11:18]
So at the end of the day, my point is that the concern about how to present it nicely
[00:11:25]
I personally think is a separate concern from sort of presenting the raw input fields and
[00:11:34]
parsing that data.
[00:11:35]
Those two things I think should be a single concern of how you present the low level fields
[00:11:42]
and how do you parse the data.
[00:11:44]
And if those two things know about each other there are certain nice things you could do
[00:11:48]
because a date input has certain implications for what type it will parse into.
[00:11:54]
Presenting it in terms of nice styles and that sort of thing.
[00:11:58]
My thought on that is that that should be a separate concern.
[00:12:02]
Not that you couldn't have nice abstractions to help with that but I think that you can
[00:12:08]
handle that pretty well by just saying, hey, I know how to render these fields that you
[00:12:13]
have and hopefully the form API you have knows whether it's a date field or a text field
[00:12:20]
or a password field and it knows how to put the right attributes for that.
[00:12:25]
But then it should allow you to put other HTML attributes to style it.
[00:12:30]
That's how I think of it.
[00:12:31]
Yeah, same here.
[00:12:32]
I feel like it could be useful to have the form API provide those helpers but at some
[00:12:39]
point it's going to conflict with what your UI designer will want or what you will want
[00:12:46]
the application to look like.
[00:12:49]
So it can help you get started maybe to get started with having a nice looking thing but
[00:12:56]
in the end you will want to replace it at some point or have it be quite customizable.
[00:13:04]
I think I mentioned this before but I feel like that's also the same reason why we don't
[00:13:08]
have a lot of UI packages with nice inputs, with nice buttons, with all those niceties.
[00:13:15]
Except the ones that are like this is a standard material UI from Google.
[00:13:21]
There's a package for that.
[00:13:23]
And that makes sense because if your company says or your team says we're going to build
[00:13:28]
it with this material UI style then this makes a lot of sense.
[00:13:32]
It's going to work for you in the long run or in some version of the long run.
[00:13:38]
But otherwise it's unlikely that it's going to work out.
[00:13:43]
So I do feel like they should be separate.
[00:13:46]
Because you can get backed into a corner where you have a nice getting started experience,
[00:13:53]
things look really good and you're like wow this is simple.
[00:13:56]
There are so few decisions I need to make.
[00:13:58]
It looks nice.
[00:14:00]
It gives me what I need.
[00:14:02]
And then you get stuck.
[00:14:04]
And that's no fun.
[00:14:05]
No, no, you don't get stuck.
[00:14:07]
You start saying well we can't do this because this library doesn't allow us to.
[00:14:15]
Or you start doing hacks or you start forking it or you start using a separate library for
[00:14:22]
those specific inputs.
[00:14:24]
Yeah, exactly.
[00:14:27]
So I feel pretty strongly personally that the way that the view is presented should
[00:14:33]
be extremely unopinionated and flexible except for the basic low level fields.
[00:14:43]
That should be opinionated.
[00:14:45]
That should be like hey if you're building like if you say here's a password input field
[00:14:51]
that our API helps you do that.
[00:14:53]
If you say here's a date field our API helps you do that and it helps you parse that because
[00:14:59]
it knows things about the expected format that will come from the date pickers that
[00:15:07]
are going to be presented to users in their browser.
[00:15:10]
So I feel like we agree that presenting a nice UI is not part of what a form API should
[00:15:16]
do.
[00:15:17]
But then what is remaining is our potentially three things in my opinion.
[00:15:23]
Two of those I totally agree with the third one I'm still not sure.
[00:15:27]
One of them is handling errors like validation errors and making sure they look good and
[00:15:35]
that they appear when they should based on the user's interaction.
[00:15:38]
We can go more about that later.
[00:15:40]
The second one is doing the actual validation saying this field should take a phone number
[00:15:48]
and a phone number looks like this for instance.
[00:15:50]
And that could potentially be a whole separate API in practice and a whole separate package.
[00:15:56]
And the third one is what to show when.
[00:15:59]
So like for instance having conditional fields, conditional inputs presenting them how, presenting
[00:16:05]
them where and with some configuration.
[00:16:10]
And I feel like that last point is what a lot of form APIs, at least what I've seen
[00:16:17]
in React land, they get very hairy.
[00:16:22]
They try to customize everything using like one record to be able to express anything
[00:16:28]
and everything.
[00:16:30]
And you were basically inventing a new DSL and at some point you're going to get stuck
[00:16:34]
because it's not going to be able to do what you want at all or in a nice way.
[00:16:39]
Is that what you feel as well?
[00:16:41]
Yeah, I'm definitely on the same page with you here.
[00:16:45]
And you brought up a lot of good insights here I think.
[00:16:49]
So you said looking at fields and potentially having that be its own API.
[00:16:55]
So one of the things that I encountered, so a little bit of background, I tried to sort
[00:17:02]
of create a form API for the Elm pages v3 project from first principles.
[00:17:10]
And I built an API and I wasn't sort of looking at docs for other form APIs.
[00:17:19]
I was just sort of like what do I need here?
[00:17:21]
And to a certain extent I'm coupling it to certain framework elements that I can hook
[00:17:26]
into which a vanilla Elm form API is not going to do.
[00:17:31]
But otherwise I'm trying to solve a lot of similar problems from first principles.
[00:17:37]
And it's actually a really good exercise because I built an API.
[00:17:43]
I actually was really happy with certain pieces of it and certain pieces of it I was very
[00:17:48]
unhappy with.
[00:17:49]
And I basically threw it away and rummaged through the pieces that I liked to salvage
[00:17:55]
them for a new from scratch API.
[00:17:59]
And I came up with a totally different approach.
[00:18:02]
And then what I've done now is I've sort of surveyed a lot of these popular form APIs.
[00:18:07]
I've asked people what APIs they used.
[00:18:10]
I've gotten some like code snippets of some homegrown solutions and looked at those, looked
[00:18:16]
at what's in common, had some discussions with people.
[00:18:18]
It's been really cool to compare and to see the roadblocks I ran into with my initial
[00:18:25]
design, the things I tried with my new design, which I'm really pleased with and to compare
[00:18:30]
all those.
[00:18:31]
So for like one of the things I hit up against with my initial prototype that I was really
[00:18:39]
unhappy about was I made it too rigid because I was trying to make it this combinator style
[00:18:46]
pattern where like we've talked about in the past, like with Elm GraphQL, this unlocked
[00:18:52]
some really cool things to say, hey, is it coming?
[00:18:55]
What do you mean with a combinatorial API?
[00:18:58]
Basically the, not combinatorial, but like a combinator.
[00:19:03]
Just meaning things you combine, but I think of it as like a recursively defined thing.
[00:19:11]
So like a selection set, so you could either have a selection set and a field in Elm GraphQL,
[00:19:17]
or you could say a field is just a special case of a selection set.
[00:19:22]
It's a selection set with one selection.
[00:19:24]
Yeah.
[00:19:25]
So it's kind of like for people who don't know GraphQL, JSON that decode value can contain
[00:19:33]
a JSON decode value again.
[00:19:36]
Exactly.
[00:19:37]
Exactly.
[00:19:38]
So it's like a recursively defined data type and you can sort of deal with it in a very
[00:19:42]
composable way because of that.
[00:19:44]
You don't have to go into these special cases where you have to deal with it differently
[00:19:48]
if it's a field or a selection set, or if it's a JSON object or a JSON string or whatever.
[00:19:55]
So at first I thought, well, that seems like a really nice approach.
[00:19:59]
I'll make it really composable.
[00:20:01]
And I said, why should a field be different than a form?
[00:20:06]
It's really the same thing.
[00:20:07]
It parses into something and a field is a special case of a form.
[00:20:11]
Right.
[00:20:12]
Yeah.
[00:20:13]
Which seems like a really good idea in theory.
[00:20:15]
In practice, I didn't like it at all.
[00:20:18]
And the reason is because it made it too rigid both for presenting the view and for doing
[00:20:27]
the decoding.
[00:20:28]
So like, for example, if I wanted to do password confirmation, check that a password matches
[00:20:34]
a password confirmation field.
[00:20:36]
Or if I wanted to select a dropdown and based on that show a different field.
[00:20:45]
Yes, you need to do a lot of and thens or something.
[00:20:48]
And then and then when you try to build that with something where you're actually like
[00:20:54]
presenting the view, the types become so rigid that you can't really solve the problem the
[00:21:01]
way you want to.
[00:21:03]
And you get stuck.
[00:21:05]
You get stuck in a corner.
[00:21:07]
So I basically found that doesn't like it's not a nice way to work with forms.
[00:21:11]
So what I arrived at for when I threw that design away and built a new one from scratch,
[00:21:17]
what I arrived at was embracing these two things being different.
[00:21:21]
And I said, OK, I have fields and I have forms and they are different things.
[00:21:28]
And I'm essentially going to declare these fields in an applicative style.
[00:21:32]
So meaning, you know, you pipe things just like a JSON decode pipeline.
[00:21:38]
You can say required field, optional field, and it's applicatively building up this thing.
[00:21:44]
So you usually start with like succeed user and then you say required first string decoder,
[00:21:52]
required last string decoder.
[00:21:54]
You decode that into a user.
[00:21:56]
So similar approach.
[00:21:58]
You build up a form and you you pipe in all of these fields.
[00:22:04]
So you say with this field, with this field, with this field.
[00:22:07]
And if all of them pass their validation, because I'm guessing you're also passing the
[00:22:12]
validation functions, then you have something that it validates.
[00:22:17]
Right.
[00:22:18]
Right.
[00:22:19]
So that's that's the thing that turned out being really nice about this approach is now
[00:22:22]
you have a field that has this logic for how to parse something.
[00:22:27]
It's typed so you can you can map it.
[00:22:30]
You can say with a with a client validation that is actually going to transform the data
[00:22:35]
or could could fail and not transform the data.
[00:22:39]
And then it it stops the chain of validations and transformations.
[00:22:45]
So you can parse, don't validate, but you can build that up in a composable way, starting
[00:22:50]
with like field text.
[00:22:52]
And then you can even that could give you a maybe it could parse into a maybe string
[00:22:57]
and then you can pipe that into field required.
[00:23:00]
And then you can pipe that into field with validation and now you can transform that.
[00:23:08]
Or instead of starting with field text, you could start with field date.
[00:23:13]
And now it's going to parse into a date type or field time or these different core input
[00:23:18]
types.
[00:23:19]
We have field checkbox and it knows what that's going to parse into.
[00:23:22]
So you have that parser.
[00:23:24]
It knows how to present it.
[00:23:27]
So now you you have like your view function just receives all of those fields that you
[00:23:34]
piped in.
[00:23:35]
So now you're going to have your first name field, your last name field, your except terms
[00:23:39]
field, which is a checkbox field.
[00:23:43]
And now you render that and you just you just call, you know, render field function that
[00:23:50]
takes that field and it knows how to render it with the appropriate attributes.
[00:23:54]
But you can also pass in your own custom attributes.
[00:23:57]
But it's just a custom view function.
[00:23:59]
So you can render it into whatever type you want.
[00:24:02]
You can render it with whatever surrounding divs and context and HTML you want.
[00:24:10]
What if the field is not valid yet?
[00:24:13]
Right.
[00:24:14]
So if the field is not valid, that's OK, because you don't have a parsed like the checkbox.
[00:24:21]
It just has a raw value.
[00:24:23]
So the raw value at the end of the day, it's actually just a string, even if it's a checkbox.
[00:24:29]
So I'm confused.
[00:24:30]
So those functions, those presenting view functions, they take the parsed value or the
[00:24:37]
raw value?
[00:24:38]
They get the raw value.
[00:24:39]
Oh, OK.
[00:24:40]
Yeah, then which means that you can call that function whether or not each individual field
[00:24:46]
succeeded.
[00:24:47]
Right.
[00:24:48]
And because you need to, because you need to present a form before you have valid data.
[00:24:52]
So yeah.
[00:24:53]
Yeah.
[00:24:54]
So it's important to store the raw data, I feel like, because otherwise you get into
[00:25:00]
some weird interactions.
[00:25:04]
So maybe we mentioned this in a previous episode, but for instance, one case that is a bit problematic
[00:25:10]
is when you want to have an input for a number.
[00:25:15]
And you store it as a number in your model.
[00:25:18]
And then one of the problems is like if you, if the user empties the field, then that is
[00:25:24]
not a valid number anymore.
[00:25:26]
So what you do is you have, you show the default value, which is zero or something like that.
[00:25:32]
So then there's a zero that is inserted into the field, meaning that you can't just say
[00:25:38]
one something, you have to say 10 or things like that.
[00:25:44]
It gets weird.
[00:25:45]
There are so many pitfalls like that where something seems like a really good idea and
[00:25:50]
you try it and it doesn't work out.
[00:25:53]
And parsing things into valid data and storing that as your input seems like a good idea,
[00:26:01]
you run into dead ends like that.
[00:26:03]
Yeah, it kind of sucks.
[00:26:05]
Yeah.
[00:26:06]
But which is all the more reason that it really belongs in an API.
[00:26:10]
Because doing it, I mean, certainly you can, and I've, some people have shared some nice
[00:26:16]
looking hand rolled APIs for dealing with forms and you can get something pretty decent.
[00:26:24]
You can basically say, put an on change or on input handler with a, you could sort of
[00:26:32]
wrap something that puts like a, that you give it a setter and a getter at the end of
[00:26:38]
the day, you're saying, how do I get this field's current value out of the model?
[00:26:42]
And how do I set this field's value in the model?
[00:26:46]
And then you take that raw model, which stores the low level data, and you use that to parse
[00:26:54]
into some sort of result or something and you can present the errors and it works out
[00:27:00]
okay.
[00:27:01]
But what you lose is, you know, for one thing, something with opinions about how to present
[00:27:10]
errors keyed to certain fields and how to get the errors for a given field.
[00:27:16]
And there are these things you can abstract out that are really nice.
[00:27:19]
It's nice to abstract out things about how to present an individual field with the correct
[00:27:25]
HTML attributes and how to keep that in sync with the parsing logic like we talked about.
[00:27:32]
Obviously, I think that having an API to abstract these things is a really good idea.
[00:27:37]
So one of the things that I was looking at in serving these different form APIs out there
[00:27:44]
and comparing it to what I came up with is some of the APIs take this approach where
[00:27:50]
essentially somehow or other you're teaching the form API how to get the raw value from
[00:27:58]
the model and how to put it into the model.
[00:28:00]
You could do that by putting a message on each individual event and having a clause
[00:28:08]
in your update function to set that value.
[00:28:11]
Or some APIs will just say, hey, we're going to have the same message for all of these,
[00:28:17]
but give me a function that tells me how to set a value and how to read a value.
[00:28:25]
We'll use those functions in my message.
[00:28:28]
So at the end of the day, you're teaching it how to set the raw values and get the raw
[00:28:33]
values.
[00:28:34]
That's one approach.
[00:28:35]
Another approach is to essentially have just low level data.
[00:28:41]
So if you have, so a field has its raw value and it has the field state.
[00:28:49]
And I really like the approach of having basically unstructured data that's just key value pairs
[00:28:55]
as your source of truth, have your model manage that.
[00:28:59]
Then the form can completely take ownership over that.
[00:29:03]
And so like for example, the Itake Elm form, that package uses more of that low level approach
[00:29:11]
where essentially you give it the string key for how to get a field.
[00:29:18]
And then it can handle writing to that field or reading from that field.
[00:29:24]
But so it keeps track of this low level data, including the state if a field has been blurred
[00:29:30]
or changed, things like that.
[00:29:32]
So it can manage all of that state for you.
[00:29:34]
And the nice thing about that is you don't need to write getters and setters for every
[00:29:38]
individual field and handle all of that boilerplate.
[00:29:42]
And it can nicely handle all of those events for you.
[00:29:46]
So I really like that approach.
[00:29:49]
What I didn't like so much about the way that particular library does it is that it doesn't
[00:29:54]
take it all the way.
[00:29:57]
What I mean by that is a field has three different types of values it can have.
[00:30:03]
It can be an empty field, it can be a Boolean field or it can be a string field.
[00:30:08]
And so when you're...
[00:30:10]
In that API?
[00:30:11]
Yeah.
[00:30:12]
Okay.
[00:30:13]
So when you're rendering a field, you need to do form.getField as string or form.getField
[00:30:21]
as bool and then use that to render the value.
[00:30:24]
But why not just encapsulate those details and then just say get this field value.
[00:30:33]
And you might be thinking, wait, if you're saying get field value, don't you need to
[00:30:37]
pass it a string?
[00:30:38]
Which means that that's a string to keep in line.
[00:30:42]
You need to say get field first and you need to say set field first.
[00:30:47]
Or when you define that field, you need to say the name of that field.
[00:30:51]
So it can get out of sync.
[00:30:53]
And that's absolutely true.
[00:30:55]
My solution to that is this applicative pattern.
[00:30:59]
You can, you know, when you say like with field, you pipe your form with all these fields
[00:31:06]
that you're basically declaring.
[00:31:08]
When you say with field and then you say, you know, first and you give a field for a
[00:31:14]
text input.
[00:31:15]
The thing you get in the function that you're applying, the applicative starter function,
[00:31:21]
that first field that you have has a name and your functions for rendering a form field
[00:31:29]
can use that.
[00:31:30]
So, and you can even encapsulate it so that the only way to render using these input render
[00:31:36]
helpers is with this field type.
[00:31:39]
So you can't just pass in a string and get it wrong.
[00:31:41]
And you don't have to reach in and say first, you know, first field dot name.
[00:31:47]
You have to pass a field type to the input render helper and it knows how to take the
[00:31:52]
name off of that value.
[00:31:54]
So in that way, you define your source of truth for a field is you do have to name each
[00:32:01]
of your fields.
[00:32:02]
Yeah, which I think is a good practice for accessibility anyway.
[00:32:06]
That's true.
[00:32:07]
And actually it can help with things like a password manager can detect certain field
[00:32:13]
names and help you fill in information, which is like something I'm often frustrated by
[00:32:18]
when it doesn't begin.
[00:32:21]
Please people let my password manager fill information for me.
[00:32:25]
It's better at it than I am.
[00:32:27]
Yeah.
[00:32:28]
Yeah.
[00:32:29]
So this is just like a semantic form thing that you should probably do anyway.
[00:32:31]
As you say, I totally agree.
[00:32:33]
So the nice thing is you only have to define that name in one place and everywhere else
[00:32:38]
you're using this sort of field type that you pass in and now you've defined your source
[00:32:44]
of truth in one place.
[00:32:45]
So what do you mean with the field type?
[00:32:47]
Like that part is confusing me a bit.
[00:32:50]
So the field is, you can think of it as an opaque type that you can pass to a field render
[00:32:58]
function.
[00:32:59]
So you say, Hey, I have this first name field, please render it.
[00:33:02]
And this, the form API also gives you a function to render that field.
[00:33:06]
It knows the name of that field.
[00:33:09]
It has access to your form state.
[00:33:12]
And so it can just reach in and say, Hey, I'm rendering the first name field.
[00:33:15]
I need to go get that value from this raw unstructured data.
[00:33:21]
So you don't need to write setters or getters to put it in the right low level place.
[00:33:24]
You just can have a, you know, the form manage all of that state for you.
[00:33:29]
It reaches in and grabs it.
[00:33:31]
It knows the attributes to put on that input field, because you know, if you say field.password,
[00:33:38]
that information is on that field data type that you're using to render it.
[00:33:42]
So you can still customize those attributes, I'm guessing, right?
[00:33:46]
To make it look like however you want.
[00:33:48]
Yeah, exactly.
[00:33:49]
Yeah, that's right.
[00:33:50]
You can wrap it in some other construct or.
[00:33:54]
Exactly.
[00:33:55]
Literally all it is, is you get a helper function that takes a list of HTML attributes that
[00:34:00]
it will add on in addition to the baseline attributes it includes, like the value attribute,
[00:34:07]
the name attribute, the type attribute, if it's type password, type text, type telephone,
[00:34:12]
and then it will give you HTML.
[00:34:16]
And you render that HTML and you can put whatever you want around it.
[00:34:20]
You're just defining a function that renders HTML.
[00:34:23]
So I think that's pretty, pretty cool.
[00:34:26]
And so like in this API I'm working on, I'm defining the equivalent helper functions for
[00:34:33]
rendering an Elm CSS input element or an Elm HTML input element.
[00:34:38]
At the end of the day, it's actually the same thing.
[00:34:42]
But it's convenient to be able to add styled attributes to it if you're using Elm CSS.
[00:34:46]
But you could define a custom module to do that as well, right?
[00:34:51]
At the end of the day, it's just some data that knows what all of this information about
[00:34:56]
how to present a field and you could swap out a custom implementation.
[00:35:01]
So yeah, so I think that, I don't know, I think that that applicative pattern solves
[00:35:07]
this out of sync keys pretty well.
[00:35:11]
Like you no longer have to worry about, because I always feel a little strange when I'm doing
[00:35:18]
this like low level access where I'm getting essentially something from a dictionary that
[00:35:23]
I don't know if it's going to return something and my keys could get out of sync and you
[00:35:28]
know, that doesn't feel right.
[00:35:30]
As an Elm developer, we want to make those things a little more robust.
[00:35:34]
And I think this solves that pretty well.
[00:35:35]
Yeah.
[00:35:36]
Yeah.
[00:35:37]
I don't like putting things into a dictionary because I would, I like to look at my model
[00:35:41]
and see what fields are there, which can be quite hard to figure out with a dictionary.
[00:35:49]
Well, like a dict, I mean.
[00:35:51]
Yeah.
[00:35:52]
This approach, the source of truth for that is just, you know, you look at the bottom
[00:35:58]
of your form definition and it's just a series of pipelines of with field first and then
[00:36:06]
the definition of that field with field last with field except.
[00:36:11]
So that's your source of truth.
[00:36:12]
You look, so you still have one place to look and the form state is this opaque type.
[00:36:18]
It gives you things like whether you tried to submit the form.
[00:36:23]
And so that kind of brings me to another area I looked at kind of comparing the approaches
[00:36:29]
I came up with with these other packages out there.
[00:36:33]
So in looking at these other packages, there's another problem to solve, which is how and
[00:36:39]
when do you present error messages?
[00:36:42]
And so, you know, a common approach would be like, wait until you click the submit button
[00:36:47]
to present any errors.
[00:36:49]
At least.
[00:36:50]
I mean, you at least need to show the errors at that moment.
[00:36:54]
That's right.
[00:36:55]
That's right.
[00:36:56]
Exactly.
[00:36:57]
And otherwise you're going to confuse the hell of your users.
[00:37:01]
Yeah.
[00:37:02]
Otherwise you've sort of defeated the purpose of like painstakingly building up this validation
[00:37:07]
logic to present errors to users.
[00:37:09]
Tell me what is wrong, please.
[00:37:12]
You better show them when they hit submit.
[00:37:14]
And you probably don't want to show them errors as they're typing in most cases.
[00:37:22]
I mean, in some cases you might want to, in some cases you might want to wait until they've
[00:37:26]
at least tried to submit or until they've at least blurred the field, like entered into
[00:37:32]
focus on a new field.
[00:37:33]
At Arrima, our form library is pretty much only doing that actually.
[00:37:39]
It's tracking what has been touched and whether the submit button has been shown, pressed,
[00:37:45]
sorry.
[00:37:47]
And based on that, show the errors that you get through some configuration and a validation
[00:37:52]
library that is external to the form API.
[00:37:56]
That is basically what we do at Hemio.
[00:37:58]
I actually don't remember if we do any sort of parse to validate.
[00:38:01]
I guess we do, but not sure.
[00:38:04]
So I guess we don't.
[00:38:06]
But yeah, so our form API is basically just about showing the errors at the right moment.
[00:38:14]
And that gives us a lot of flexibility for the rest.
[00:38:16]
Right.
[00:38:17]
Yeah.
[00:38:18]
And I mean, at the end of the day, the needs of this API for Elm pages do differ slightly
[00:38:24]
from if you're, for example, just going to be encoding it into JSON that you're hitting
[00:38:30]
into an API, right?
[00:38:32]
Because if you're going to just be encoding it, then why bother parsing into all these
[00:38:38]
nice types?
[00:38:39]
It doesn't, unless you want to give like a real time preview of something or optimistic
[00:38:44]
UI, things like that.
[00:38:47]
Which can be handy, right?
[00:38:48]
So it really does depend on the specific needs of your application.
[00:38:52]
But it is nice if you can give it a nice experience for parsing data.
[00:38:58]
Even in how to show error messages, like I saw different approaches to this problem.
[00:39:04]
And one of the approaches is to just have a hard coded set of logic for when to display
[00:39:12]
it.
[00:39:13]
So if you've submitted, then show the errors.
[00:39:17]
Otherwise for everything.
[00:39:20]
And after you've submitted, then once the field gets blurred, show the new error, for
[00:39:27]
example.
[00:39:28]
You mean before the form has been submitted, show the errors for the field.
[00:39:33]
Once you've tried to submit, then changing errors after the field blurs, for example.
[00:39:41]
Yeah, I think what we do at Humeo, I think that's what we felt was nicer for UX and accessibility.
[00:39:50]
Don't show the errors when you're typing it.
[00:39:51]
But as soon as you blur it, or as soon as you've submitted the field, or the form, I
[00:39:57]
think, then update the error as you update the field.
[00:40:01]
Yeah, so my thinking on that was, so yeah, I saw the two approaches I saw to that problem
[00:40:09]
were, number one, just have an opinion and hard code that into the API.
[00:40:15]
And number two, have some sort of validation strategy that you can configure somewhere.
[00:40:20]
Yeah, which is what we did.
[00:40:23]
So what I ended up doing to solve that problem is I just have form state that I expose in
[00:40:30]
that view function that you define, where you get all of those fields that you can render
[00:40:35]
however you want.
[00:40:36]
And so in the form state, the form API I'm working on manages the state for whether you've
[00:40:44]
attempted to submit.
[00:40:47]
And also each field, you get the state for whether that field has been blurred or changed,
[00:40:57]
for example.
[00:40:58]
So between that kind of global form state and the individual field state, you can choose
[00:41:05]
to present errors based on that state.
[00:41:08]
That's the approach I've taken.
[00:41:09]
So you can just write like a little error rendering helper that renders it however you
[00:41:15]
want to render your errors.
[00:41:17]
In fact, your errors can be any type.
[00:41:19]
It doesn't have to be string.
[00:41:20]
There's like a type variable for your error type.
[00:41:23]
And you can present your errors however you want and whenever you want based on the form
[00:41:30]
and field state.
[00:41:31]
That's the approach I went with.
[00:41:33]
Seems like pretty flexible.
[00:41:35]
Yeah.
[00:41:36]
And then people can have an opinion saying, oh, wait, OK, we want to show the errors as
[00:41:40]
soon as it's been blurred or whatever.
[00:41:43]
Exactly.
[00:41:44]
Yeah.
[00:41:45]
It's totally customizable.
[00:41:47]
It's kind of like what you talked about the last episode.
[00:41:50]
You're building on top of a layer and you can only do whatever that layer allows you
[00:41:54]
to do.
[00:41:55]
So you make it more general and people can do whatever they want on top of that.
[00:42:00]
But they can only do whatever your platform is giving them.
[00:42:04]
Exactly.
[00:42:05]
Exactly.
[00:42:06]
At the end of the day, what I've really been thinking about with designing this form API
[00:42:13]
and considering other approaches is what opinions should I have about forms and what opinions
[00:42:21]
should I let the user have about forms?
[00:42:24]
And so I feel like the only ones that you can enforce are the ones that are proven to
[00:42:31]
be better accessibility wise and the rest you kind of have to let them choose.
[00:42:37]
Yeah.
[00:42:38]
There's also the question of if I have an opinion on this, can I do something useful
[00:42:44]
with it?
[00:42:45]
Something useful being improve accessibility, make it safer in terms of wiring or preventing
[00:42:52]
things from going wrong or getting out of sync.
[00:42:55]
More guarantees.
[00:42:56]
Always nice.
[00:42:57]
More guarantees.
[00:42:58]
So if you can't do something with an opinion, then why have it?
[00:43:03]
Right?
[00:43:04]
Yeah.
[00:43:05]
Yeah, at least as far as API design is concerned.
[00:43:09]
So for example, you know, if you have an opinion about errors, if you say fields can have errors,
[00:43:17]
using that opinion, you can abstract away details about associating an error with a
[00:43:22]
field because you have a concept of that in the API.
[00:43:25]
There's a cost to having that opinion, but it seems pretty reasonable.
[00:43:29]
If you have opinions about now the thing is the browser has certain opinions, so why not
[00:43:35]
take on those opinions in your API?
[00:43:37]
So if you know the browser has opinions about, you know, if you go to the MDN docs, it's
[00:43:43]
clearly listed out that there's like a finite set of currently available field input types.
[00:43:51]
So all right, let's have opinions about that.
[00:43:54]
We know what kind of data they parse into.
[00:43:56]
So using those opinions, we can abstract away some of those details, give you more guarantees
[00:44:01]
and safety.
[00:44:02]
So yeah, so that's been like a guiding principle is make sure that you get something out of
[00:44:07]
having an opinion and basically have opinions about things that the browser also has opinions
[00:44:13]
about.
[00:44:14]
Like the browser also has opinions about fields should have names.
[00:44:17]
Fields should be inside of a form.
[00:44:19]
So my API abstracts that away.
[00:44:22]
Fields should have labels as well.
[00:44:24]
Yeah, although actually I didn't, so far I haven't abstracted that away because I've
[00:44:30]
essentially said you can present labels however you want.
[00:44:33]
I guess I could have a simple helper function that says render a label for this, but there
[00:44:40]
are actually different ways you can render a label.
[00:44:43]
You can wrap your input in a label.
[00:44:48]
Or you end, in that case, you don't have to say what that label is for, or you can do
[00:44:53]
a label for an input element.
[00:44:55]
Yeah, so that one is going to be hard to, yeah, I guess you could have helpers that
[00:45:02]
lean one way or another.
[00:45:04]
Right, so you can have different opinions on that and what does the user really gain
[00:45:09]
from that opinion?
[00:45:11]
So I don't know, I might change my approach on that one, but so far my form API does not
[00:45:16]
have an opinion on that.
[00:45:17]
Yeah, no, I would at least start with not having one.
[00:45:20]
At least unless you come up with something nice, but otherwise leave it.
[00:45:25]
Yeah, and I think as far as accessibility goes, I haven't actually built this yet, but
[00:45:31]
I'm working on essentially if you try to submit a form, there are some accessibility considerations
[00:45:39]
for setting focus on the first field with an error.
[00:45:44]
That's going to affect the experience for a screen reader and also just bringing your
[00:45:50]
attention to that field.
[00:45:51]
Yeah, that makes a lot of sense.
[00:45:53]
I didn't know about that.
[00:45:54]
Yeah, so those are some things that again, like having an abstraction can do some of
[00:46:00]
that research for best practices for accessibility and have opinions where it seems like a reasonable
[00:46:07]
idea to come down on a strong opinion there.
[00:46:10]
But how do you know which field is first in your form?
[00:46:14]
Well, the way you build them up in the applicative pipeline, you just say, hey, I'm going to
[00:46:20]
assume that you're presenting it in the same order that you declared them in.
[00:46:24]
Yeah, that's an assumption, right?
[00:46:27]
Yes.
[00:46:28]
Or can you inform?
[00:46:31]
If your view doesn't depend directly that applicative, then it's going to be hard to
[00:46:39]
enforce, right?
[00:46:40]
Yeah.
[00:46:41]
No, you're right.
[00:46:43]
It gets a little bit odd there.
[00:46:48]
You also have to consider in practice, how much of an issue is that going to be?
[00:46:55]
How intuitive is it that you?
[00:46:58]
I can't believe that I'm hearing those words from your mouth, Dillon.
[00:47:02]
No, I mean, we talk about this a lot.
[00:47:04]
We should cover every error.
[00:47:06]
We're a library and framework authors.
[00:47:09]
We should do everything perfect for the user.
[00:47:12]
Yeah, I mean, if you take control over too much, then you can become rigid and take away
[00:47:18]
the user's ability to do things in a custom way.
[00:47:22]
I really strongly believe that the way you render the view should be pretty unopinionated.
[00:47:27]
Yeah, me too.
[00:47:29]
Maybe an Elm review rule if you're checking the support.
[00:47:31]
Yeah, I was thinking of Elm review.
[00:47:34]
But that also has some...
[00:47:36]
Yeah, maybe.
[00:47:37]
That would be a pretty easy one, right?
[00:47:40]
I don't know.
[00:47:42]
Well, I guess if you look at control flow, if they've abstracted it away to other...
[00:47:47]
Yeah, that's what I'm thinking as well.
[00:47:50]
We should try to enforce...
[00:47:52]
To make sure that things work as much as possible to the extent that it's possible.
[00:47:57]
And here it feels like it's a bit hard to do.
[00:48:01]
But I would say it's a bit scary to make an assumption that something is right.
[00:48:06]
Unless you write it in documentation, this has to be in the same order that you display
[00:48:11]
things.
[00:48:12]
Right.
[00:48:13]
Yeah, yeah.
[00:48:14]
And you do have to make certain trade offs like that.
[00:48:15]
But I definitely experienced...
[00:48:17]
Like I mentioned, my initial prototype did have this approach where you build up the
[00:48:23]
view and the parsing as you go along and a form and a field are the same thing.
[00:48:29]
And you sort of combine different sub forms together.
[00:48:34]
And in that scenario, you do know the order because you're just putting one after the
[00:48:42]
other.
[00:48:43]
And that's what I feel like a lot of forms in React land did.
[00:48:47]
But it also ties you up with how do you display things and how, when, and then you have a
[00:48:53]
new DSL, which is tricky to make things work with everything.
[00:48:58]
Like two bridges.
[00:49:00]
And it's not a nice way to, if you say I want to have a display group and I want to have
[00:49:08]
some visual thing on the side here.
[00:49:11]
And so now you're appending parts of the form that actually is just parsing to unit type.
[00:49:19]
And why is it parsing to unit type?
[00:49:21]
Well because it doesn't actually represent a field.
[00:49:25]
It's just a visual element and it's appending it.
[00:49:28]
But then you want to change your view and you want to put some other form elements next
[00:49:34]
to it.
[00:49:35]
And it just feels like the wrong abstraction for building up a view in this sort of composable
[00:49:42]
combinator way.
[00:49:43]
So it didn't work for me.
[00:49:45]
I wasn't feeling it.
[00:49:47]
So then there's another piece, which is how do you do dependent validations?
[00:49:54]
So you have password, password confirmation, you have a check in and a checkout date and
[00:50:00]
the check in must be before the checkout date.
[00:50:04]
Or you only have to check this field if this checkbox is checked, something like that.
[00:50:11]
Right.
[00:50:12]
Right.
[00:50:13]
So I came up with two pieces to help with this.
[00:50:16]
So first for the sort of dependent validations.
[00:50:20]
So I initially, you sort of alluded to how in this pattern I'm using for the view, if
[00:50:25]
one of the individual fields doesn't parse, how do you present that when it fails?
[00:50:32]
Because you need to show a view no matter what, whether everything is parsing correctly
[00:50:36]
or not, whether there are validation errors or not.
[00:50:38]
But you're showing, you're giving the raw values, right?
[00:50:41]
You're giving the raw values.
[00:50:43]
You actually can give like a value that might be parsed or might not be parsed, right?
[00:50:49]
And no, or not right.
[00:50:54]
Like I don't get it.
[00:50:55]
Sorry.
[00:50:56]
What do you mean?
[00:50:57]
What I mean is I cannot call a function whether or not the form is valid with a parsed value
[00:51:05]
because parsing might fail and then I can't call the function with a parsed value.
[00:51:11]
But if I say, hey, I'm going to parse this date field.
[00:51:16]
Here's a required date.
[00:51:18]
The type it parses into is not a maybe date, it is a date.
[00:51:21]
I'm going to give you, so I can't give you a date, but I can give you a possibly parsed
[00:51:27]
date value and you can check if it's parsed or not and you can use it if you want to.
[00:51:32]
So you're giving both the raw value and the parsed value?
[00:51:36]
Well usually you don't want to use the raw value.
[00:51:38]
So that was one of the things I was trying to avoid in my design is I didn't want the
[00:51:44]
user to ever have to deal with the raw value.
[00:51:47]
And that's one of the things in surveying these different form parsing APIs is that
[00:51:53]
I did see some APIs where the solution to these sort of dependent validations is, hey,
[00:51:59]
you can define something that gets access to all of the previous raw values and then
[00:52:05]
you have to sort of reapply your parsing logic and hope it doesn't get out of sync or hope
[00:52:10]
that it's fine to compare the raw value.
[00:52:14]
So you're talking about getting the parsed values in the end then or in the decoding
[00:52:20]
or validating?
[00:52:21]
Yeah.
[00:52:22]
I mean, you could have it in the view if you wanted to.
[00:52:25]
Yeah, I guess.
[00:52:26]
It's in case the user wants to do something with it, in case the user wants to...
[00:52:33]
Pan a match rather than reparse something.
[00:52:35]
Yeah.
[00:52:36]
You kind of like take a, I don't know, some value you decode it into and render it next
[00:52:44]
to the field.
[00:52:45]
And you say, hey, I was able to...
[00:52:47]
Not only was I able to parse this value, but you put the word tomorrow next to a field
[00:52:52]
because you've set it as tomorrow or you've set it as today or you put the text you're
[00:52:57]
checking in today because you selected today, you have access to that parsed date.
[00:53:03]
It's a possibly parsed date, but you check if it's there and then you have access to
[00:53:09]
that.
[00:53:10]
So you sort of may as well give access to that value in case you want to use it somewhere
[00:53:15]
in the view.
[00:53:16]
But you can also use it in a function for doing like dependent validations.
[00:53:22]
And so now, so at first my approach to this was I was only calling this function to do
[00:53:32]
these dependent validations if everything parsed correctly.
[00:53:36]
And then I can give you all of these parsed values and you can sort of do these dependent
[00:53:41]
validations and combine it into a nice type given everything that parsed into nice types.
[00:53:48]
And the problem with that is now if you're going through the form and you want to get
[00:53:54]
this nice error message next to your password confirmation that says this doesn't match
[00:53:58]
your password as you're typing, so you know whether or not you're good.
[00:54:02]
Now you only get it if you filled the rest of the form.
[00:54:06]
Exactly.
[00:54:07]
So you have something that is a required date field or just a required string or whatever
[00:54:13]
it may be.
[00:54:14]
Now it's not possible because it was unwrapping that maybe type when you did the required
[00:54:20]
or whatever validations you did for individual fields.
[00:54:23]
And if any of those have any problem, you don't get that error, which is not good, right?
[00:54:28]
So you should be able to have fine grade control over when you display these dependent validation
[00:54:35]
errors rather than being locked into some arbitrary thing that's based on convenient
[00:54:41]
parsing.
[00:54:42]
So what I came up with is a validation API that lets you take these like validated types
[00:54:51]
and combine them.
[00:54:52]
So you can say like validation.map2 and then you can take the password and the password
[00:54:58]
confirmation and you can do validation. and then you can add a validation error to an
[00:55:05]
individual field which takes the password confirmation and associates an error with
[00:55:11]
that field.
[00:55:12]
Again, it's type safe because you're not just passing it a string password confirmation
[00:55:16]
for the name of the field, but you're passing it a field.
[00:55:19]
So the only way you can associate an error is by giving it an actual field that you know
[00:55:24]
you're using on your farm.
[00:55:26]
So that's a lot of code.
[00:55:27]
We're talking about a lot of ideas here, but I'm pretty happy with how that turned out.
[00:55:34]
So now...
[00:55:35]
So just to get it right, because I'm not sure that I do.
[00:55:38]
If you use like map2, you're going to show the errors for every individual one of those
[00:55:45]
two things or map3.
[00:55:46]
Yes.
[00:55:47]
For every individual one of those three things, regardless of whether the other failed.
[00:55:50]
Exactly.
[00:55:51]
Okay.
[00:55:52]
If any individual field's validations fail, that error will always be shown, no matter
[00:56:01]
what.
[00:56:02]
Okay.
[00:56:03]
Okay.
[00:56:04]
Yeah, I think that makes sense.
[00:56:05]
But in addition, you can take all of those fields and you could say, hey, I need to check
[00:56:11]
these two fields to present possible errors.
[00:56:16]
And I don't care what other fields are parsing or not, but if I'm able to parse this password
[00:56:25]
and password confirmation, or if I'm able to parse this check in date and check out
[00:56:29]
date.
[00:56:30]
So meaning if you've entered a check in date and you haven't entered a check out date,
[00:56:35]
it's not going to run that.
[00:56:36]
Because if you do validation.map2 with your check in and check out, well, one of them
[00:56:44]
doesn't parse into a date.
[00:56:46]
It's not going to run that dependent validation.
[00:56:47]
But if you do a dependent validation on those two, and now you have two dates, you can check
[00:56:55]
that the check in date is before the check out date.
[00:56:58]
And it will run those assuming that those two fields parse, but it doesn't care about
[00:57:03]
the rest of the form.
[00:57:04]
All right.
[00:57:05]
Yeah, that's pretty nice.
[00:57:07]
And then you can use all of that to build up the final parsed value.
[00:57:10]
So if you want to parse, don't validate into a nice type, you say, hey, we don't need the
[00:57:16]
password confirmation for the parsed value.
[00:57:19]
We just need the password because that is the data type we want to parse into.
[00:57:23]
But we're only going to parse into that if there's a matching password, you can do that.
[00:57:28]
And in fact, you could even parse in, you could even recover and parse into a type by
[00:57:35]
defaulting values if any of the individual fields failed to parse.
[00:57:42]
And that's nice because you might want to like show a preview or optimistic UI or well,
[00:57:48]
not optimistic UI if there's not an in flight submission because it's not valid.
[00:57:52]
You're not going to have an in flight submission, but you could render a preview of something
[00:57:58]
while something is so you're updating some, you're creating a new product listing and
[00:58:03]
you can show a real time preview of that product listing.
[00:58:07]
Even though you didn't enter a price and the price is required, you can default it to zero
[00:58:14]
or and show that in the product listing.
[00:58:17]
Okay.
[00:58:18]
So you can have a default that's still forbid it from being parsed for real.
[00:58:24]
Exactly.
[00:58:25]
Okay.
[00:58:26]
So there's a few layers to this.
[00:58:29]
There's a parse value.
[00:58:32]
There's a parse value with defaults with fallbacks.
[00:58:36]
There's validation errors and there's things that are not, have not been filled yet or
[00:58:43]
can't be parsed.
[00:58:44]
Yeah.
[00:58:45]
And the parse values with fallbacks are actually all it is, is it's just this kind of combining
[00:58:54]
function tells you how to like parse given all of the individual fields.
[00:59:00]
You can check if they have any errors and you can combine them together.
[00:59:06]
If that whole process succeeds where there were no errors on individual fields and you
[00:59:11]
didn't add any dependent errors in addition to that in that function, then you successfully
[00:59:17]
parse a value.
[00:59:18]
But you can still parse into a value even if you added errors associated with fields.
[00:59:25]
And so you can say, you know what?
[00:59:27]
Yes, I want to present all of the errors with individual fields, which your view function
[00:59:31]
already does.
[00:59:32]
But I also want to show whether or not there are errors.
[00:59:36]
I want to get a parse value if I can so I can show a preview.
[00:59:40]
So you can still do that.
[00:59:42]
Now in the this Elm pages API I'm working on for receiving the incoming request.
[00:59:51]
So you submit the actual form data.
[00:59:53]
It receives the form data.
[00:59:55]
You reuse that same form parser function and you just don't want to get that value at all
[01:00:02]
if there are any errors or anything went wrong.
[01:00:04]
So in that scenario where you didn't give a price for a new inventory, it's like I don't
[01:00:11]
want to parse into data.
[01:00:13]
I just want to immediately give an error and say, no, this isn't going to work.
[01:00:18]
So now I feel like the question is, are you going to publish that as a separate package?
[01:00:22]
Yeah, I have.
[01:00:23]
I've gotten that question and I have been thinking about that.
[01:00:26]
I certainly I'm pretty happy with some of these designs I came up with for this API
[01:00:34]
and I want to shake things up.
[01:00:37]
I would love to like influence the way we approach form handling in Elm.
[01:00:43]
And so I don't know, I haven't decided yet what that's going to look like if it's just
[01:00:50]
writing some posts about my findings and sharing the Elm pages form API I build, or if it's
[01:00:57]
going to look like me building a separate vanilla form parsing API that's independent
[01:01:04]
of Elm pages, or if the two will somehow be connected that I'll have like my own pages
[01:01:10]
form parsing API use my vanilla one and build off of it or something.
[01:01:15]
So those are different paths I'm considering.
[01:01:17]
But I hope that maybe I can sway some opinions about some best practices for handling forms
[01:01:24]
in Elm.
[01:01:25]
Yeah, let us know if you want to have this as package or...
[01:01:29]
Yeah, I might just play around with it to see how that feels.
[01:01:34]
And I mean, if nothing else to showcase my ideas about forms in a way that you can easily
[01:01:40]
share on an Ellie and play around with.
[01:01:42]
So yeah, so the Elm pages form API does have like, like I mentioned, certain coupling to
[01:01:50]
Elm pages features like data sources for doing server side validations.
[01:01:56]
And it also hooks into state.
[01:01:59]
So the Elm pages framework manages the state for you.
[01:02:03]
So we talked about using low level form state, but the Elm pages framework, this new version
[01:02:09]
will actually keep track of the client side form state for you.
[01:02:14]
So you don't need to wire anything into your model and it can hook into that.
[01:02:17]
And it can keep track of in flight submissions.
[01:02:21]
And you can use it so you can run your same form parser on the in flight submissions.
[01:02:27]
What is it?
[01:02:28]
In flight submission.
[01:02:29]
If you press the submit button, it's valid, it's sending it to the server.
[01:02:34]
That's an in flight submission.
[01:02:35]
It hasn't come back with a response yet, but you are submitting a new product.
[01:02:40]
So you know, if you're submitting a new item to your to do list, you can show optimistically,
[01:02:47]
you can show that new to do list item, or if you're deleting a to do list item, you
[01:02:51]
can optimistically gray out that to do list item.
[01:02:56]
So you can reuse that same parser for the in flight requests, the client side requests,
[01:03:02]
like the client side state and the server incoming server request parser.
[01:03:08]
So yes, it is going to be like coupled to those things.
[01:03:12]
But we'll see maybe I'll maybe I'll build a version that's decoupled from that as well.
[01:03:16]
I do feel like forms are one of those areas where it is nice to have an abstraction to
[01:03:22]
have a consistent thing across your application.
[01:03:26]
But it is something that if you want to get started, you can just wing it, build it yourself
[01:03:32]
from scratch every time.
[01:03:34]
So if you unless didn't build a really great package for everyone to use with or without
[01:03:43]
some pages, I think that if you want to get started, you should probably be build your
[01:03:48]
own thing.
[01:03:50]
The only thing I'm wondering about is accessibility.
[01:03:53]
Like if you have a package that makes accessibility better, which is going to be hard to think
[01:03:59]
about yourself, like all the best practices, then maybe it's worthwhile.
[01:04:03]
But otherwise, I feel like, yeah, just to get started, build your own version, none
[01:04:08]
of a general thing.
[01:04:10]
But like you have this form, just do a manual model and update and view.
[01:04:15]
Yeah, I mean, you could definitely start there.
[01:04:17]
I mean, if you're not doing anything too complex, but if the problem then becomes if there are
[01:04:24]
many places where things can get out of sync.
[01:04:29]
So like how many places do you have to define logic that you could get wrong?
[01:04:35]
How many points of failure are there for things like defining your validation logic?
[01:04:42]
Could you forget to run a validation for a field?
[01:04:45]
Could you forget to show the errors?
[01:04:48]
Could you show the errors for the wrong field?
[01:04:51]
Exactly.
[01:04:52]
Could you set a wrong field?
[01:04:54]
Could you forget to wire in a setter?
[01:04:57]
Absolutely.
[01:04:58]
Yeah.
[01:04:59]
Could.
[01:05:00]
But in practice Dillon, like how often does this happen?
[01:05:03]
Really?
[01:05:04]
And also it's like how much cognitive load is it taking up as you're working on these
[01:05:10]
things, making sure you didn't get any of these details wrong.
[01:05:13]
So I personally have convinced myself that we can build a pretty nice abstraction using
[01:05:23]
low level form state and an applicative pipeline.
[01:05:27]
And parse don't validate.
[01:05:29]
Parse don't validate.
[01:05:30]
I also didn't mention like, but the individual field API for building up a field that's composable,
[01:05:37]
you start with field.text and then you pipe that to field.required.
[01:05:42]
You can pipe that to field.withvalidation.
[01:05:45]
That uses a phantom builder API, which works very nicely for that use case because you
[01:05:50]
know, you, yeah.
[01:05:52]
So I'm very happy with that.
[01:05:54]
I'll share links to these preview APIs that people can take a look at if they're curious
[01:05:58]
to see more details.
[01:06:00]
But I feel like this is a combination of a lot of things that we've talked about.
[01:06:05]
There are big types, build a pattern, applicative, phantom types, using the platform.
[01:06:13]
Exactly.
[01:06:14]
It really is.
[01:06:16]
Yeah.
[01:06:17]
So I've convinced myself at least that I think there's a nice abstraction we can build here.
[01:06:23]
I hope I have convinced some others, but I'll have to put it out there and let people get
[01:06:29]
their hands on it and see what they think.
[01:06:31]
It definitely sounds interesting to me, but I'm not sure I could build it right now with
[01:06:37]
my understanding at least.
[01:06:38]
So yeah, either you're going to have to write about it or publish it for me to try it out.
[01:06:44]
There were a lot of details that were really tricky to figure out.
[01:06:51]
Even if future me went in a time machine and told me, and I listened to this episode about
[01:06:58]
these sort of design considerations and dead ends, of course, it's hard to learn those
[01:07:03]
lessons without trying it yourself.
[01:07:06]
But also...
[01:07:07]
But it was yourself from the future.
[01:07:10]
That's true.
[01:07:11]
What are you talking about?
[01:07:15]
Time travel, kind of like a time traveling debugger.
[01:07:17]
But I still had a lot of really tricky puzzles to solve.
[01:07:24]
One of the things that I will say is definitely challenging with using this API is the types
[01:07:30]
can be tricky.
[01:07:32]
As you know, working with phantom builder pipelines, it gives you error messages for
[01:07:38]
nice things, but it can be tough to figure out how do I make this API happen.
[01:07:45]
It can feel a little cryptic.
[01:07:47]
And also, if you're building...
[01:07:49]
So there's some things that are similar to the mini builds alam codec API for building
[01:07:55]
custom type codecs.
[01:07:58]
It's using that kind of API?
[01:08:01]
Yep.
[01:08:02]
Okay.
[01:08:03]
Yeah, where you have this sort of applicative thing where you declare each of the fields
[01:08:08]
and then you use those in this function, right?
[01:08:11]
And...
[01:08:12]
Right.
[01:08:13]
Yeah.
[01:08:14]
It's a bit tricky, but the error messages can be very cryptic.
[01:08:20]
And even for a very seasoned Elm developer, it can be hard to know what went wrong if
[01:08:28]
you mess something up in terms of the error messages.
[01:08:31]
But once it compiles...
[01:08:33]
It works.
[01:08:34]
It works.
[01:08:36]
So there's definitely a trade off there, but I feel pretty good about the set of trade
[01:08:40]
offs.
[01:08:41]
I mean, you're rarely going to say, well, the API is great, but I'm going to just drop
[01:08:51]
everything because the error messages are a bit hard to read.
[01:08:54]
Like yeah, totally.
[01:08:57]
I mean, if we can improve the error messages, definitely.
[01:09:01]
But do it.
[01:09:02]
But yeah, there's never going to be a...
[01:09:05]
Well, not never, but...
[01:09:07]
Yeah, if the API works well, then that's what is most important.
[01:09:12]
It would be interesting to know whether we could have configurable errors, like tell
[01:09:17]
the compiler, like, hey, if we have an error message with this phantom type, then show
[01:09:24]
this kind of message to tell people how to...
[01:09:28]
Interesting.
[01:09:29]
Like give links to examples or something or to an FAQ.
[01:09:32]
Whoa, that would be cool.
[01:09:35]
It sounds like an amazing idea actually.
[01:09:37]
Yeah, that's definitely a challenge.
[01:09:38]
And it's one that I run into, like when I'm building codecs using Elm codec, it's like
[01:09:47]
when I build it and it's compiling, I'm pretty convinced that it's going to work and do what
[01:09:51]
I expect.
[01:09:53]
But when I'm building it up, if I get an error, like I basically have to be very careful to
[01:09:57]
incrementally build it up and have it in a working state at each step.
[01:10:01]
And often I use like debug.to do.
[01:10:05]
Because like if you forgot an argument in that Lambda that you have for every field
[01:10:11]
in your codec, the error messages would not help you understand that that's what happened
[01:10:18]
at all.
[01:10:19]
Yeah.
[01:10:20]
So build your...
[01:10:21]
Build it one field at a time.
[01:10:23]
Yeah.
[01:10:24]
Yeah, build it one field at a time.
[01:10:25]
And I'll often just like put debug.to do somewhere.
[01:10:28]
So it's just like, all right, I want to focus on this part of the error now, get that right,
[01:10:33]
and focus on the next part of the error.
[01:10:35]
We haven't described how the Elm codec API looks like, but I feel like this is not the
[01:10:40]
time for it.
[01:10:42]
Yeah.
[01:10:43]
Well, we do have an episode on codecs and we'll drop a link to the docs as well with
[01:10:49]
a good example there.
[01:10:51]
Yeah.
[01:10:52]
Well, this may be our wonkiest episode yet.
[01:10:55]
We really got into a lot of nerdy API design details here.
[01:10:59]
I feel like the fancy build pattern was also quite wonky.
[01:11:03]
So...
[01:11:04]
True.
[01:11:05]
True.
[01:11:06]
But at the end of the day, hopefully all these wonky details go to serve a better developer
[01:11:14]
experience.
[01:11:15]
So...
[01:11:16]
Yep.
[01:11:17]
I'm looking forward to a blog post or a package, Dillon.
[01:11:20]
Yeah.
[01:11:21]
I'll work on that.
[01:11:23]
Yeah.
[01:11:24]
For the release of this episode, please.
[01:11:26]
I'll do my best.
[01:11:28]
I've actually been thinking about doing a blog post comparing some concrete examples
[01:11:35]
of these different approaches as well.
[01:11:37]
Because I think it's a good exercise in API design and also for posterity, it's nice to
[01:11:43]
know what kind of tradeoffs there are for these different form API approaches.
[01:11:49]
Yeah.
[01:11:50]
Well, until next time.
[01:11:52]
Until next time.