How do you deal with forms?

Trigger warning: I hate form libraries. I really do. I’ve been fighting with them for as long as I can remember (mostly with the Django framework). As soon as I want to build something a bit custom, I would scratch my head, cry in despair, close my computer, put it to fire, and promise myself to never try to build custom forms ever again.

But since forms are central to the web, and since I love Elm so much I thought I would find a way to build custom forms quite easily. Until now I’ve been creating forms “from scratch”: one view function and one Msg per field, and a parsing function, you know the drill. But this ended up being very repetitive, so I started searching for packages, hoping to find something that would allow me to:

  • Parse the form input and get the data in a specific type, or show errors in the form
  • Have full control on the rendering of the form and the widgets

One of the packages I found is composable-form, which looked really nice (I thought the composition part was very clever, as well as the parsing part).

Unfortunately, as soon as you need custom widgets or specific rendering (eg. if you need to change the way the form is rendered, or add classes to input fields), things seem to get messy (or maybe I missed something): you need to copy-paste whole parts of the package, understand the internals, and adapt them. To help you understand why I couldn’t get this package work for my usecase, here’s what I need to do: my forms are not just one block of fields, but are split into sections, which are not just a title and some fields, but blocks with a title, a description and possibly other Html elements. And “submit” buttons sometimes have other buttons with other actions, or just a “Cancel” button to close a modal.

I know there are other packages out there, such as elm-form-decoder 1.4.0 (which requires one error variant for each error for each field, and doesn’t come with any rendering mechanism).

I could continue rolling my own repetitive form code, just like I could write my own JSON decoding library, but hopefully the one in Elm allows me to do everything I need, which is not the case with forms. I’m surprised I couldn’t find a package that allows customizing the rendering/widgets without copying all the package code. Maybe that’s just not possible (I’ve tried to come up with a generic proof of concept but didn’t manage to), and the best way to deal with this is to roll your own form code? Or maybe that’s possible and I’m missing something?

Anyway, I’m very curious about the way you folks deal with forms in your projects, and I’d be happy to read it! :sparkles:

8 Likes

Hi,

I also am not a fan of forms in general, but I had started recently looking into composable-form and elm-form-decoder. I like the composable-form because the parsing is pretty straight forward as well as not having to have a msg for each field.

I am commenting because I am curious as to what others think. I started looking into these form libraries only recently, so I have similar questions to you.

1 Like

I personally dislike form packages in JS and I never felt I needed them. In Elm this feeling is bigger.
I usually create a msg that requires an InputField type (example type InputField = Nick String | Email String | Age Int | Password String | PasswordConfirm string) instead of creating N msgs.
And I create my model using custom types to avoid impossible states and call views based on the current form state. Forms can be very specific, I prefer to model myself, but it may be a personal preference. One thing that I like to do, is for sections that use Maybe and/or should be parsed, to be correctly validated/parsed and then extract in then next step:

type Model
    = LoadingUsersModel
    | SelectUserModel { date : String, users : List User, selectedUser : Maybe User  }
    | DoSomethingModel { date : Date, selectedUser : User }
    | ...

type Msg
    = ReceivedUsers (Result String (List User))
    | Input InputType
    | SelectUser User
    | ...

type InputType
    = Date String
    | ...
2 Likes

It depends a lot on the design requirements, specially how and when you show errors. E.g.

  • Disable the save button if invalid
  • Leave the save button enabled all the time
  • Show an error toast if trying to save an invalid form
  • Show an error banner when clicking the save button
  • Show inline errors on fields when unfocusing
  • etc

This is what we do at the moment:

  • Each form field has an associated type e.g. type Field = Email | Name | …
  • Each field has its own message. We have messages for field changes and fields unfocusing.
  • We render the form using some helpers we built. We have more control in this way as we have complex design requirements.
  • We use validators e.g. elm-validator-pipeline 2.0.0
  • The validators returns errors included the Field type. Then we can show inline errors after unfocusing a field.
3 Likes

Hi @sephi – that is an excellent question, and not sure I have the best answer, but here is where I am at …

With elm-ui, I’m fairly satisfied with building up my own form helper functions as needed. Here is an example, and where it is used

Partial applications are very handy for things like forms!

I suspect that forms are very dependent on the application structure, and the best solution may be custom to your application.

Validation is can be done with functions like this, which is used here

This could all likely be done a lot better, but even in its current state, I enjoy using it and adding new features is enjoyable – it is better than anything else I’ve worked with yet (React, various JS libs, etc).

There are just so many ways to do form stuff, and the answer to how your form should be done is too dependent on your exact project to import someone else’s decisions on how forms should work. Even within one project its hard to standardize form logic, since different pages, forms, and fields are going to have their own exception cases. Everything on the internet is arguably a form (typing and clicking into inputs, and submitting it).

So I am skeptical of form packages for the reason that, the package author may have done their forms a particular way, but if (when?) you ever hit the point where you want to deviate from how they did it then you are out of luck; you cant customize their package.

One tip I can recall is to avoid using functions like the following, especially deep in your application

isValid : ... -> Bool

The reason being that valid data is presumably fundamentally different from invalid data, so it should ideally end up as a different type. Like imagine you want to validate if "1" represents a valid integer. You dont just want True; at some point the user will want the actual Int 1. You dont want to be struck in a weird spot where you have a Bool floating around saying it is valid, and then after that you have the handle the case where you cant successfully parse the Int from the String, implying its not actually valid afterall.

So instead, validate into a new type.

validate : value -> Result Error validValue

This technique of validating into a different type is useful enough that I would do it in its own right even if I didnt have an excuse to, such as if I would otherwise be validating a String to a String

type ValidString = ValidString String 

validateString : String -> Result Error ValidString

Now, since a ValidString and an invalid String are not the same type, it is a compile error to be using an invalid String where you need a ValidString (such as when you want to submit your form).

3 Likes

Indeed one of the hardest things to get right in Elm. I’ve also played with composable-form and others, without luck.

A solution that is working for my particular use case (SPAs and configuration-heavy UIs) is to separate the form state definition from the html representation, something like this:

type alias Field state input error a =
    { key : String
    , parser : input -> Result error a
    , value : state -> input
    , update : input -> state -> state
    }

That allows me to create form elements by explicitly stating how they consume user input:

type FormError 
    = MustHaveAName

type alias Inputs =
    { name : String
    , address : String
    }

name : Field Inputs String FormError String
name =
    { key = "name"
    , parser = Ok
    , value = .name
    , update = \val inputs -> { inputs | name = val }
    }
    |> Field.notEmpty MustHaveAName

(Note: The “key” here is the actual value the server expects for that value)
(Note: Field.notEmpty is a just a Result.andThen over the parser, that fails if the string is empty)

Finally, I can create the UI layer, that is usually very different from app to app (I use Tailwind):

textField : Field state String error a -> state -> List (Attribute state) -> Html state
textField field state attrs =
    customInput "text"
        (\val -> field.update val state)
        (field.value state)
        (Html.Attributes.name field.key :: attrs)

(Note: I like to use the state as message in form elements, I find it easier to manage)

These fields can be consumed in any way using the parsers, I’ve played with several methods and the most interesting is to do something similar to composable-form, with Form.fromField, and then compose to create a form result. The problem with this idea is that it expects a specific kind of backend :frowning_face: , but anyway, I hope this helps.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.