spotifyovercastrssapple-podcasts

elm-form

We discuss the power of decoupling from data types using low-level data, and how dillonkearns/elm-form gives you simpler wiring that feels like magic but isn't.
April 24, 2023
#81

Transcript

[00:00:00]
Hello Jeroen.
[00:00:02]
Hello Dillon.
[00:00:04]
Well, we talked about exploring a new API design for forms a while back, but since then
[00:00:12]
I would say I've perhaps formed some new opinions.
[00:00:15]
Did you?
[00:00:16]
Did you?
[00:00:17]
I just wanted to inform you.
[00:00:19]
Oh, thank you.
[00:00:20]
That's very nice of you.
[00:00:22]
Informally, of course.
[00:00:23]
Okay, how am I supposed to react to this, man?
[00:00:26]
You don't need to mutate at all.
[00:00:32]
It's okay.
[00:00:33]
What?
[00:00:34]
What's that mutation joke coming out of the blue?
[00:00:37]
You just said react, so I felt obliged to say something to defend Elm's honor.
[00:00:42]
So yeah.
[00:00:43]
Well, I guess in my view, this is how people should react.
[00:00:52]
You don't want to start a flame war or even start at the tiniest ember, right?
[00:00:58]
I just want to give people an update.
[00:01:00]
That's all.
[00:01:01]
That's all I'm saying.
[00:01:02]
All right.
[00:01:03]
That sounds good.
[00:01:04]
In it?
[00:01:05]
So yeah, we did.
[00:01:06]
Moving along, we did have an in-depth discussion about designing a new form API.
[00:01:20]
And at the end of this episode, we'll link to the episode in the show notes, you asked
[00:01:25]
me if I was planning to create a standalone version of this form API.
[00:01:29]
At the time, I wasn't sure whether or if I could or would.
[00:01:36]
And a few more people asked me, and it turns out I did it.
[00:01:42]
I haven't quite hit publish.
[00:01:44]
Actually, I might do it before this episode goes live.
[00:01:47]
I think I will, but I did it.
[00:01:50]
I extracted a standalone form API.
[00:01:52]
So all of these ideas we talked about are no longer coupled to Elm Pages.
[00:01:58]
And it turns out it worked out pretty well.
[00:02:00]
Cool.
[00:02:01]
So I'm guessing that simplifies Elm Pages API quite a lot.
[00:02:05]
It removed several modules from the API doc sidebar, which was nice.
[00:02:11]
It's already quite large.
[00:02:12]
Well, I guess you still have to learn them, but they're just in a different package.
[00:02:17]
You have to learn them, but it provides an incremental migration path, which is nice
[00:02:22]
if you wanted to change over from a different framework or vanilla Elm over to Elm Pages.
[00:02:30]
And it also gives you the ability to create little standalone shareable LE snippets because
[00:02:34]
you don't need a framework to use this form API.
[00:02:37]
So yeah, they turned out really nicely.
[00:02:40]
So what did you end up doing to make this work, to extract this to a separate package?
[00:02:46]
How did that impact Elm Pages API?
[00:02:50]
And is your form package influenced in any way with how it will interact with Elm Pages?
[00:02:57]
Yeah, you know, it's interesting.
[00:02:59]
At first, I wasn't sure whether some of these ideas would translate to a standalone tool.
[00:03:05]
But after I've gone through this process, I've gotten a little more clarity on it.
[00:03:09]
And I think they do translate.
[00:03:11]
But really, the core idea is this opinion that form data should be managed for you.
[00:03:20]
And you should defer parsing it into higher level data or errors to a later stage.
[00:03:27]
Because when people are, you know, designing things that prematurely parse, you end up
[00:03:33]
with structured data or like semi structured data in your model.
[00:03:38]
And you what I realized is that by doing that you're coupling to your specific app, because
[00:03:47]
you're coupling to those data types like your, this is one of the really interesting things
[00:03:51]
in in Elm is it kind of makes some of these types of coupling more clear, because you
[00:03:56]
have to be so explicit, you know, not just in what, what state you depend on and things
[00:04:01]
like that, of course, but also what types you depend on.
[00:04:06]
Because you can't just say, Oh, I'll take any type, and I'll check if this type is there.
[00:04:10]
And I'll do these special things with these types.
[00:04:12]
You know, you can have like, you can have a type variable, or you can have a concrete
[00:04:17]
type, but you you can't cast things and you can't check what the type is.
[00:04:24]
And so, so what I what I realized is that when, you know, it's a type of coupling, if
[00:04:33]
you have like a form field, and you have a message for changing that form field, you
[00:04:38]
know, for example, check in date changed, if you have a message for that, and then you
[00:04:45]
have some maybe date, because it parses into a maybe date, you've coupled that to this
[00:04:52]
type in your domain.
[00:04:54]
The reason is a problem is because by coupling to that, you don't have a way to abstract
[00:05:00]
things away from that.
[00:05:03]
So like, you know, when you couple to something, you have to deal with it as a special case.
[00:05:10]
But if you decouple from data, and you just say we have low level data, like all we have
[00:05:14]
is key value pairs, then you just store all of that as a single entity.
[00:05:20]
So this is just form data.
[00:05:22]
It's totally unstructured, it's totally untyped, it's just strings, which at the end of the
[00:05:26]
day, that is like, you know, even if you have a number input field, you can be in a state
[00:05:34]
where it's invalid, you can type 1.2.
[00:05:37]
And when you do one dot, that is a string, which is not a valid number.
[00:05:43]
So you need to be able to represent strings at the end of the day, they're all strings.
[00:05:48]
To kind of summarize the previous episode, like Elm Form is a lot about handling things
[00:05:53]
at the very low level, like keeping things in a low level data state, just pairs of strings
[00:06:02]
as a list.
[00:06:03]
So for each field, you will have a value, and the name of the field, and a list of those
[00:06:09]
is just the form data.
[00:06:12]
So that's what you use as the low level data.
[00:06:15]
And you will never transform it to anything else until you actually try to parse it, which
[00:06:21]
you do when you submit it, basically.
[00:06:25]
When you try to submit it.
[00:06:27]
Yes, and also when fields change, when there's a form field event that changes a field, then
[00:06:35]
you present real time client-side validation errors.
[00:06:38]
So you're basically kind of putting these two pieces together, but they're completely
[00:06:44]
decoupled.
[00:06:45]
So you've got low level form data, and you've got a parser.
[00:06:49]
And those two things, the low level form data doesn't know or care about the parser.
[00:06:55]
So it's completely decoupled from it.
[00:06:58]
It is a completely agnostic data format.
[00:07:01]
That's low level, it's not coupled to domain types.
[00:07:03]
The parser does know about your domain types, it parses into whatever types you want to
[00:07:08]
parse into.
[00:07:09]
But by separating those two pieces, the form API can completely take charge of managing
[00:07:16]
all of your form state, including not just the values for your form fields, but the field,
[00:07:24]
the term I'm using for it is field status, but like whether something has been blurred
[00:07:28]
or changed.
[00:07:30]
So all of that low level form state is completely managed by the API.
[00:07:35]
So the end result is you, so the wiring is you have to use form.init in your model to
[00:07:45]
init it, you have to have a form.model in your model, you have to have a form.message
[00:07:55]
in your message, and you need to have an update clause that delegates to form.update.
[00:08:03]
And then you need to render in your view.
[00:08:06]
And that's it.
[00:08:07]
And now that might sound like, okay, that's just like what we're used to doing with Elm
[00:08:13]
packages.
[00:08:14]
Yeah, that's basically the Elm architecture.
[00:08:16]
Right.
[00:08:17]
Like to call it, right?
[00:08:19]
Exactly.
[00:08:20]
The triplets, the Elm triplets.
[00:08:22]
But this is not the pattern that we're used to using for form packages.
[00:08:27]
The pattern that we're used to for form packages is wiring in messages and model fields for
[00:08:34]
every single field.
[00:08:36]
So this simplifies that.
[00:08:39]
So it is a single form.model value for all of the forms you might have on a given page.
[00:08:47]
So if you have multiple forms that you could submit on a page or even throughout your pages.
[00:08:53]
So this is one of the really cool things.
[00:08:54]
You know, if you have, you can wire up this form state, you know, in your shared model
[00:09:01]
between pages and maintain it in a single place.
[00:09:04]
So that was one of the reasons why I wasn't sure if this pattern would translate from
[00:09:09]
Elm pages into a more general use case was because Elm pages had this built in assumption
[00:09:16]
where it had, so it managed the model for the user, right?
[00:09:22]
So Elm pages as a framework can have its own model and then the user's model sits within
[00:09:28]
its model, right?
[00:09:29]
And so it can manage the form state, which is the state for any form.
[00:09:36]
And then it can have its own message.
[00:09:39]
So the Elm pages framework can have its own message, which knows about these form events.
[00:09:44]
So all you have to do is render a form and it renders with this pages message type, you
[00:09:51]
know, HTML pages message.
[00:09:53]
And then the framework knows how to deal with that message.
[00:09:56]
So you don't, that means that you can have a form with real time client side validations
[00:10:03]
that dynamically manages all the form state without needing to call form.init, form.update,
[00:10:10]
have a message for it.
[00:10:12]
All you do is render your form and the framework just takes care of it.
[00:10:15]
So it's, it almost feels like magic when you're using JavaScript and you just wire up a component
[00:10:21]
and it just knows how to use this global state.
[00:10:25]
But it's actually not magic.
[00:10:27]
And if you look at the time traveling debugger, you can see all these explicit messages that
[00:10:31]
are being received.
[00:10:32]
You can still use the time traveling debugger.
[00:10:35]
It's not magic.
[00:10:36]
It's just a baked in opinion.
[00:10:37]
So the magic is that it's decoupled from your type because it doesn't need to know what
[00:10:45]
type your form parses into to take ownership over that form state because it knows what
[00:10:51]
form state looks like.
[00:10:52]
It's just key value strings and blurred, changed, et cetera.
[00:10:57]
Right.
[00:10:58]
So it feels magic because you don't have to wire it in yourself.
[00:11:02]
And the reason why you don't have to wire it yourself is because the types are known
[00:11:08]
because there are those low level data, right?
[00:11:11]
So it's a list of string tuples.
[00:11:15]
Whereas if my form was something that I made myself with the custom types and custom updates,
[00:11:22]
custom messages and so on, then I would have to wire it in somehow.
[00:11:28]
Right.
[00:11:29]
Exactly.
[00:11:30]
In this case, I just have to tell you where it is and that's it.
[00:11:34]
Basically right.
[00:11:35]
In this case, in an Elm pages v3 application still in beta, you say, you know, you say
[00:11:44]
render HTML for the form and you give it a form ID.
[00:11:48]
And that form ID is the unique key that it's going to manage in the dictionary of the form
[00:11:54]
state for all forms in the app.
[00:11:57]
I'm guessing you have an ID so that two forms with the same fields don't conflict.
[00:12:03]
Like if you have a first name in two different pages, they're not using the same data under
[00:12:08]
that.
[00:12:09]
Exactly.
[00:12:10]
So that, you know, that is a constraint of how this package operates is it does operate
[00:12:15]
under the assumption that you're using, you're giving unique form IDs.
[00:12:21]
Actually, I think this would be a really good review rule to check that you have unique
[00:12:26]
form IDs.
[00:12:27]
I was even thinking like, so you can check one, one cool way to check this.
[00:12:34]
It would be kind of interesting to check that like you could by convention check that the
[00:12:40]
declaration name of a form matches the string that you use for the unique identifier.
[00:12:46]
But you still need the ability to make something unique if you render like a delete button
[00:12:52]
for every item or or a quantity, you know, quantity field for every item in the cart.
[00:13:01]
Those each need to have a unique form ID.
[00:13:03]
So in that case, but it would be possible to just scan the page with Elm review and
[00:13:09]
say this needs to have a unique form ID.
[00:13:12]
And if you do, for example, list dot map for each item in the cart, you should use something
[00:13:18]
from that list dot map in your unique ID to to ensure that it's unique.
[00:13:24]
Sounds like react where they tell you, hey, don't use the index as the key for this in
[00:13:29]
this loop.
[00:13:30]
I'm slightly confused here is the form ID for the field or for the form.
[00:13:37]
I thought it was for the form.
[00:13:39]
So that you differentiate between two forms on different pages or different sections of
[00:13:44]
the page.
[00:13:45]
The the form ID is for the form.
[00:13:49]
And then you also have to give a name for each field that's unique within that form.
[00:13:56]
Okay, so you would like to have something that checks that the form IDs are unique,
[00:14:02]
and that for each form, the names are unique.
[00:14:05]
Right.
[00:14:06]
And that would be a best practice just in general for accessibility to have it.
[00:14:12]
Yeah, for accessibility purposes to have a name for each form field is a best practice.
[00:14:18]
In the case of the form, it actually doesn't matter if you have a unique ID if you're only
[00:14:24]
rendering one form on the page.
[00:14:25]
But that's just one requirement that this API has.
[00:14:30]
I think it's a fairly reasonable one.
[00:14:32]
But it would be nice to have either the framework or Elmer view check for whenever you misuse
[00:14:40]
that.
[00:14:41]
Yeah, and that would be very nice.
[00:14:42]
And it's a bit hard to tell which one is more suited because each one will have its own
[00:14:48]
pitfalls and things that it can check and things that it can't check.
[00:14:52]
So a bit hard, but yeah, definitely that would be useful.
[00:14:56]
Yeah, so originally, I wasn't sure if that same pattern would translate outside of Elm
[00:15:04]
pages or if it would even be interesting because this whole core value proposition was manage
[00:15:11]
low level state so that you can simplify the wiring.
[00:15:15]
So you have this magic trick that it's still regular Elm code, but you can just say, render
[00:15:23]
form, give it a unique ID.
[00:15:26]
And now you have real time client side validations for this form parser you defined.
[00:15:31]
And it's like giving you the errors on the fly and you haven't added anything to your
[00:15:36]
model or, you know, all you did is render something in your view.
[00:15:40]
It turns out, I think it's still pretty useful.
[00:15:43]
One of the reasons I think it's useful is because I extracted it out in such a way that
[00:15:49]
you can do that same pattern in your own application if you want to.
[00:15:54]
So you know, I mean, other frameworks could integrate this in the same sort of pattern
[00:16:00]
that Elm pages does.
[00:16:02]
But in your own code base, you could have a single sort of shared model between pages
[00:16:08]
that has all form state and you could have like a shared message.
[00:16:13]
I'm not sure if people use this pattern very commonly.
[00:16:16]
I'm not sure I had seen it before I kind of came up with this for Elm pages.
[00:16:20]
Have you seen this before Jeroen?
[00:16:22]
Of having a sort of framework message and wrapping the page specific messages in the
[00:16:30]
sort of app message?
[00:16:31]
I don't know if I've seen it in packages.
[00:16:34]
I definitely use it at work on something Elm SPA like.
[00:16:38]
I was going to wonder like, do I do that for Elm Review?
[00:16:40]
But no, Elm Review doesn't use messages at all.
[00:16:43]
But at least not in the API.
[00:16:45]
So no, I do use this in a work project, but I don't know if I've seen it in packages.
[00:16:53]
Nice.
[00:16:54]
Yeah.
[00:16:55]
So then, for example, in your code base or other code bases that use that same pattern,
[00:17:00]
you can get the same ergonomics and the same sort of magic trick that Elm pages does where
[00:17:06]
you just create an app level message and each page doesn't have to worry about wiring up
[00:17:14]
that form state.
[00:17:15]
And I have to say, I used to find it to be a bit of a nightmare working with forms in
[00:17:21]
Elm.
[00:17:22]
I used to kind of dread wiring up every single message every time.
[00:17:26]
And this is a big relief.
[00:17:27]
I think, correct me if I'm wrong, but the reason why it works so well for you is because
[00:17:34]
you're using code generation to wire that up partially.
[00:17:39]
Right.
[00:17:40]
Yeah.
[00:17:41]
So that's what I'm doing at work as well.
[00:17:43]
And that only works if you know how something will look right.
[00:17:47]
You can only code generate things that you're aware of.
[00:17:51]
So as you said, if something is opinionated or there is some convention around the practice,
[00:17:57]
then you can make nice tools around it or automate boilerplate writing.
[00:18:04]
So yeah, doing something conventional or standard or really helps with having a nice experience.
[00:18:10]
But I think that's partially the reason why things like Ruby took off because they had
[00:18:15]
so many things around convention.
[00:18:17]
You correct me if I'm right, because I don't know Ruby at all.
[00:18:21]
But since everyone did things in a specific way, people can make tools that assumed or
[00:18:27]
presumed that things were done that way.
[00:18:30]
And then you can make very nice tools.
[00:18:33]
That's also kind of what we do with Elm in a lot of ways.
[00:18:37]
We know that none of our functions will have mutations and all those kinds of things.
[00:18:46]
And we play with that.
[00:18:47]
The only limitation is that we still need to resolve the types in a way that makes everything
[00:18:52]
work together.
[00:18:53]
But apart from that, we kind of do the same with our tooling.
[00:18:58]
And that's why we have a lot of awesome tools.
[00:19:01]
Right.
[00:19:02]
Yes.
[00:19:04]
The thing with Ruby is that it has a lot of capacity for metaprogramming and patching
[00:19:10]
in things.
[00:19:11]
And I think that is one of the things that made it successful.
[00:19:15]
I mean, Rails was sort of built on a lot of those features, being able to use method missing
[00:19:21]
and just patch in methods that made things ergonomic and gave you conventions for being
[00:19:29]
very productive.
[00:19:30]
And I think that's been sort of like a challenge in Elm is like when we have to be so explicit
[00:19:36]
and this programming language is not, it's the opposite of dynamic.
[00:19:40]
It's very static, which means you often have to be very explicit, which sometimes means
[00:19:44]
boilerplate.
[00:19:45]
And so the one downside with having one opinionated way of doing things that you have to abide
[00:19:53]
by is that you're limited in how you do things.
[00:19:57]
So if you do that with your form package, if you have an opinion in a way of managing
[00:20:04]
that form, then you better hope that it's good.
[00:20:07]
Right.
[00:20:08]
Right.
[00:20:09]
Yeah.
[00:20:10]
Which you seem to think it is.
[00:20:11]
I haven't played with it yet.
[00:20:13]
Yeah.
[00:20:14]
We'll see later on.
[00:20:15]
Yeah.
[00:20:16]
Well, you, yeah, you could share your thoughts once you try it out.
[00:20:19]
And we kind of talked about this in our original episode where we talked about exploring this
[00:20:23]
initial API design.
[00:20:26]
But when you have, so choosing what you have opinions on and choosing what you couple to
[00:20:32]
choosing what assumptions to make, right.
[00:20:34]
If you, so this form API has an opinion that form data is a thing.
[00:20:41]
Form fields are a thing.
[00:20:43]
They are input or text area elements and they have key value pairs.
[00:20:49]
That I mean, that is like, that's not my opinion.
[00:20:55]
That's the browser's opinion.
[00:20:57]
So I think that that's a pretty solid foundation for an API to build on.
[00:21:02]
Because you're working with conventions defined by WWC, I think.
[00:21:10]
World Wide Web Consortium.
[00:21:13]
Yeah.
[00:21:15]
Some MDN.
[00:21:16]
You're conforming to MDN.
[00:21:18]
Exactly.
[00:21:19]
That's right.
[00:21:20]
Yes, exactly.
[00:21:22]
So by doing that, this has been something that I've been really thinking about a lot
[00:21:28]
recently is sort of how do we fill that gap where like exactly this sort of sweet spot
[00:21:34]
that Rails and Ruby were able to fit into where you get to use this magic, which makes
[00:21:41]
things very sleek and you can give an amazing demo because you're just like, hey, I just
[00:21:46]
put this thing here and all these things happen.
[00:21:48]
It just works.
[00:21:49]
You don't have to like, okay, I wire this in here.
[00:21:51]
And I have to have a message that responds to this thing.
[00:21:54]
And oh, the types aren't right for this.
[00:21:56]
They didn't line up here.
[00:21:57]
So we have to change that.
[00:21:58]
Right.
[00:21:59]
It's like, that's not very sexy.
[00:22:01]
Right.
[00:22:02]
But you can get a lot closer to that magic when you bake in these assumptions.
[00:22:08]
Yeah.
[00:22:09]
The one thing that I've quite liked with that I've very much liked with all the element
[00:22:14]
implementations where they do code generation and something quite like magic in a way.
[00:22:21]
I'm mostly thinking of Elm SPA where they generate some of the files to do the boilerplate
[00:22:27]
between the main application and all the different pages.
[00:22:31]
The nice thing with that is that you can look at the boilerplate and you can see how things
[00:22:35]
are done.
[00:22:37]
And in Elm SPA's case, you can even overwrite how the boilerplate writing is done, wiring
[00:22:44]
is done.
[00:22:45]
I'm guessing with Elm Pages you won't see that.
[00:22:49]
Right.
[00:22:50]
Yeah.
[00:22:51]
Elm Pages has a specific, I mean, it tries to, you know, give you the right extension
[00:22:56]
points to customize things, but it is like, it generates its main.elm, which does its
[00:23:03]
thing and in that particular part.
[00:23:06]
So it's more like Elm SPA gives you a sort of customizable main.elm, I believe, and Elm
[00:23:15]
Pages gives you points to customize that it pulls into the generated main.elm, but main.elm
[00:23:22]
is not customizable.
[00:23:23]
And there are some limitations, but there are some conveniences with that.
[00:23:27]
Yeah.
[00:23:28]
And it doesn't have to be important to look at how things are implemented under the hood.
[00:23:33]
It really depends on how much do you need it to understand the whole thing.
[00:23:37]
Like for instance, Elm Review has the same mechanism where you give it a list of rules
[00:23:42]
that you config and Elm Review pulls that in and we're good.
[00:23:47]
No one needs to understand how things are wired in into main.
[00:23:51]
They just need to understand what a rule is, what it does, and that's it.
[00:23:56]
Right.
[00:23:57]
Exactly.
[00:23:58]
I mean, that's the power of a good abstraction.
[00:24:01]
Of course, if it's possible to abstract out a certain detail completely, then that's ideal.
[00:24:08]
Yeah.
[00:24:09]
Now I'm curious, will people be able to see the Elm files that you've generated in the
[00:24:16]
Elm stuff folder?
[00:24:18]
If people are really curious on how things work under the hood?
[00:24:20]
Oh yeah.
[00:24:21]
Yeah.
[00:24:22]
You can see all of the code that's generated.
[00:24:24]
Absolutely.
[00:24:25]
Yeah.
[00:24:26]
It's there on the file system.
[00:24:27]
Yeah.
[00:24:28]
I wonder if the editor picks it up, like all the connections.
[00:24:31]
Oh, this function is used in this generated file.
[00:24:34]
Because that would help with understanding.
[00:24:38]
That could get people lost maybe in some cases, but maybe, I don't know.
[00:24:42]
That's a good question.
[00:24:43]
Yeah.
[00:24:44]
It might not, because it does create a separate folder that copies over the Elm.json.
[00:24:51]
Yeah.
[00:24:52]
I've definitely thought about these kinds of considerations.
[00:24:55]
It gets interesting creating an intuitive experience where the editor gives you what
[00:25:00]
you're expecting there.
[00:25:01]
I actually think that it won't, because I don't think it does that for Elm Review either.
[00:25:07]
You can't see where the config that you've defined is used.
[00:25:11]
Right.
[00:25:12]
Right.
[00:25:13]
Yeah.
[00:25:14]
Exactly.
[00:25:15]
Yeah.
[00:25:16]
So we talked about decoupling from the domain-specific data types for having a separate form parser
[00:25:23]
and a separate sort of low-level data type.
[00:25:27]
We haven't yet touched on using that low-level data type for submissions.
[00:25:34]
And this is actually another one of the key pieces that Elm Pages uses for this magic
[00:25:40]
is if you're able to do code sharing, then you can send that low-level data.
[00:25:47]
So what typically will happen with a front-end application these days is you'll have some
[00:25:54]
form data.
[00:25:55]
You manage the form inputs programmatically through JavaScript somehow.
[00:26:01]
And on submit, you intercept the on submit, and then you end up encoding some JSON objects
[00:26:10]
that you send up to an API or something.
[00:26:12]
And you can do that using my Elm Form API.
[00:26:15]
I have a with on submit, and you can use that to...
[00:26:21]
You can even write your sort of form parser in such a way that you can take your form
[00:26:29]
field inputs, and you could even parse it into a JSON object if you wanted to, for example.
[00:26:35]
You could parse it into a tuple with a JSON object and a nice Elm type, which you can
[00:26:41]
use to show optimistic UI, to show the in-progress creating item that you've got.
[00:26:48]
There are all sorts of things you could do.
[00:26:50]
It's quite flexible in that regard, but that works.
[00:26:53]
But that's an extra layer of glue.
[00:26:56]
You know, Lamdara talks a lot about...
[00:26:58]
You know, Mario has given some great talks about this philosophy in Lamdara of removing
[00:27:03]
glue layers.
[00:27:04]
And I think he's done a brilliant job with that.
[00:27:07]
And I believe that this actually removes an additional layer of glue that Lamdara still
[00:27:13]
has in it without using this approach.
[00:27:15]
Because if you think about it, it's a lot of glue to turn these...
[00:27:21]
You know, to do all this managing of form input data and sort of parsing it at various
[00:27:27]
stages.
[00:27:28]
It turns into a lot of glue.
[00:27:30]
And then when you want to send that data, that you're turning this sort of unstructured
[00:27:35]
form data into structured data that you can send up to the server, even with Lamdara,
[00:27:39]
when you can say send to backend, you still...
[00:27:43]
There's an additional challenge that you can't trust that user input because...
[00:27:48]
Because someone can just craft that same request to the backend.
[00:27:52]
Exactly.
[00:27:53]
So they could take the underlying bytes that are sent, they could hack on that, and they
[00:27:58]
could say, okay, well, you know, this represents an Elm type, which is a valid username, which
[00:28:04]
valid usernames must be this many characters and can't have an at or whatever.
[00:28:09]
So that's a nice opaque type, right?
[00:28:11]
It's impossible for that type to be created unless it goes through that code path, right?
[00:28:17]
Except with Lamdara wire, when you do send to backend, it just has a bytes decoder.
[00:28:23]
And magically, that opaque type comes into existence from those bytes.
[00:28:27]
And if you were to hack those bytes and give it a string that's not a valid username, so
[00:28:32]
you actually can't trust it.
[00:28:33]
And, you know, if you look at the meetdown code, Martin Stewart pointed me to some code
[00:28:38]
in that code base, which is a Lamdara app, really, really cool service.
[00:28:43]
And he has a few helpers that sort of have security around that.
[00:28:50]
So he's actually considered that, you know, you know how much we love opaque types and
[00:28:55]
getting guarantees around that, but then you sort of lose those guarantees and you have
[00:28:58]
to revalidate all of your opaque type constraints.
[00:29:01]
Right.
[00:29:02]
And again, only at the edges, but still you have to do it.
[00:29:07]
Yeah.
[00:29:08]
Which, yeah.
[00:29:09]
Right.
[00:29:10]
It starts to feel quite messy.
[00:29:11]
And then you're like, wait a minute, can I really rely on this?
[00:29:15]
And now you're dealing with opaque types that you can't trust.
[00:29:19]
Right?
[00:29:20]
The whole point is guarantees.
[00:29:22]
So that's like, that's the stuff of our nightmares.
[00:29:25]
Right.
[00:29:26]
Yep.
[00:29:27]
Literally.
[00:29:28]
I mean, if you can't trust opaque types, then who can you trust?
[00:29:32]
Exactly.
[00:29:33]
Exactly.
[00:29:34]
Think of the children.
[00:29:36]
So there's the problem.
[00:29:42]
How might this form API help with that?
[00:29:44]
Well, I'm glad you asked.
[00:29:46]
Hey, Dillon, how can your form API, how can it help with that?
[00:29:52]
Excellent question, Jeroen.
[00:29:54]
Good.
[00:29:55]
The answer is you defer parsing that data until you receive it on the server.
[00:30:02]
So now you, with LambdaRacend to backend, what are you sending?
[00:30:07]
You're sending low level key value pairs of strings.
[00:30:11]
Which is the underlying, I mean, this is, as far as the browser is concerned, that's
[00:30:16]
the only thing that exists.
[00:30:17]
Right.
[00:30:18]
That's what form data is.
[00:30:19]
It's low value, low level key value pairs keyed off of the field names.
[00:30:24]
So you can submit those.
[00:30:27]
You don't even need to write an encoder or anything for the submission.
[00:30:31]
You just let it submit those.
[00:30:32]
You can use send to backend to send that low level data.
[00:30:35]
Now in your LambdaRacend backend, when you receive the low level data, now you run your
[00:30:40]
parser.
[00:30:41]
If you want to, you can run that.
[00:30:43]
I mean, you run that parser on the front end.
[00:30:46]
You know, you render with that sort of parsing logic.
[00:30:49]
So you get your client side validations in real time as you type.
[00:30:52]
But then it reruns those same validations because you're using the exact same parser
[00:30:56]
on the backend.
[00:30:57]
Now that's your source of truth.
[00:30:58]
So now the source of truth doesn't come from data that's serialized from the front end
[00:31:04]
to the backend.
[00:31:05]
You've got low level data as the thing you're sending to the backend.
[00:31:10]
And your source of truth is your code sharing of your form between the front end and the
[00:31:15]
backend.
[00:31:16]
So now you can trust your opaque types because you're running them in a trusted environment
[00:31:19]
in the backend.
[00:31:20]
And I actually did a small example with a really very simple little LambdaRacend toy
[00:31:26]
app I have, but I tried it out and it works great.
[00:31:31]
You just run your form parser on the backend and get your data and check if it's valid
[00:31:36]
or invalid and it works beautifully.
[00:31:38]
Yeah.
[00:31:39]
So people can still hack the bytes that are sent, but the backend will just refuse it
[00:31:45]
because, hey, this is not a opaque type.
[00:31:47]
And I'm guessing they will have to handle that somehow.
[00:31:51]
But that's not a lot of work.
[00:31:53]
Do you mean the opaque type of the, what they're sending is key value strings?
[00:31:59]
Yeah.
[00:32:00]
What I mean is they get the backend gets the low level data and they parse it into an opaque
[00:32:05]
type or an error, a result, a failing result.
[00:32:11]
And then they need to handle the successful case and the error case.
[00:32:15]
Yes.
[00:32:16]
And that's running through backend code.
[00:32:17]
So that can be trusted.
[00:32:19]
So the thing is like conceptually, any user, trusted or not, has the ability to send data
[00:32:29]
to the server.
[00:32:30]
Right.
[00:32:31]
And it's the server's job to then take that unverified, untrusted data and decide whether
[00:32:37]
to trust it and to do something with it or ignore it.
[00:32:41]
Yeah.
[00:32:42]
And it should probably never really trust it anyway.
[00:32:45]
Right.
[00:32:46]
Exactly.
[00:32:47]
So by deferring, parsing it from low level to structured data until it gets to the server,
[00:32:55]
you are following that conceptual mental model of not trusting it because you're just like,
[00:33:01]
hey, you can send me key value pairs, right?
[00:33:03]
Like you can go to, you know, I could open up some, you know, web tools and make a submission
[00:33:11]
to Amazon, to some Amazon forum and type in the key value pairs I want to.
[00:33:18]
And I could try to like put some unwanted values in there, but you know, and they can
[00:33:23]
decide what to do with that in return.
[00:33:25]
But it's the job of the server to decide what to trust because the client can't be trusted.
[00:33:30]
The client can be anything.
[00:33:33]
And we don't really care what the client is.
[00:33:35]
Like the client could be Vim or it could be a script or it could be a curl command.
[00:33:41]
And we don't really know, like we can kind of try to put some things through JavaScript
[00:33:46]
onto the page to put like a, you know, a little hidden field to try to prevent that.
[00:33:52]
But we can't really know that we can trust the client because the client can do anything.
[00:33:59]
But the server we control.
[00:34:00]
It's kind of weird that we have both the sentence, the client can't be trusted and the client
[00:34:05]
is king.
[00:34:06]
But try to merge those together.
[00:34:07]
I don't know if I've heard that one.
[00:34:08]
Yeah.
[00:34:09]
I've heard that one before.
[00:34:11]
You haven't heard that one?
[00:34:12]
No.
[00:34:13]
It's mostly for restaurants or.
[00:34:16]
I've heard the customer is king.
[00:34:18]
Customer is always right.
[00:34:19]
Oh, maybe that's just a.
[00:34:21]
It's French because customer is client.
[00:34:25]
That makes sense.
[00:34:26]
That makes sense.
[00:34:27]
That's fair.
[00:34:28]
So that's a French English joke.
[00:34:29]
Hmm.
[00:34:30]
Interesting.
[00:34:31]
It's always a bit hard to figure out who can you tell those jokes that mix two languages.
[00:34:38]
I have some jokes that I can only tell my family because they're like French and Dutch.
[00:34:44]
And it's a mix of those.
[00:34:47]
There's some good ones, but you can only tell them to specific groups of people.
[00:34:52]
And it's such a shame.
[00:34:53]
We can tell the Elm React jokes here, but that's about it.
[00:34:58]
So because we're dealing with low level data, you don't have a lot of guarantees around
[00:35:03]
it to start with.
[00:35:05]
So for instance, how do you make sure that in a form that I'm going to send the backend
[00:35:11]
that I have not forgotten a field at the backend expects?
[00:35:16]
Like you know, usually we try to create a nice opaque type and that opaque type in it,
[00:35:22]
it expects something of a specific shape.
[00:35:25]
Like if it's Jason, then you parse it and you expect it to be a specific shape.
[00:35:30]
If it does, then you get an okay.
[00:35:32]
And otherwise you get an error.
[00:35:34]
And that is always a bit tricky to get right in the sense that if you forget it, then you're
[00:35:40]
likely not going to notice it unless you write specific tests for that, which I personally
[00:35:46]
don't, but I probably should.
[00:35:49]
So how do you avoid that problem with your form API?
[00:35:53]
Now do you mean like a required field?
[00:35:56]
Yeah, let's say there's a required field that is the first name and you forgot to add a
[00:36:03]
field for that because you know, you and me, we both like guarantees and all those things.
[00:36:10]
And we like things type check.
[00:36:12]
And I would like to check that this first name is never forgotten.
[00:36:16]
So how do I ensure that?
[00:36:18]
Is there something in your form or is it just something that you have to write tests for?
[00:36:22]
Yeah.
[00:36:23]
So you can, you can write a required form field.
[00:36:27]
And if that field is absent, then that validation will fail.
[00:36:30]
So your form will just be parsed as invalid and you'll get a client side, you know, you'll
[00:36:35]
get a client side validation.
[00:36:37]
You can forget to render a form field.
[00:36:42]
The form fields we discussed in the last episode on this, that it uses this sort of Elm codec
[00:36:47]
like pattern where there's a pipeline where you sort of pipe through adding each field
[00:36:54]
in the form, and then you get those in a Lambda and you, you use that to parse into the data
[00:36:59]
type and you use that to render your view with all the form fields.
[00:37:04]
You could forget to render a form field.
[00:37:08]
And if it's, you know, if it's optional and you don't do any validation on it, then that
[00:37:13]
that is something that will pass that validation because it's just like, yeah, there's no,
[00:37:19]
this field isn't there and that would be valid.
[00:37:21]
So that is something to look out for.
[00:37:23]
Again, an Elm review rule would be a great, great way to check that you've used every
[00:37:27]
field in your, in your view.
[00:37:29]
I think the unused parameters rule would mostly work, but you probably should not.
[00:37:35]
It could be used still because you need to use it in two places.
[00:37:40]
Oh, cause you use, you use your form fields in your, in your parser definition.
[00:37:46]
You use them in two places.
[00:37:48]
You use them in your view to render a custom, custom view, however you want to render it.
[00:37:53]
And you also use it to do dependent validations, to combine together all of your fields into
[00:38:00]
a data type.
[00:38:01]
Okay.
[00:38:02]
So in practice, would you write tests for that?
[00:38:05]
Yeah, I think, I think it's a great thing to write tests for.
[00:38:08]
Yeah, absolutely.
[00:38:09]
Yeah.
[00:38:10]
That would be a really good thing for using Elm program test for.
[00:38:14]
Yeah, that would actually work quite well.
[00:38:16]
Yeah.
[00:38:17]
So we talked about sending this low level form data with Lambdaera and I was pretty
[00:38:22]
excited with how nicely this worked.
[00:38:25]
And again, if you wanted to, you could even take it a step further and have these app
[00:38:29]
messages where the form messages don't need to be wired in for every single page, right?
[00:38:35]
You could have a single application wide message for your form messages and you just render
[00:38:41]
your, so you could get that same magic that Elm pages sort of bakes in.
[00:38:46]
You could bake that into your own framework or there could be a Lambdaera specific framework
[00:38:51]
that bakes that in.
[00:38:52]
So there's all sorts of opportunities for sort of getting that magic there, which is,
[00:38:57]
which is pretty cool.
[00:38:58]
But yeah, I was pretty excited with how this, this pattern still works with Lambdaera.
[00:39:03]
And to me, this, this takes this like Lambdaera philosophy of like removing glue and removes
[00:39:10]
a big piece, right?
[00:39:11]
Cause you're like, wow, I can build an app with no glue.
[00:39:15]
That's amazing.
[00:39:16]
So you're saying I can like send data to the backend with no glue and receive it from the
[00:39:20]
backend with no glue and it's all real time data.
[00:39:22]
That's amazing.
[00:39:23]
It's like, yeah, no, no glue.
[00:39:25]
Okay.
[00:39:26]
Well, what if I want to like send up some data, like input some data and send it up?
[00:39:30]
It's like, okay, you write a message and you, it's like, oh, okay.
[00:39:34]
That's, that sounds like a lot of, a lot of glue.
[00:39:38]
So I feel like, I feel that this removes a last remaining piece of Lambdaera glue if
[00:39:43]
you, if you use this pattern in Lambdaera.
[00:39:45]
You're removing glue while keeping things working.
[00:39:50]
Yes.
[00:39:51]
In a way that is predictable.
[00:39:53]
Right.
[00:39:54]
Now this gets to an interesting question in the case of Lambdaera.
[00:39:58]
So, you know, Lambdaera also has this really interesting guarantee that you can migrate
[00:40:04]
data and it's like a guaranteed safe migration because you have to account for all of your
[00:40:11]
data and how you, even how you would deal with messages that are coming into the server
[00:40:18]
from an old version.
[00:40:20]
Yeah.
[00:40:21]
So when you say migration or migrating messages and models, it's when a old client talks to
[00:40:27]
a new backend or the other way around or in all those directions that are possible, it
[00:40:35]
migrates things in a way that they can communicate safely.
[00:40:41]
Right exactly.
[00:40:42]
And you can migrate your front end model and to new versions of the app and all this.
[00:40:47]
Right.
[00:40:48]
So that's one of the big value propositions of Lambdaera.
[00:40:51]
So then you take this form data, which maybe let's say before it was more structured form
[00:40:57]
data, you had a check-in date was in your front end model and your checkout date and
[00:41:04]
your first name and last name.
[00:41:06]
And now, and you had a send to backend, which took that and you had a specific message for
[00:41:13]
that.
[00:41:14]
And now you lose the migration of all of that type safe data that knew exactly what the
[00:41:20]
shape is and what you had in the front end and all of that.
[00:41:22]
Right.
[00:41:23]
So this is something I've been thinking about a little bit.
[00:41:26]
I think that let's say that you, let's say that you add a new field and it's a required
[00:41:33]
field.
[00:41:34]
Now you are going to, if the user's on that page, now suddenly there's a new field, but
[00:41:40]
there's also going to be a client side validation that says you're missing a required field.
[00:41:45]
Wait, just so we're clear, the client is on the old version or the new version with a
[00:41:51]
new field?
[00:41:52]
Well, if the client migrates to the new version.
[00:41:55]
Okay.
[00:41:56]
I hope I'm understanding how it works correctly.
[00:41:59]
Maybe it does it not migrate.
[00:42:00]
I think it migrates.
[00:42:02]
I could be wrong on that.
[00:42:03]
I have to check, but whatever it does, the source of truth is the form parser and the
[00:42:11]
backend checks that source of truth and the backend is able to give validation errors
[00:42:17]
as well.
[00:42:18]
So, I think it lines up.
[00:42:21]
I might be missing a detail about how this works, but I believe it's the paradigm still
[00:42:27]
works out for at least for cases where you're adding a field.
[00:42:32]
If you're removing a field, of course, it's all kind of messy.
[00:42:36]
There's no simple, neat way to migrate a form as somebody is entering data into a form.
[00:42:44]
I think with the Lemdara, if you have a frontend in V1 and it sends a message to your backend
[00:42:50]
in V2, then that message will be, that sent to backend message will be migrated.
[00:42:56]
Because your data is low level, you will not have a migration.
[00:43:01]
Although I'm guessing you could write a migration, even when the types don't change.
[00:43:06]
If you wanted to create an introduce a new variance name, just to be explicit about that.
[00:43:12]
Yeah, you absolutely could.
[00:43:13]
That's a good point.
[00:43:14]
If you wanted to, you still could.
[00:43:16]
But in this case, you would probably say, hey, you're missing this required field.
[00:43:19]
Just refresh the page because you can't migrate.
[00:43:24]
You can't add missing data to form, right?
[00:43:27]
Right.
[00:43:28]
Exactly.
[00:43:29]
Yeah.
[00:43:30]
And I believe it, looking at the Lemdara docs, I think it does migrate the frontend model
[00:43:34]
as well.
[00:43:35]
So you're going to get an update where it's showing you the new form fields.
[00:43:42]
Yeah.
[00:43:43]
Then it's mostly an issue about when do you get the update and when is the, how does the
[00:43:48]
deployment work?
[00:43:50]
Right.
[00:43:51]
But at the end of the day, I think it's still like, so my bottom line is I feel like you
[00:43:58]
don't lose a whole lot by using the lower level form data to send it to the backend
[00:44:04]
because at the end of the day, you're going to need to handle the possibility of an invalid
[00:44:11]
form being sent to the backend.
[00:44:14]
There's no avoiding that because clients can send anything.
[00:44:17]
Even if it's only supposed to send through Lemdara, you can still hack it and send bytes
[00:44:22]
over the wire.
[00:44:23]
It's like, that's just how servers work.
[00:44:25]
You can send data to them.
[00:44:28]
And so I think that this approach works really well where you're just saying the source of
[00:44:34]
truth, the gatekeeper is the form parser and you can code share that to show the client
[00:44:40]
side validations and turn it into structured data on the backend or get validations, which
[00:44:46]
we can send back to the client and say, Hey, you must have bypassed something, but you
[00:44:50]
gave me something invalid, right?
[00:44:52]
Or you can also do backend specific validations.
[00:44:56]
That's also possible with this design.
[00:44:59]
But I think it's a sound approach because at the end of the day, you still need to handle
[00:45:04]
the possibility of receiving invalid data on the backend.
[00:45:08]
And if there's a migration and suddenly the front end just migrates to something where
[00:45:14]
now there's a missing required field, you send it to the backend.
[00:45:17]
It doesn't matter that you didn't do a migration.
[00:45:19]
The new version of the code will handle that validation and then you'll also see that validation
[00:45:27]
error on the front end.
[00:45:28]
So I think it's a sound approach.
[00:45:32]
Maybe some Lambdaero nerd will give me a corner case.
[00:45:37]
I would be very interested to hear if there's a corner case where it really isn't as safe,
[00:45:42]
but I think it works pretty well.
[00:45:44]
So as far as I can tell, this works pretty well when the backend expects the same low
[00:45:49]
level data that you're using in the front end.
[00:45:53]
So that works very well on pages.
[00:45:55]
I can work very well in Lambdaero as well.
[00:45:57]
How does that work with REST APIs or GraphQL?
[00:46:02]
Do you just keep that low level data in the front end?
[00:46:07]
And then when you try to submit, you need to parse that and make the REST or GraphQL
[00:46:13]
request, but you don't have that same technique that you use on the backend about getting
[00:46:18]
low level data.
[00:46:19]
Right.
[00:46:20]
Yeah, that's a great question.
[00:46:22]
So and at first when I was extracting this to a standalone API, I wasn't sure if that
[00:46:28]
would pan out and if the pattern would apply well to a use case where it's front end only
[00:46:34]
Elm because we're talking about full stack on pages v3 and Lambdaero Elm.
[00:46:39]
But actually, I think it does work quite nicely.
[00:46:42]
And one of the patterns I hinted at earlier is that so in your form definition, you take
[00:46:51]
all of your form fields and combine them and you can add additional validations and dependent
[00:46:57]
validations between the form fields.
[00:46:59]
And then you tell it how to parse that successfully or unsuccessfully into structured data.
[00:47:06]
So you could parse it into a record just like a JSON decoder.
[00:47:11]
You can parse into a record, but you could parse it into any data.
[00:47:15]
And as you're parsing it, you're basically telling it, you know, so the most common pattern
[00:47:20]
will have will be to have like a type alias for a record and to use that record constructor,
[00:47:26]
you know, just like in a decoder, you say decode dot succeed user, and then you just
[00:47:32]
keep adding on these decoders to applicatively add data to that constructor.
[00:47:39]
But that record constructor is just a function.
[00:47:43]
So you could just as well say a function that takes this value and this value and turns
[00:47:49]
it into these values.
[00:47:50]
And you can put it into a record if you want.
[00:47:52]
But what if instead of putting it into a record, you created a tuple with a JSON encode object
[00:48:01]
and the structured data if you wanted some structured data to show in a pending UI to
[00:48:08]
show this is the item that we're creating right now.
[00:48:11]
But you can also construct, you know, you could even construct like an API request payload
[00:48:17]
thing that maybe there's a custom type for which endpoint you're sending to and maybe
[00:48:22]
there's a JSON encode value for the payload to send.
[00:48:28]
Or you could build up, you know, an Elm GraphQL input object or whatever it is.
[00:48:33]
So like, because you have the data right there, you have all of the fields directly to combine
[00:48:40]
into whatever data type you want.
[00:48:43]
So, so when you render the form, you can say, with on submit, and, and then you can either
[00:48:52]
get the valid or invalid data.
[00:48:55]
And with the valid data, you have the successfully parsed data type.
[00:48:59]
So we've talked about low level data with forms.
[00:49:02]
Do you see any other places where we could use this technique?
[00:49:06]
I'm thinking maybe HTML bytes or I mean, we use it for JSON to some extent, maybe?
[00:49:14]
No, it's not the same thing, right?
[00:49:18]
This is a very good question.
[00:49:20]
I'm trying to think now.
[00:49:21]
I have noticed as a general principle, with API design, sometimes when I have a type variable
[00:49:30]
in an API I'm designing, I'll try to sort of squeeze out the type variable by distilling
[00:49:36]
things down to lower level data.
[00:49:38]
So that's sort of like something I look for when I'm designing APIs.
[00:49:42]
But but yeah, that's a good question.
[00:49:44]
Like what are the pain points from because I really feel like, you know, I might sound
[00:49:49]
like a broken record now, but it just feels like the way we were working with forms in
[00:49:55]
Elm before was so tedious.
[00:49:59]
And we worked with forms that way for so long.
[00:50:03]
And it like it wasn't it wasn't good.
[00:50:07]
I don't know.
[00:50:09]
I mean, we were clueless and now we are glueless.
[00:50:16]
Let's just say it's been a formative experience.
[00:50:18]
We needed a reform.
[00:50:20]
We can all agree.
[00:50:21]
I almost feel like I have just made one pun and you're like, okay, now I can throw them
[00:50:28]
all.
[00:50:31]
Never never look a pun artist directly in the eye.
[00:50:37]
Your kids are never going to look you in the eye.
[00:50:40]
It's too dangerous.
[00:50:43]
It's a great question.
[00:50:44]
Can you think of anything that might benefit from low level data?
[00:50:48]
I don't have anything in mind.
[00:50:50]
No, no.
[00:50:51]
But I'm sure the listener will tell us if they come up with something.
[00:50:56]
Please do.
[00:50:57]
Actually, there's one use case.
[00:51:00]
Simon Liddell, he made a Elm URL package.
[00:51:04]
I don't remember the exact name.
[00:51:06]
Elm app URL.
[00:51:07]
Elm app URL.
[00:51:08]
And that is basically the URL as a string.
[00:51:12]
Let's keep it as a string.
[00:51:13]
Let's pattern match on it as a string.
[00:51:16]
Or as a list of strings, but still pretty low level.
[00:51:19]
So yeah, I think that works instead of having a parser with a type and yeah, URL.
[00:51:26]
Good point.
[00:51:27]
Yeah, actually, you know, this technique of defunctionalization too, in a way is a form
[00:51:33]
of this.
[00:51:34]
So defunctionalization being instead of having like closures of things to execute, you turn
[00:51:41]
it into data, just like a message is defunctionalization.
[00:51:45]
Instead of having like an on click function, you have a message.
[00:51:50]
And each and the handling for each message will do a different thing.
[00:51:56]
Right.
[00:51:57]
And so I think there are certain ways to kind of combine this idea of defunctionalization
[00:52:02]
and decoupling to lower level data.
[00:52:05]
For example, an HTTP call.
[00:52:09]
You could so like Mario and I were discussing with the Elm pages back end task API, it doesn't
[00:52:17]
use Elm HTTP, it has its own back end task HTTP API.
[00:52:21]
So how could you take a request, but the request is this sort of opaque thing that you can't
[00:52:28]
do anything with.
[00:52:29]
So if you extracted that out to its parts and had the request object be just a data
[00:52:36]
type, maybe that would make it more shareable.
[00:52:40]
Yeah, because that would be a common denominator.
[00:52:43]
Yeah, I'm not sure it's exactly the same principle, though, because like, there's still, it still
[00:52:50]
knows about some of the types in your specific use case, whereas we're talking about not
[00:52:56]
knowing about anything like form data doesn't know about anything.
[00:52:59]
Yeah, I'm gonna have to think on this a little bit.
[00:53:01]
But but yeah, listeners, please do tweet at us or message us in the Elm Radio Slack channel
[00:53:07]
if you think of anything.
[00:53:08]
Don't talk about it too hard, because I don't want to delay the publishing of this package.
[00:53:13]
Good point.
[00:53:16]
So there is one thing I feel I should mention, which this package works with Elm CSS.
[00:53:24]
And it works with Elm HTML.
[00:53:27]
It does not work with Elm UI, unfortunately.
[00:53:29]
The reason is because this package has strong opinions about form fields.
[00:53:35]
And Elm UI doesn't expose a way to natively render form fields, except by dropping into
[00:53:41]
HTML.
[00:53:43]
But there's no way to render semantic form fields.
[00:53:45]
So like, this library, this package assumes form field events, like native HTML form field
[00:53:53]
events.
[00:53:54]
And it also just doubles down on trying to use forms for accessibility and wrap things
[00:53:59]
in form elements and that sort of thing.
[00:54:01]
So that's just sort of a baked in opinion.
[00:54:04]
I would love to see Elm UI expose some way to render form components, but at the moment,
[00:54:11]
it doesn't expose that.
[00:54:13]
So it was quite an interesting experience, sort of.
[00:54:17]
When I was building this, I first started with an experiment to extract it to a standalone
[00:54:23]
package.
[00:54:25]
I just took this Elm package starter repo that I have, and I just copied the relevant
[00:54:33]
code in there, and then without having the specific Elm pages dependencies, and then
[00:54:40]
I just saw what was read.
[00:54:42]
It was really, really interesting.
[00:54:45]
Like a few of those things, like those were the points that I needed to abstract away.
[00:54:52]
And what I ended up doing was really like doing an inversion of control in a lot of
[00:54:56]
those places, which turned out to be, I think, a really just a nice way to design it overall.
[00:55:03]
For example, like the original API just sort of reached in and grabbed state from the sort
[00:55:10]
of Elm application state, which it knew about.
[00:55:13]
But then when you decouple it and it doesn't know what Elm application state is, now you
[00:55:19]
have to pass that in.
[00:55:20]
So you say, is it submitting this form?
[00:55:24]
And then what I ended up with is I ended up creating like a little adapter module.
[00:55:30]
It's just like using the adapter pattern.
[00:55:32]
So it's a very thin layer that, so instead of calling form.renderHTML in an Elm pages
[00:55:39]
app, you can call pages.form.renderHTML.
[00:55:43]
And whereas the standalone package takes a record where you give it submitting equals
[00:55:50]
and then however you want to manage that, you can say model.submitting in Elm pages,
[00:55:56]
it knows whether it's submitting because it handles submitting for you.
[00:56:01]
So when you say submit, Elm pages takes the low level form data.
[00:56:06]
It has, it keeps track of the state in its own framework level model of the in progress
[00:56:13]
form submissions.
[00:56:15]
And it knows how, given its model, how to say whether a field is being submitted.
[00:56:22]
That was, so it was pretty cool.
[00:56:24]
Like I basically just said like, oh, all these things that depend on the Elm pages application
[00:56:30]
state.
[00:56:31]
Okay.
[00:56:32]
I'll invert that.
[00:56:33]
Submitting equals some Boolean.
[00:56:36]
And then in the adapter, I took all that code that had the logic that knew about the Elm
[00:56:41]
pages application state to deduce whether it was submitting or not.
[00:56:45]
And just gave that as the value for submitting equals all that logic I extracted that knew
[00:56:50]
about Elm pages.
[00:56:51]
So it's pretty cool.
[00:56:53]
It was a lot cleaner than I expected.
[00:56:55]
When you started, did you think it would not be possible?
[00:56:58]
I kind of did.
[00:57:00]
I also like had a few places where it was coupled to the backend task API, because you
[00:57:05]
could do backend specific validations.
[00:57:08]
If you wanted to, for example, check that a username is unique or try sending an email
[00:57:13]
to check if an email is valid or whatever on the backend.
[00:57:17]
But it turned out I was able to extract that out into this API where you combine together
[00:57:25]
a set of form definitions, and it will try running the parser on any of those.
[00:57:31]
And it turned out it was a very light wrapper to be able to parse into a form.
[00:57:39]
Basically what it turned into is, so instead of the form parsing into a user record, or
[00:57:48]
the form parsing into a JSON encode value, what it ended up being is just the form can
[00:57:54]
parse into a backend task.
[00:57:58]
If it fails to parse into a backend task, that means that the client-side validation
[00:58:03]
has failed.
[00:58:04]
If it succeeds in parsing into a backend task, now you can continue doing backend validations.
[00:58:11]
And I ended up splitting off this thing that you can render to kind of give server state.
[00:58:19]
So you can give error messages from the backend if you want to.
[00:58:25]
I was actually not expecting it to work out.
[00:58:28]
Maybe not even at all.
[00:58:29]
So it was a pleasant surprise.
[00:58:31]
But now that I did it that way, I think it's really nice also to be able to just look at
[00:58:36]
the docs separately, to learn this tool separately, for people to be able to talk about this package,
[00:58:42]
not just like, oh, it's this Elm Pages specific thing.
[00:58:45]
It's the same API, if somebody's using Elm Pages or somebody's using Lambda or somebody's
[00:58:50]
using Elm SBA, they can all talk about the same package.
[00:58:53]
Yeah.
[00:58:54]
I think it might add a bit of confusion if you're used to neither, because then, oh,
[00:58:59]
what is this form?
[00:59:00]
Oh, well, this is in that other package.
[00:59:02]
Oh, I didn't see that one.
[00:59:04]
But apart from that, sounds like a clear win to me.
[00:59:08]
Especially if people start using it in other places, and then they start using Elm Pages,
[00:59:11]
then the learning curve is a lot smaller.
[00:59:14]
So that's nice.
[00:59:15]
Right.
[00:59:16]
Yeah.
[00:59:17]
Yeah.
[00:59:18]
One of the tricky things, design decisions that I dealt with here was, there's this config.
[00:59:25]
So I have this options type, which when you render a form, you give it options, which
[00:59:31]
has like, it must have the form ID, the unique ID for the form.
[00:59:36]
But it can also have a form method.
[00:59:39]
So a form can be get or post.
[00:59:41]
This is like, most front-end only applications aren't going to care about this, because they're
[00:59:50]
not progressively enhancing a form, whereas Elm Pages uses progressive enhancement to
[00:59:55]
say...
[00:59:56]
So I have Elm Pages examples where I use get form submissions, because when you do a get
[01:00:04]
form submission, it appends query parameters to your URL and reloads the page.
[01:00:11]
So for example, if you're doing a search query, that can be a really nice way to do that.
[01:00:17]
And you can progressively enhance that.
[01:00:18]
So instead of doing a hard page load, you can do an XHR request.
[01:00:25]
That's great.
[01:00:26]
But so anyway, the Elm Form API has this baked in opinion that a form method is a thing that
[01:00:33]
exists.
[01:00:34]
And most people will probably ignore it, but it does have that baked in, even though probably
[01:00:40]
Elm Pages apps will be the only ones to use it.
[01:00:42]
But a Lambdaera app could use it too, if it wanted to, or a front-end only app.
[01:00:47]
So I left it in there because it seemed like a reasonable opinion that that exists, since
[01:00:53]
it does exist in the browser.
[01:00:55]
But yeah, so I created this options record and use a little builder pattern to build
[01:01:02]
up these options.
[01:01:04]
And I share that options record between the Elm Pages package and the standalone package.
[01:01:11]
So I'm guessing that means that whenever you need to do a change in the form package, you
[01:01:20]
will also need to update that in Elm Pages.
[01:01:24]
So you will need to bump them together to keep them in sync.
[01:01:28]
What I ended up doing is I just basically in that little facade module that I have in
[01:01:33]
the Elm Pages API, pages.form, what I did is one of the arguments is the standalone
[01:01:42]
packages options.
[01:01:43]
Got you.
[01:01:44]
Okay.
[01:01:45]
And you transform that to the options of Elm Pages.
[01:01:49]
Right, exactly.
[01:01:50]
So I supplemented.
[01:01:51]
So the Elm Pages options are a superset of that.
[01:01:55]
So I said, you know what, I'm just going to, so that they can share as much as possible,
[01:02:00]
I'm going to share this options type between the two.
[01:02:02]
Even though there are a few additional options that you can pass into the Elm Pages one.
[01:02:06]
So I think that worked out pretty nicely, but that was definitely a subtle design decision
[01:02:11]
there.
[01:02:12]
All right.
[01:02:13]
So if people want to know more about Elm Form and Elm Pages, or your form package, did you
[01:02:19]
even call it Elm Form?
[01:02:20]
What is the name?
[01:02:22]
Did you announce it during this whole episode?
[01:02:25]
No, I somewhat was leaving room for the possibility that I changed my mind about what to call
[01:02:32]
it.
[01:02:33]
But yeah, I think for now I'm calling it Elm Form.
[01:02:35]
I think Elm Form, I don't think I need to get too clever about it.
[01:02:39]
It does kind of like represent a, I think, pretty different philosophy around how to
[01:02:45]
deal with forms in Elm.
[01:02:47]
So I definitely have considered like, does that deserve a different name?
[01:02:50]
But yeah, I'm just calling it Elm Form.
[01:02:53]
Not Elm Awesome Form Meatballs or something.
[01:02:58]
That sounds delicious.
[01:02:59]
It does sound delicious.
[01:03:03]
Yeah so Elm Form.
[01:03:04]
So you know, we'll link to the package docs.
[01:03:08]
Hopefully by the time this is live, we'll link to live package docs, not the preview
[01:03:14]
docs.
[01:03:16]
And I'll try to get some nice ELE examples there, because that's one of the cool things
[01:03:19]
we can get ELE examples.
[01:03:21]
But there is a nice examples folder in the repo, which I'll link to.
[01:03:26]
And yeah, I'll also link to a few examples of it in action in Lambdaera and in Elm Pages.
[01:03:33]
So you can sort of compare the Elm Pages one.
[01:03:36]
It can get pretty sophisticated because you can do all these use cases like dealing with
[01:03:42]
in-flight submissions and parsing that data to get optimistic UI and things like that.
[01:03:47]
But yeah, so I would start there.
[01:03:50]
And if you have any feedback, let Dillon know.
[01:03:54]
Yes.
[01:03:55]
And Jeroen, until next time.
[01:03:57]
Until next time.