Form

One of the core features of elm-pages is helping you manage form data end-to-end, including

  • Presenting the HTML form with its fields
  • Maintaining client-side form state
  • Showing validation errors on the client-side
  • Receiving a form submission on the server-side
  • Using the exact same client-side validations on the server-side
  • Letting you run server-only Validations with DataSource's (things like checking for a unique username)

Because elm-pages is a framework, it has its own internal Model and Msg's. That means you, the user, can offload some of the responsibility to elm-pages and build an interactive form with real-time client-side state and validation errors without wiring up your own Model and Msg's to manage that state. You define the source of truth for your form (how to parse it into data or errors), and elm-pages manages the state.

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

Step 1 - Define the Form

What to look for:

The field declarations

Below the Form.init 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).

There will be a corresponding parameter in the function we pass in to Form.init 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

Totally customizable. Uses Form.FieldView to render all of the fields declared.

import DataSource exposing (DataSource)
import ErrorPage exposing (ErrorPage)
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 NewUser =
    { email : String
    , password : String
    }

signupForm : Form.HtmlForm String NewUser () Msg
signupForm =
    Form.init
        (\email password passwordConfirmation ->
            { combine =
                Validation.succeed Login
                    |> Validation.andMap email
                    |> Validation.andMap
                        (Validation.map2
                            (\pass confirmation ->
                                if pass == confirmation then
                                    Validation.succeed pass

                                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.isTransitioning 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 data
    -> 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" ]
        ]

Step 2 - Render the Form's View

view maybeUrl sharedModel app =
    { title = "Sign Up"
    , body =
        [ form
            |> Form.toDynamicTransition "login"
            |> Form.renderHtml [] Nothing app ()
        ]
    }

Step 3 - Handle Server-Side Form Submissions

action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
action routeParams =
    Request.formData [ signupForm ]
        |> Request.map
            (\signupResult ->
                case signupResult of
                    Ok newUser ->
                        newUser
                            |> myCreateUserDataSource
                            |> DataSource.map
                                (\() ->
                                    -- redirect to the home page
                                    -- after successful sign-up
                                    Route.redirectTo Route.Index
                                )

                    Err _ ->
                        Route.redirectTo Route.Login
                            |> DataSource.succeed
            )

myCreateUserDataSource : DataSource ()
myCreateUserDataSource =
    DataSource.fail
        "TODO - make a database call to create a new user"

Building a Form Parser

type Form error combineAndView input
type alias HtmlForm error parsed input msg =
error
{ combine : Combined error parsed
, view : Context error input -> List (Html (Msg msg))
}
input
type alias StyledHtmlForm error parsed data msg =
error
{ combine : Combined error parsed
, view : Context error data -> List (Html (Msg msg))
}
data
type alias DoneForm error parsed data view =
error
{ combine : Combined error parsed
, view : Context error data -> view
}
data
type Response error
= Response
{ fields : List ( String, String )
, errors : Dict String (List error)
}
init : combineAndView -> Form String combineAndView data

Adding Fields

String
-> Field error parsed data kind constraints
-> Form error (Field error parsed kind -> combineAndView) data
-> Form error combineAndView data

Declare a visible field for the form.

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

form =
    Form.init
        (\email ->
            { combine =
                Validation.succeed NewUser
                    |> Validation.andMap email
            , view = \info -> [{- render fields -}]
            }
        )
        |> Form.field "email"
            (Field.text |> Field.required "Required")
String
-> Field error parsed data kind constraints
-> Form error (Field error parsed Hidden -> combineAndView) data
-> Form error combineAndView data

Declare a hidden field for the form.

Unlike field declarations which are rendered using Form.ViewField 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.init
        (\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)
            )
( String, String )
-> error
-> Form error combineAndView data
-> Form error combineAndView data

View Functions

type alias Context error data =
{ errors : Errors error
, isTransitioning : Bool
, submitAttempted : Bool
, data : data
}

Rendering Forms

List (Attribute (Msg msg))
-> Maybe
{ fields : List ( String, String )
, errors : Dict String (List error)
}
-> AppContext app actionData
-> data
error
(Validation error parsed named constraints)
data
(Context error data -> List (Html (Msg msg)))
msg
-> Html (Msg msg)
List (Attribute (Msg msg))
-> Maybe
{ fields : List ( String, String )
, errors : Dict String (List error)
}
-> AppContext app actionData
-> data
error
(Validation error parsed named constraints)
data
(Context error data -> List (Html (Msg msg)))
msg
-> Html (Msg msg)
type FinalForm error parsed data view userMsg
FinalForm error parsed data view userMsg
-> FinalForm error parsed data view userMsg
String
-> Form
error
{ combine : Validation error parsed field constraints
, view : Context error data -> view
}
data
error
(Validation error parsed field constraints)
data
(Context error data -> view)
userMsg
String
-> Form
error
{ combine : Validation error parsed field constraints
, view : Context error data -> view
}
data
error
(Validation error parsed field constraints)
data
(Context error data -> view)
userMsg

Showing Errors

type Errors error
errorsForField : Field error parsed kind -> Errors error -> List error

Running Parsers

String
-> AppContext app actionData
-> data
-> Form
error
{ info | combine : Validation error parsed named constraints }
data
-> ( Maybe parsed, Dict String (List error) )
List ( String, String )
-> Form error (Validation error parsed kind constraints) data
-> ( Bool, ( Maybe parsed, Dict String (List error) ) )
List ( String, String )
-> ServerForms error parsed
-> ( Maybe parsed, Dict String (List error) )

Combining Forms to Run on Server

type ServerForms error parsed
= ServerForms (List (Form error (Combined error parsed) Never))
(parsed -> combined)
-> Form
error
{ combineAndView
| combine : Validation error parsed kind constraints
}
input
-> ServerForms error combined
(parsed -> combined)
-> Form
error
{ combineAndView
| combine : Validation error parsed kind constraints
}
input
-> ServerForms error combined
-> ServerForms error combined
(parsed -> combined)
-> Form
error
{ combineAndView
| combine :
Combined
error
(DataSource (Validation error parsed kind constraints))
}
input
error
(DataSource (Validation error combined kind constraints))
(parsed -> combined)
-> Form
error
{ combineAndView
| combine :
Combined
error
(DataSource (Validation error parsed kind constraints))
}
input
error
(DataSource (Validation error combined kind constraints))
error
(DataSource (Validation error combined kind constraints))

Dynamic Fields

( decider
-> Form
error
{ combine : Validation error parsed named constraints1
, view : subView
}
data
)
-> Form
error
( { combine : decider -> Validation error parsed named constraints1
, view : decider -> subView
}
-> combineAndView
)
data
-> Form error combineAndView data
type alias AppContext app actionData =
{ app
| path : Path
, transition : Maybe Transition
, fetchers : Dict String (FetcherState actionData)
, pageFormState :
Dict
String
{ fields :
Dict
String
{ value : String
, status : FieldStatus
}
, submitAttempted : Bool
}
}

Submission

error
{ combine : Validation error combined kind constraints
, view : viewFn
}
data
-> Form
error
{ combine :
Validation
error
(DataSource (Validation error combined kind constraints))
kind
constraints
, view : viewFn
}
data
({ fields : List ( String, String ) } -> userMsg)
-> FinalForm error parsed data view userMsg
-> FinalForm error parsed data view userMsg