Live Ellie demo: https://ellie-app.com/mzjFg6BWmMta1
elm-form
is built around the idea
of managing a single Form.Model
value as an unparsed set of raw field values and FieldStatus
(blurred, changed, etc.).
This Form.Model
can even handle form state of more than one form on a page, or even across multiple pages.
The package manages all of the unparsed state for you with a single Msg
, a single Model
entry, and then
uses your Form
definition to run its validations against the unparsed values (Model
),
and to render the form fields along with any validation errors.
If you use elm-form
with elm-pages
, the wiring is built into the framework so you don't need to wire in update
or Model
yourself, and the framework manages additional Form state for you such as in-flight form submissions. The ideas in this
package originally came from elm-pages
, but they are useful in a standalone context as well so this was split into
a separate package.
Some of these underlying ideas were discussed in the Elm Radio episode Exploring a New Form API Design.
- Progressive Enhancement - to make things more robust, and to leverage existing standards to get features for free instead of re-inventing them for every app. Forms are great for sending data to servers, lets use them! Let's go back to our Web fundamentals - it's worth reading the MDN docs on form submissions to understand the core building blocks.
- Accessibility - using standards to provide an experience that supports a broad range of users and use cases
Form.Validation
lets you build up validations and parse fields into a combined value in the same pass (if you wanted to, you could even parse into aJson.Encode.Value
or some payload to send to an APIonSubmit
)Form.Field
lets you declare the fields (in the applicative pipeline in the Form definition)- You can pass an input value when you render the form which can be used in rendering the view, and for getting initial values (
withInitialValue
)
- Forms are always rendered within a
<form>
element for accessibility, and to enable progressive enhancement - Fields are always rendered within a form field element of some kind ( or <textarea>). Rendering the view for Fields with the appropriate attributes is done by the
Form.FieldView
module. It can be rendered aselm/html
orelm-css
elements (since those are the two basic ways to render semantic HTML field tags,<input>
and<textarea>
).elm-ui
doesn't currently have a way to render semantic HTML tags for forms (<form>
tag), so there aren't anyelm-ui
helpers at the moment, though you can always render toelm/html
.
Many Elm form examples and APIs use the pattern of handling each changed field within
the update
function. For example, elm-spa-example
uses this pattern in the Settings route (and throughout the app).
❗️🛑 NOTE: This code below is NOT the pattern this package is built on ❗️🛑
type alias Model =
{ username : String
, avatar : String
-- ... an entry for each form field here
-- ... any additional app state
}
update msg model =
EnteredUsername username ->
updateForm (\form -> { form | username = username }) model
EnteredAvatar avatar ->
updateForm (\form -> { form | avatar = avatar }) model
-- .. additional handling for the remaining form fields
viewForm form =
Html.form [ onSubmit (SubmittedForm form) ]
[ input
[ onInput EnteredAvatar
, value form.avatar
-- other attributes
]
[]
-- , ... input elements for other form fields
]
This package tries to reduce boilerplate and manage form validations in a more declarative style
by parsing/validating the form as a whole rather than parsing/validating
each individual field. Here is the same Settings route with elm-pages
and elm-form
for reference.
✅👇 NOTE: the code below is the wiring pattern we use in this package. ✅👇
Instead of wiring in different Msg's and Model fields for each individual form field, the wiring in this package is done once for all form state like this:
type Msg
= FormMsg (Form.Msg Msg)
| OnSubmit (Form.Validated String SignUpForm)
-- | ... Other Msg's for your app
type alias Model =
{ formModel : Form.Model
, submitting : Bool
-- , ... additional state for your app
}
init : Flags -> ( Model, Cmd Msg )
init flags =
( { formModel = Form.init
, submitting = False
}
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnSubmit parsed ->
case parsed of
Form.Valid signUpData ->
( { model | submitting = True }
, sendSignUpData signUpData )
Form.Invalid _ _ ->
-- validation errors are displayed already so
-- we don't need to do anything else here
( model, Cmd.none )
FormMsg formMsg ->
let
( updatedFormModel, cmd ) =
Form.update formMsg model.formModel
in
( { model | formModel = updatedFormModel }, cmd )
formView : Model -> Html Msg
formView model =
signUpForm
|> Form.renderHtml
{ submitting = model.submitting
, state = model.formModel
, toMsg = FormMsg
}
(Form.options "form"
|> Form.withOnSubmit (\{parsed} -> OnSubmit parsed)
)
[]
-- this is our parsed/validated type, but it can be anything we want,
-- including Json.Encode.Value, etc.
type alias SignUpForm =
{ username : String, password : String }
signUpForm : Form.HtmlForm String SignUpForm input msg
signUpForm =
(\username password passwordConfirmation ->
{ combine =
Validation.succeed SignUpForm
|> Validation.andMap username
|> Validation.andMap
(Validation.map2
(\passwordValue passwordConfirmationValue ->
if passwordValue == passwordConfirmationValue then
Validation.succeed passwordValue
else
Validation.fail "Must match password" passwordConfirmation
)
password
passwordConfirmation
|> Validation.andThen identity
)
, view =
\formState ->
let
fieldView label field =
Html.div []
[ Html.label []
[ Html.text (label ++ " ")
, FieldView.input [] field
, errorsView formState field
]
]
in
[ fieldView "username" username
, fieldView "Password" password
, fieldView "Password Confirmation" passwordConfirmation
, if formState.submitting then
Html.button
[ Html.Attributes.disabled True ]
[ Html.text "Signing Up..." ]
else
Html.button [] [ Html.text "Sign Up" ]
]
}
)
|> Form.form
|> Form.field "username" (Field.text |> Field.required "Required")
|> Form.field "password" (Field.text |> Field.password |> Field.required "Required")
|> Form.field "password-confirmation" (Field.text |> Field.password |> Field.required "Required")
This package is designed to be hooked into frameworks, whether it's a published framework like elm-pages (which has a built-in integration), or your own internal framework. See the elm-pages docs for more details on how to render and submit your form using elm-pages.