glasserc / elm-form-result / Form.Result

A type for validating a form, collecting errors as we go.

Imagine you have a type that represents the data a user has currently entered into a form. For example, maybe it looks like this:

type alias FormData =
    { username : String
    , password : String
    , confirmPassword : String
    }

You want to validate it into a user object:

type alias User =
    { username : String
    , password : String
    }

When we build up our user type, even one bad field is enough to fail the validation. But when we display the invalid form, we want to show the problems for every single field, not just the first bad one. So when validation doesn't succeed, you want to produce a FormErrors:

type UsernameError
    = UsernameMissing

type PasswordError
    = PasswordNotStrongEnough
    | PasswordsDontMatch

type alias FormErrors =
    { username : Maybe UsernameError
    , password : Maybe PasswordError
    , confirmPassword : Maybe PasswordError
    }

We want to stop building the User as soon as we get one failed field, so you could try using Maybe.Extra.andMap:

validateUser : FormData -> Maybe User
validateUser state =
    Just (\username password _ -> User username password)
        |> MaybeEx.andMap (checkStringEmpty state.username)
        |> MaybeEx.andMap (checkPasswordStrength state.password)
        |> MaybeEx.andMap (checkMatch state.password state.confirmPassword)

But then what if the user was Nothing? You have to re-use all those field checks and even invert them to produce errors instead of successes so that you can use them in the FormErrors type.

FormResult solves this problem by building up both the error type and the validated "output" type at the same time. You feed it Results and it feeds Errs to the error type and Oks to the "output" type. After all the fields have been provided, you can convert the whole thing into a Result, which will be Ok if all the fields were Ok, or Err otherwise.

validateUsername : String -> Either UsernameError String
validateUsername s =
    if s == "" then Err UsernameMissing else Ok s

validatePassword : String -> Either PasswordError String
validatePassword s = ...

validateMatch : String -> String -> Either PasswordError String
validateMatch password confirmPassword = ...

validateUser : FormData -> Either FormErrors User
validateUser state =
    Form.Result.start FormErrors User
        |> Form.Result.validate (validateUsername state.username)
        |> Form.Result.validate (validatePassword state.password)
        |> Form.Result.checkErr (validateMatch state.password state.confirmPassword)
        |> Form.Result.toResult

Note that although state.confirmPassword does not contribute anything to our output type, we still need to check it for errors, and any errors in it should cause the validation to fail.

A fully-functioning demo can be found in the examples directory in this project's repository.

Hints


type alias FormResult err res =
{ errorType : err
, realModel : Maybe res 
}

An "in progress" form validation. The first type variable err is the form errors type. The second type variable res is the "real model" type, which you hope to get if validation succeeds.

start : err -> res -> FormResult err res

Create a form validation.

validated : Result a b -> FormResult (Maybe a -> err) (b -> res) -> FormResult err res

Add a field to an "in progress" form validation, with Err something indicating that validation has failed.

toResult : FormResult err res -> Result err res

Convert a FormResult to a Result.

maybeValid : Maybe resField -> FormResult err (resField -> res) -> FormResult err res

Add a possible output field to an "in progress" form validation, with Nothing indicating that validation has failed.

This can be useful if multiple fields in your form type correspond to a single field in your output type. In this case, a field in your output type might not be present, but without an error in the very next form error field. In that case, you can use this function to incorporate a Maybe field, and add Maybe errors using maybeErr.

maybeErr : Maybe errField -> FormResult (Maybe errField -> err) res -> FormResult err res

Add something to just the error side of an "in progress" form validation, with Just err meaning that validation has failed.

This can be useful if multiple fields in your form type correspond to a single field in your output type. In this case, you'll probably have several fields in your form error type that don't correspond to anything in your output type, so you'll want to feed a possible error in to each without touching the output type.

checkErr : Result errField a -> FormResult (Maybe errField -> err) res -> FormResult err res

A shortcut for calling maybeErr with a Result instead of a Maybe.

If the field is Err, it indicates that validation failed. If the field is Ok whatever, discard the whatever and call the error type with Nothing.

This is useful when you have a validation function that produces a Result err something but you don't actually care about the something.

ifMissing : Maybe resField -> errField -> FormResult (Maybe errField -> err) (resField -> res) -> FormResult err res

A shortcut for calling validated with a Maybe instead of a Result.

If the field is Nothing, it indicates that the validation failed, and the given error is what is used to indicate the error. If the field is Just something, use the something as the output and call the error type with Nothing.

This is useful e.g. when handling select fields, where the only thing you want to validate is that the user actually selected something.

missingAs : errField -> Maybe resField -> FormResult (Maybe errField -> err) (resField -> res) -> FormResult err res

The same as ifMissing, but with the argument order reversed.

unconditional : resField -> FormResult err (resField -> res) -> FormResult err res

Add a field to the output type of an "in progress" form validation, unconditionally.

You might use this if there are fields in your output type that are not dependent on user input.

unconditionalErr : errField -> FormResult (errField -> err) res -> FormResult err res

Add something to just the error side of an "in progress" form validation, unconditionally.

This can be useful if your error type has fields that aren't Maybe, or when Just err in an error field doesn't necessarily mean that the validation failed.