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.withMinLength 4 "Must be at least 4 characters"
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" ]
]A Form that renders to elm/html. Can be rendered with renderHtml.
A Form that renders to rtfeldman/elm-css. Can be rendered with renderStyledHtml.
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")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")The data available as the first parameter in a Form's view function.
The current validation errors for the given Form.
You can get the errors for a specific field using errorsForField.
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 [] []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.
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.
Same as renderStyledHtml but accepts a
nonce to pass to
the generated <style> tag
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.
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 []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:
view through the Context argument's input field.Form.Field.withInitialValueOne 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..."
]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.
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.
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).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.).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")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.
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.
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.
Turn a Method into "GET" or "POST".
Try parsing the Form. Usually not needed directly, usually it's better to use Form.Handler
to try parsing one of multiple Forms.
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.
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.
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"))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.
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'sIn 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.
Initialize an empty form model. This managed state for Form.Model.
import Form
init : Flags -> ( Model, Cmd Msg )
init flags =
( { formModel = Form.init
, submitting = False
}
, Cmd.none
)Update the Form.Model given the Form.Msg and the previous Form.Model. See the
README's section on Wiring.
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:
Form.Validation.FieldStatus for each field in the formSince 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.
The state for an individual Form. Model is a Dict String FormState, so it can contain the state for multiple forms.
The state for an individual form field. Since elm-form manages state for you, it tracks both the values and FieldStatus
for all fields.
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.
Lets you map a user msg within a Form.Msg.
A
Formdefinition representsview, andcombinethe fields into a parsed valueA
Formcan be used to:Render Form
<form>tag (usingrenderHtmlorrenderStyledHtml)Parse Form
parseinto aValidatedvalueFormdefinitions usingForm.Handler.While you almost always will want to render your
Formin yourviewfunction, 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
Formdefinition between your frontend and backend.elm-pageshas 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.