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, div
s, 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" ]
]
Internal.Form error combineAndView parsed input
A Form
definition represents
view
, andcombine
the fields into a parsed valueA Form
can be used to:
<form>
tag (using renderHtml
or renderStyledHtml
)parse
into a Validated
valueForm
definitions using Form.Handler
.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.
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
.
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")
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")
{ errors : Errors error
, submitting : Basics.Bool
, submitAttempted : Basics.Bool
, input : input
}
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
.
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 [] []
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.
{ 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:
view
through the Context
argument's input
field.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.
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")
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
.
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".
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 Form
s.
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.
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 : (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"))
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.
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.
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
Form
s 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.
{ 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.
{ 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.
{ 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.