What is the "Elm Way" to validate form fields?

I’ve been running through Elm Guide and have extended the Forms example code to include some extra validation checks. The following passwords will work:

Qwerty123  123uP678  3576ffPPw1

This seems to work fine, but involves a lot of chaining if/else functions and has very basic error messaging, so not so user-friendly. One improvement that could be made is to validate on form submit, rather than checking it as it’s being typed.

I’ve had a look around on the forum and haven’t found a de-facto way to error-check and validate forms:

Note (from the guide): It seems like efforts to make generic validation libraries have not been too successful. I think the problem is that the checks are usually best captured by normal Elm functions. Take some args, give back a Bool or Maybe . E.g. Why use a library to check if two strings are equal? So as far as we know, the simplest code comes from writing the logic for your particular scenario without any special extras. So definitely give that a shot before deciding you need something more complex!

  1. You could use built-in HTML5 validation fields;
  2. I’ve seen talk on the forums of using a Result.andThen or possibly even a Maybe type;
  3. Richard Feldman’s validation package here and another one here;
  4. Validation with a type Validated type (a much longer explanation here)

(1) is obviously the easiest, but has it’s limits. I’m not sure how you’d interop with Elm for validation with that method.

As for the other options, I’m not terribly confident about any of them, as there’s a bit of a learning curve and it gets more complex using different field types (radio buttons, date pickers, etc), or with lots of form fields.

  • Is this a solved problem in Elm, or are there ongoing debates?
  • Are there any good up-to-date guides on how to achieve this in a relatively easy way?
  • Are there any simple improvements I can make to the Ellie App code?
  • Can you use case statements instead of if/else for chaining && validations?

It is not a solved problem in Elm – there are many ways to go about it, and I think you might get lots of different suggestions in this thread. I even think forms is not a solved problem in any programming language or framework.

3 Likes

I usually use a similar pattern to this one: elm-spa (rtfeldman), but for very complicated forms I also use a lot of if/else

I’ve been implementing RealWorld for myself from scratch and I discovered an interesting solution to form validation when I was implementing the registration page.

Here are the bits and pieces that make up my solution to form validation in src/Page/Register.elm:

type alias Model =
    { username : String
    , email : String
    , password : String
    , errorMessages : List String
    , isDisabled : Bool
    }

type alias ValidatedFields =
    { username : Username
    , email : Email
    , password : Password
    }

validate : Model -> V.Validation ValidatedFields
validate { username, email, password } =
    V.succeed ValidatedFields
        |> V.required (validateUsername username)
        |> V.required (validateEmail email)
        |> V.required (validatePassword password)

validateUsername : String -> V.Validation Username
validateUsername rawUsername =
    case Username.fromString rawUsername of
        Just username ->
            V.succeed username

        Nothing ->
            V.fail "username can't be blank"

validateEmail : String -> V.Validation Email
validateEmail rawEmail =
    case Email.fromString rawEmail of
        Just email ->
            V.succeed email

        Nothing ->
            V.fail "email can't be blank"

validatePassword : String -> V.Validation Password
validatePassword rawPassword =
    case Password.fromString rawPassword of
        Ok password ->
            V.succeed password

        Err Password.Blank ->
            V.fail "password can't be blank"

        Err (Password.TooShort expectedLength) ->
            V.fail <|
                String.concat
                    [ "password must be at least "
                    , String.fromInt expectedLength
                    , " "
                    , String.pluralize
                        expectedLength
                        { singular = "character"
                        , plural = "characters"
                        }
                    , " long"
                    ]

I’m obviously biased but I prefer my solution over the one given in elm-spa (rtfeldman).

When the form is submitted we handle the validation as follows:

SubmittedForm ->
    validate model
        |> V.withValidation
            { onSuccess =
                \{ username, email, password } ->
                    ( { model | errorMessages = [], isDisabled = True }
                    , Register.register
                        options.apiUrl
                        { username = username
                        , email = email
                        , password = password
                        , onResponse = GotRegisterResponse
                        }
                        |> Cmd.map options.onChange
                    )
            , onFailure =
                \errorMessages ->
                    ( { model | errorMessages = errorMessages }
                    , Cmd.none
                    )
            }

The validation module exposes an API similar to NoRedInk/elm-json-decode-pipeline.

module Lib.Validation exposing
    ( Validation
    , fail
    , required
    , succeed
    , withValidation
    )


type Validation a
    = Success a
    | Failure (List String)


succeed : a -> Validation a
succeed =
    Success


fail : String -> Validation a
fail message =
    Failure [ message ]


required : Validation a -> Validation (a -> b) -> Validation b
required va vf =
    case ( vf, va ) of
        ( Success f, Success a ) ->
            Success (f a)

        ( Failure es, Success _ ) ->
            Failure es

        ( Success _, Failure es ) ->
            Failure es

        ( Failure es1, Failure es2 ) ->
            Failure (es1 ++ es2)


withValidation :
    { onSuccess : a -> b
    , onFailure : List String -> b
    }
    -> Validation a
    -> b
withValidation { onSuccess, onFailure } va =
    case va of
        Success a ->
            onSuccess a

        Failure es ->
            onFailure es

The overall pattern seems to be:

  1. Define your fields to be whatever input you’re willing to accept.
  2. Define a record to hold the cleaned/trimmed/validated data, see ValidatedFields.
  3. Define your validation functions for the individual fields however you like, they return Validation a.
  4. Compose your validation functions, see validate, using functions from the validation module, for e.g. required. But you could implement optional and any other function you desire.
  5. Use withValidation when it’s time to perform the validation.

I don’t know how this scales to more complicated forms but I currently like where the abstraction is heading and how it works for these simple forms in the RealWorld app.

An interesting side note

I’ve been exploring applicative style APIs and I recently came across Applicative Programming, Disjoint Unions, Semigroups and Non-breaking Error Handling by Tony Morris. The API I stumbled upon happens to be the same API, semantically speaking, that he describes in that document. According to him, “Scalaz uses this API for web form field validation. [*]”

4 Likes

Another different approach is Parse, don’t validate in a type-driven design. There’s also a pattern available for this process. At first, it is somehow confusing for people out of computer science field (like me), but when you’re become familiar with the pattern another door opens in your mind when facing input validation scenarios like web forms.

1 Like

We are using elm-form-decoder 1.4.0 for all the form validation needs.

Although, it is not really a validation, but more like decoding.

3 Likes

Us, too. Form decoder can express almost any requirements around user-input-to-data-structure conversion. Solid choice I would say.

@grigorious @dwayne @novid @grrinchas @ymtszw So it seems like @lydell was correct that there are lots of ways to go about form validation!!

I quite like the idea of parsing the form inputs before they reach the type to be stored as Elm data (either with an intermediary type, or validating inputs before they’re stored), but each of these methods could warrant their own chapter in a book!!

As with a lot of things Elm related, I know enough to see how some of the pieces work, but not enough to understand how things fit together at scale.

Elm Spa is too advanced for me, although I understand bits of the form validation page — @dwayne solution does look similar to Elm’s json decoder but I’ve never used Cmd.map and only understand 60% of the rest.

Elm form decoder looks like a neat solution, catching errors before they reach the Goat type — but that blog post is quite a lot simpler than the given Github example. There’s still a lot of question marks, such as “how do you render the specific error next it’s form field”, and more general questions about how it all fits together. I’d also be a little cautious about it’s longevity should Elm get updated.

I’m almost done with “Elm in Action” book, which goes a long way to explaining basic features in Elm, but going from that to working with package documentation and figuring out how the pieces fit together is a big problem (for me).

So yeah, for each of these I think I’d need an “Explain it like I’m 5” blog post(s) — they’re quite complex. A simplified, well-commented Ellie App might go a long way too :slight_smile:

Perhaps this course on elm-validate might help me understand also? I think Elm has lost a really good teacher with @rtfeldman moving to Roc Lang.

Thanks all!

1 Like

Thanks for the kind words! The way I’ve personally found best to approach forms is:

  • Treat the form like any other Elm program: save relevant state in Model, render it in view, update with update. My motto is: “forms are programs.” They aren’t special. Don’t go on a quest to find the magic formula to break them down into LEGO bricks that snap together—just write the program!
    • If the form is really small, anything more than elm-html is unlikely to justify its inherent cost of abstraction. If the form is really complex, a form-building library (or formulaic pattern) consistently seems to turn out to be incompatible with certain edge cases that are unique to this particular form. Then the thing that seemed helpful at first ends up being something I have to do contortions to work around.
    • Ever since realizing I could just write the program, handling edge cases—especially around interactions involving several different fields at once (typically the hardest part about forms)—is maximally easy because all the information I could possibly need is easily accessible right there in Model, and I’m in complete control of all the logic!
  • Really the only packages I reach for are things that are concerned with data (and not with the concept of “forms” at all), e.g. email validation functions.
  • For any user inputs that are going to be rendered, I always want to remember the original String the user entered (even if it’s going to be validated into a number) so I can render back to the user exactly what they typed into the field—regardless of whether it was valid. It’s a more frustrating user experience to type something and have it not even appear in the field (as if I hadn’t even pressed the key) compared to having it show up and then seeing an error message.
  • I also like to record some info in the Model about which fields are “in progress” and therefore not to be validated yet - e.g. if the user starts typing in the email field, don’t display a big red validation error right after they type the first letter because "r" isn’t a valid email address.

Hope that helps! :smiley:

15 Likes

I think I’m going to have to re-read this a few times to completely “get it” but I get the general gist. Don’t try to be too clever and stick to simple validation packages for strings/emails etc (if at all).

Are there any simple form examples that follow this pattern in the wild? The Elm Spa (RealWorld) example is too complicated for me at this stage.

Yes that definitely helps!

Thanks!

I’ll second this. This package is brilliant, IMHO. The ability to take form inputs, strings basically, and decode them into structured data with great error handling, without being opinionated on how to represent your forms in the UI is fantastic.

We used to use composable forms, which is also a great package, but found that it was limiting in how you render your forms if there’s any complexity in the layout.

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