dillonkearns / elm-form / Form

Example

Let's look at a sign-up form example.

What to look for:

The field declarations

Below the Form.form call you will find all of the form's fields declared with

|> Form.field ...

These are the form's field declarations.

These fields each have individual validations. For example, |> Field.required ... means we'll get a validation error if that field is empty (similar for checking the minimum password length). This field definition defines some information that will be used when render the field using Form.FieldView (whether it is a date input, password input, etc.).

There will be a corresponding parameter in the function we pass in to Form.form for every field declaration (in this example, \email password passwordConfirmation -> ...).

The combine validation

In addition to the validation errors that individual fields can have independently (like required fields or minimum password length), we can also do dependent validations.

We use the Form.Validation module to take each individual field and combine them into a type and/or errors.

The view

In your view you can lay out your fields however you want. While you will be using Form.FieldView to render the fields themselves, the rendering for everything besides the fields (including label's, divs, etc.) is completely up to you.

import Form
import Form.Field as Field
import Form.FieldView as FieldView
import Form.Validation as Validation
import Html exposing (Html)
import Html.Attributes as Attr
import Route
import Server.Request as Request
import Server.Response exposing (Response)

type alias SignupForm =
    { email : String
    , password : String
    }

signupForm : Form.HtmlForm String SignupForm () Msg
signupForm =
    Form.form
        (\email password passwordConfirmation ->
            { combine =
                Validation.succeed SignupForm
                    |> Validation.andMap email
                    |> Validation.andMap
                        (Validation.map2
                            (\passwordValue confirmationValue ->
                                if passwordValue == confirmationValue then
                                    Validation.succeed passwordValue

                                else
                                    passwordConfirmation
                                        |> Validation.fail
                                            "Must match password"
                            )
                            password
                            passwordConfirmation
                            |> Validation.andThen identity
                        )
            , view =
                \info ->
                    [ Html.label []
                        [ fieldView info "Email" email
                        , fieldView info "Password" password
                        , fieldView info "Confirm Password" passwordConfirmation
                        ]
                    , Html.button []
                        [ if info.submitting then
                            Html.text "Signing Up..."

                          else
                            Html.text "Sign Up"
                        ]
                    ]
            }
        )
        |> Form.field "email"
            (Field.text
                |> Field.required "Required"
            )
        |> Form.field "password"
            passwordField
        |> Form.field "passwordConfirmation"
            passwordField

passwordField =
    Field.text
        |> Field.password
        |> Field.required "Required"
        |> Field.withClientValidation
            (\password ->
                ( Just password
                , if String.length password < 4 then
                    [ "Must be at least 4 characters" ]

                  else
                    []
                )
            )

fieldView :
    Form.Context String input
    -> String
    -> Validation.Field String parsed FieldView.Input
    -> Html msg
fieldView formState label field =
    Html.div []
        [ Html.label []
            [ Html.text (label ++ " ")
            , field |> Form.FieldView.input []
            ]
        , (if formState.submitAttempted then
            formState.errors
                |> Form.errorsForField field
                |> List.map
                    (\error ->
                        Html.li [] [ Html.text error ]
                    )

           else
            []
          )
            |> Html.ul [ Attr.style "color" "red" ]
        ]

Building a Form Parser


type alias Form error combineAndView parsed input =
Internal.Form error combineAndView parsed input

A Form definition represents

A Form can be used to:

Render Form

Parse Form

While you almost always will want to render your Form in your view function, you may also want to parse your form in a few more advanced use cases.

In a full-stack Elm application, you can try parsing with your Form definition since you can use code sharing to share the same Form definition between your frontend and backend. elm-pages has several built-in functions to help with this.

You may also want to parse your form data in your frontend to take in-flight form submissions and parse them into your parsed values.


type alias HtmlForm error parsed input msg =
Form error { combine : Pages.Internal.Validation error parsed Basics.Never Basics.Never
, view : Context error input -> List (Html msg) } parsed inpu
}

A Form that renders to elm/html. Can be rendered with renderHtml.


type alias StyledHtmlForm error parsed input msg =
Form error { combine : Pages.Internal.Validation error parsed Basics.Never Basics.Never
, view : Context error input -> List (Html.Styled.Html msg) } parsed inpu
}

A Form that renders to rtfeldman/elm-css. Can be rendered with renderStyledHtml.

form : combineAndView -> Form error combineAndView parsed input

Initialize the builder for a Form. Typically an anonymous function is passed in to this function, with one parameter for each field that comes after.

form =
    Form.form
        (\name email ->
            { combine =
                Validation.succeed User
                    |> Validation.andMap name
                    |> Validation.andMap email
            , view = \info -> [{- render fields -}]
            }
        )
        |> Form.field "name" (Field.text |> Field.required "Required")
        |> Form.field "email" (Field.text |> Field.required "Required")

Adding Fields

field : String -> Field error parsed input initial kind constraints -> Form error (Validation.Field error parsed kind -> combineAndView) parsedCombined input -> Form error combineAndView parsedCombined input

Declare a visible field for the form.

Use Form.Field to define the field and its validations.

form =
    Form.form
        (\email ->
            { combine =
                Validation.succeed SignupForm
                    |> Validation.andMap email
            , view = \info -> [{- render fields -}]
            }
        )
        |> Form.field "email"
            (Field.text |> Field.required "Required")

View Functions


type alias Context error input =
{ errors : Errors error
, submitting : Basics.Bool
, submitAttempted : Basics.Bool
, input : input 
}

The data available as the first parameter in a Form's view function.

Showing Errors


type Errors error

The current validation errors for the given Form. You can get the errors for a specific field using errorsForField.

errorsForField : Validation.Field error parsed kind -> Errors error -> List error

Get the List of errors for a given field.

Often it's helpful to define a helper function for rendering a fields errors using your application's layout and style conventions.

import Form
import Form.Validation as Validation
import Html exposing (Html)

errorsView :
    Form.Context String input
    -> Validation.Field String parsed kind
    -> Html msg
errorsView { submitAttempted, errors } field =
    if submitAttempted || Validation.statusAtLeast Validation.Blurred field then
        -- only show validations when a field has been blurred
        -- (it can be annoying to see errors while you type the initial entry for a field, but we want to see the current
        -- errors once we've left the field, even if we are changing it so we know once it's been fixed or whether a new
        -- error is introduced)
        errors
            |> Form.errorsForField field
            |> List.map
                (\error ->
                    Html.li
                        [ Html.Attributes.style "color" "red" ]
                        [ Html.text error ]
                )
            |> Html.ul []

    else
        Html.ul [] []

Rendering Forms

renderHtml : { submitting : Basics.Bool, state : Model, toMsg : Msg mappedMsg -> mappedMsg } -> Options error parsed input mappedMsg extras -> List (Html.Attribute mappedMsg) -> Form error { combine : Pages.Internal.Validation error parsed named constraints, view : Context error input -> List (Html mappedMsg) } parsed input -> Html mappedMsg

Render the form to elm/html.

view model =
    signUpForm
        |> Form.renderHtml
            { submitting = model.submitting
            , state = model.formState
            , toMsg = FormMsg
            }
            (Form.options "signUpForm")
            []

Note: In elm-pages, you'll want to use the Pages.Form.renderHtml function instead.

renderStyledHtml : { submitting : Basics.Bool, state : Model, toMsg : Msg mappedMsg -> mappedMsg } -> Options error parsed input mappedMsg extras -> List (Html.Styled.Attribute mappedMsg) -> Form error { combine : Pages.Internal.Validation error parsed field constraints, view : Context error input -> List (Html.Styled.Html mappedMsg) } parsed input -> Html.Styled.Html mappedMsg

Render the form to rtfeldman/elm-css.

view model =
    signUpForm
        |> Form.renderStyledHtml
            { submitting = model.submitting
            , state = model.formState
            , toMsg = FormMsg
            }
            (Form.options "signUpForm")
            []

Note: In elm-pages, you'll want to use the Pages.Form.renderStyledHtml function instead.

Render Options


type alias Options error parsed input msg extras =
{ id : String
, action : Maybe String
, method : Method
, input : input
, onSubmit : Maybe ({ fields : List ( String
, String )
, method : Method
, action : String
, parsed : Validated error parsed } -> msg)
, serverResponse : Maybe (ServerResponse error)
, extras : Maybe extras 
}

The Options for rendering a Form. You can build up Options by initializing the default Options with init and then adding options with functions from Render Options like withInput, withOnSubmit, etc.

options : String -> Options error parsed () msg extras

Initialize a set of default options with a unique id for your Form. Note that even if you are rendering the same form multiple times this ID needs to be unique in order to manage the state of each form independently.

For example,

cartView model items =
    items
        |> List.map
            (\item ->
                itemForm
                    |> Form.renderHtml
                        { submitting = model.submitting
                        , state = model.formState
                        , toMsg = FormMsg
                        }
                        (Form.options ("cart-item-" ++ item.id))
                        []
            )
        |> Html.div []

withInput : input -> Options error parsed () msg extras -> Options error parsed input msg extras

You can pass in an input value to the Options that are passed in to renderHtml or renderStyledHtml.

You can use whichever data type you want as your input value. You will then have access to that value in two places:

  1. The Form's view through the Context argument's input field.
  2. Form.Field.withInitialValue

One example where you would use an input value is if you have an existing UserProfile from the server that you want to use to pre-populate the form fields.

import Form
import Form.Field as Field
import Form.Validation as Validation

type alias UserProfile =
    { name : String
    , email : String
    }

userProfileForm : Form.HtmlForm String UserProfile UserProfile msg
userProfileForm =
    (\name email ->
        { combine =
            Validation.succeed UserProfile
                |> Validation.andMap name
                |> Validation.andMap email
        , view =
            \context ->
                [ Html.h2
                    []
                    [ Html.text
                        --  use the input to display Model data
                        (context.input
                            ++ "'s Profile"
                        )
                    ]
                , fieldView "Name" name
                , fieldView "Email" email
                , if context.submitting then
                    Html.button [ Html.Attributes.disabled True ] [ Html.text "Updating..." ]

                  else
                    Html.button [] [ Html.text "Update" ]
                ]
        }
    )
        |> Form.form
        |> Form.field "name"
            (Field.text
                |> Field.required "Required"
                |> Field.withInitialValue .name
            )
        |> Form.field "email"
            (Field.text
                |> Field.required "Required"
                |> Field.withInitialValue .email
            )

view model =
    [ model.userProfile
        |> Maybe.map
            (\userProfile ->
                userProfileForm
                    |> Form.renderHtml
                        { submitting = model.submitting
                        , state = model.formState
                        , toMsg = FormMsg
                        }
                        (Form.options "userProfile"
                            |> Form.withInput userProfile
                        )
                        []
            )
        |> Maybe.withDefault "Loading Profile..."
    ]

withAction : String -> Options error parsed input msg extras -> Options error parsed input msg extras

Set the action attribute of the rendered <form> element. Note that the action attribute in the withOnSubmit is preprocessed in the browser, so the String will point to the same URL but won't necessarily be the exact same String that was passed in. For example, if you set options |> Form.withAction "/login", your onSubmit will receive an absolute URL such as { action = "https://mysite.com/login" {- , ... -} }.

Setting the action can be useful if you are progressively enhancing form behavior. The default browser form submission behavior is to submit to the current URL if no action attribute is set, and an action is present then the form submission will go to the given URL. If you are attempting to use progressive enhancement then you can simulate this behavior through your withOnSubmit handler, or you may be using a framework that has this simulation built in like elm-pages.

See also https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action.

withOnSubmit : ({ fields : List ( String, String ), method : Method, action : String, parsed : Validated error parsed } -> msg) -> Options error parsed input oldMsg extras -> Options error parsed input msg extras

You can add an onSubmit handler to the Form's Options. If you are using a framework that is integrated with elm-form (such as elm-pages), then you can rely on the framework's onSubmit behavior. Otherwise, you will need to do something with the form when there is a valid form submission.

There are a few different approaches you can use.

  1. Progressively enhance the raw FormData submission. Since withOnSubmit gives you access to { fields : List ( String, String ) {- ... -} }, you can propagate the raw key-value pairs (fields) and send those to your API. If you are doing full-stack Elm with elm-pages or Lamdera, then this can be a great fit because you can do code sharing and re-use your Form definition on the backend to parse the raw FormData. However, you may not want to use this approach with frontend-only Elm because you may prefer to communicate with your backend using more structured data like JSON rather than FormData (which is just key-value strings).
  2. Parse into your preferred type, then with an on-submit Msg, check if the data is Valid, and if it is, use the parsed data to make a request to your API (by JSON-encoding the value, building a GraphQL request, etc.).
  3. In your Form's combine, directly parse into a representation of your request, such as a Json.Encode.Value, a Cmd Msg, a Task error Msg, or an intermediary data type that represents an API request.

Let's look at an example of approach (3). In this example, we define a Request record alias which represents an API request. Note, there is nothing special about this Request type, this is just an example ot illustrate this general pattern, but consider the best types for your use case when you adapt this example for your app.

import Form
import Form.Field as Field
import Form.Validation as Validation

type alias Request =
    { path : String
    , body : Encode.Value
    , expect : Http.Expect Msg
    }

sendRequest : Request -> Cmd Msg
sendRequest request =
    Http.post
        { url = "https://myservice.com/api" ++ request.path
        , body = Http.jsonBody request.body
        , expect = request.expect
        }

userProfileForm : Form.HtmlForm String Request input msg
userProfileForm =
    (\name email ->
        { combine =
            Validation.succeed
                (\nameValue emailValue ->
                    { path = "/api/user"
                    , body =
                        Encode.object
                            [ ( "name", Encode.string nameValue )
                            , ( "email", Encode.string emailValue )
                            ]
                    }
                , expect = Http.expectJson GotUpdatedProfile profileDecoder
                )
                |> Validation.andMap name
                |> Validation.andMap email
        , view = \context -> [{- ... view here -}]
        }
    )
        |> Form.form
        |> Form.field "name" (Field.text |> Field.required "Required")
        |> Form.field "email" (Field.text |> Field.required "Required")

withServerResponse : Maybe (ServerResponse error) -> Options error parsed input msg extras -> Options error parsed input msg extras

You can render your Form with an initial set of values and errors that semantically represent a server response.

Conceptually, this is like sending a traditional form submission to a backend. When this happens in a <form> with no JavaScript event handlers, the server responds with a new page load, and that newly rendered page needs to contain any field errors and persist any field values that were submitted so the user can continue filling out their form.

In an elm-pages app, you can submit your forms with JavaScript turned off and see this exact behavior, but you need to be sure to wire in a ServerResponse so that the form state is persisted in the freshly rendered page.

You can also use this ServerResponse to send down server-side errors, especially if you are using full-stack Elm.

withGetMethod : Options error parsed input msg extras -> Options error parsed input msg extras

The default Method from options is Post since that is the most common. The Get Method for form submissions will add the form fields as a query string and navigate to that route using a GET. You will need to progressively enhance your onSubmit to simulate this browser behavior if you want something similar, or use a framework that has this simulation built in like elm-pages.


type Method
    = Get
    | Post

An HTTP method to use for the form submission. The default when you build Options with Form.options is Post.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method for more about the default browser behavior.

Note that the default browser behavior can be simulated with client-side code using progress enhancement principles, but you'll need to implement that yourself to get that same behavior unless you're using a framework that has it built in like elm-pages.

methodToString : Method -> String

Turn a Method into "GET" or "POST".

Running Parsers

parse : String -> Model -> input -> Form error { info | combine : Pages.Internal.Validation error parsed named constraints } parsed input -> Validated error parsed

Try parsing the Form. Usually not needed directly, usually it's better to use Form.Handler to try parsing one of multiple Forms.


type Validated error value
    = Valid value
    | Invalid (Maybe value) (Dict String (List error))

Parsing a Form will either give you a Valid type with the parsed value (nothing went wrong), or else an Invalid type if it encountered any validation errors. The Invalid type contains a Dict of errors (the keys are the field names).

The Invalid data also contains the parsed value if it is able to parse it. For example, if you have a Field defined with

Form.form
    (\checkin ->
        { combine =
            Validation.succeed Checkin
                |> Validation.andMap checkin
        , view = [{- ... view here -}]
        }
    )
    |> Form.field "checkin"
        (Field.time { invalid = \_ -> "Invalid time value" }
            |> Field.withMin
                { hours = 10
                , minutes = 0
                }
                "Check-in must be after 10"
            |> Field.withMax
                { hours = 12
                , minutes = 0
                }
                "Check-in must be before noon"
        )

A time of 7:23 would be parsed as Invalid (Just { hours = 7, minutes = 23 }) (Dict.fromList [ ( "checkin", [ "Check-in must be after 10" ] ) ]). Because the Field.withMin validation is recoverable, it is able to add a validation error while still successfully parsing to a Time value.

Progressively Enhanced Form Techniques (elm-pages)

Hidden Fields

Hidden fields are a useful technique when you are progressively enhancing form submissions and sending the key-value form data directly. In elm-pages apps this is used often and is an idiomatic approach. If you are wiring up your own onSubmit with a Msg and never submit the forms directly, then you will likely include additional context as part of your Msg instead of through hidden fields.

hiddenField : String -> Field error parsed input initial kind constraints -> Form error (Validation.Field error parsed FieldView.Hidden -> combineAndView) parsedCombined input -> Form error combineAndView parsedCombined input

Declare a hidden field for the form.

Unlike field declarations which are rendered using Form.FieldView functions, hiddenField inputs are automatically inserted into the form when you render it.

You define the field's validations the same way as for field, with the Form.Field API.

form =
    Form.form
        (\quantity productId ->
            { combine = {- combine fields -}
            , view = \info -> [{- render visible fields -}]
            }
        )
        |> Form.field "quantity"
            (Field.int |> Field.required "Required")
        |> Form.field "productId"
            (Field.text
                |> Field.required "Required"
                |> Field.withInitialValue (\product -> Form.Value.string product.id)
            )

hiddenKind : ( String, String ) -> error -> Form error combineAndView parsed input -> Form error combineAndView parsed input

Like hiddenField, but uses a hardcoded value. This is useful to ensure that your Form.Handler is parsing the right kind of Form when there is more than one kind of Form on a given page.

updateProfile : Form.HtmlForm String ( String, String ) input msg
updateProfile =
    Form.form
        (\first last ->
            { combine =
                Validation.succeed Tuple.pair
                    |> Validation.andMap first
                    |> Validation.andMap last
            , view = \_ -> []
            }
        )
        |> Form.field "first" (Field.text |> Field.required "Required")
        |> Form.field "last" (Field.text |> Field.required "Required")
        |> Form.hiddenKind ( "kind", "update-profile" ) "Expected kind"

Dynamic Fields

dynamic : (decider -> Form error { combine : Pages.Internal.Validation error parsed named constraints1, view : subView } parsed input) -> Form error ({ combine : decider -> Pages.Internal.Validation error parsed named constraints1, view : decider -> subView } -> combineAndView) parsed input -> Form error combineAndView parsed input

Allows you to render a Form that renders a sub-form based on the decider value.

For example, here is a Form that shows a dropdown to select between a Post and a Link, and then renders the linkForm or postForm based on the dropdown selection.

import Form.Handler
import Form.Validation as Validation
import Form.Field as Field

type PostAction
    = ParsedLink String
    | ParsedPost { title : String, body : Maybe String }


type PostKind
    = Link
    | Post

dependentForm : Form.HtmlForm String PostAction input msg
dependentForm =
    Form.form
        (\kind postForm_ ->
            { combine =
                kind
                    |> Validation.andThen postForm_.combine
            , view = \_ -> []
            }
        )
        |> Form.field "kind"
            (Field.select
                [ ( "link", Link )
                , ( "post", Post )
                ]
                (\_ -> "Invalid")
                |> Field.required "Required"
            )
        |> Form.dynamic
            (\parsedKind ->
                case parsedKind of
                    Link -> linkForm
                    Post -> postForm
            )

linkForm : Form.HtmlForm String PostAction input msg
linkForm =
    Form.form
        (\url ->
            { combine =
                Validation.succeed ParsedLink
                    |> Validation.andMap url
            , view = \_ -> []
            }
        )
        |> Form.field "url"
            (Field.text
                |> Field.required "Required"
                |> Field.url
            )


postForm : Form.HtmlForm String PostAction input msg
postForm =
    Form.form
        (\title body ->
            { combine =
                Validation.succeed
                    (\titleValue bodyValue ->
                        { title = titleValue
                        , body = bodyValue
                        }
                    )
                    |> Validation.andMap title
                    |> Validation.andMap body
                    |> Validation.map ParsedPost
            , view = \_ -> []
            }
        )
        |> Form.field "title" (Field.text |> Field.required "Required")
        |> Form.field "body" Field.text


Form.Handler.run
    [ ( "kind", "link" )
    , ( "url", "https://elm-radio.com/episode/wrap-early-unwrap-late" )
    ]
    (dependentForm |> Form.Handler.init identity)

--> (Valid (ParsedLink "https://elm-radio.com/episode/wrap-early-unwrap-late"))

Wiring

elm-form manages the client-side state of fields, including FieldStatus which you can use to determine when in the user's workflow to show validation errors.


type alias Msg msg =
Internal.FieldEvent.Msg msg

When you render a Form using Form.renderHtml or Form.renderStyledHtml, you will pass in a toMsg to turn a Form.Msg into your application's Msg. That means you'll often have a Msg type like

import Form

type Msg
    = FormMsg Form.Msg

-- | ... other Msg's

In an elm-pages application, you will render your Form using Pages.Form.renderHtml (or renderStyledHtml) and the msg type is a PagesMsg.PagesMsg, which is a framework-provided Msg with all of the glue handled at the framework-level. You can also use a similar pattern in your own applications to reduce the wiring for each new page in your app.

init : Model

Initialize the Form.Model.

import Form

init : Flags -> ( Model, Cmd Msg )
init flags =
    ( { formModel = Form.init
      , submitting = False
      }
    , Cmd.none
    )

update : Msg msg -> Model -> ( Model, Platform.Cmd.Cmd msg )

Update the Form.Model given the Form.Msg and the previous Form.Model. See the README's section on Wiring.

Model


type alias Model =
Dict String FormState

The state for all forms. This is a single value that can be used to manage your form state, so when you render your Forms you will get client-side validations based on the state managed through this value. The state that is included here for each Form is:

Since this manages the state of multiple Forms, you can even maintain this in your application-wide Model rather than in a page-specific Model. In an elm-pages application, this is managed through the framework, but you can achieve a similar wiring by managing the Form.Model globally.

In more advanced cases, you can manually modify the state of a form. However, it's often enough to just let this package manage the state for you through the Form.update function. Since this is a Dict String FormState, you can use Dict operations to clear or update the state of forms if you need to manually manage form state.


type alias FormState =
{ fields : Dict String FieldState
, submitAttempted : Basics.Bool 
}

The state for an individual Form. Model is a Dict String FormState, so it can contain the state for multiple forms.


type alias FieldState =
{ value : String
, status : Validation.FieldStatus 
}

The state for an individual form field. Since elm-form manages state for you, it tracks both the values and FieldStatus for all fields.


type alias ServerResponse error =
{ persisted : { fields : Maybe (List ( String
, String ))
, clientSideErrors : Maybe (Dict String (List error)) }
, serverSideErrors : Dict String (List error) 
}

The persisted state will be ignored if the client already has a form state. It is useful for persisting state between page loads. For example, elm-pages server-rendered routes use this persisted state in order to show client-side validations and preserve form field state when a submission is done with JavaScript disabled in the user's browser.

serverSideErrors will show on the client-side error state until the form is re-submitted. For example, if you need to check that a username is unique, you can do so by including an error in serverSideErrors in the response back from the server. The client-side form will show the error until the user changes the username and re-submits the form, allowing the server to re-validate that input.

mapMsg : (msg -> msgMapped) -> Msg msg -> Msg msgMapped

Lets you map a user msg within a Form.Msg.

toResult : Validated error value -> Result ( Maybe value, Dict String (List error) ) value

Parsing a Form gives you a Validated type. This helper turns it into a Result that is semantically the same. This can be useful for using a parsed Form value in a pipeline.