Sometime Elm’s newcomers complain about the complexity and the large amount of boilerplate needed to create forms.
Forms are the main interaction point between users and applications and should be simple to build, but also reliable and usable.
This post is about my journey in learning form with Elm.
The styling of this project is done mainly on a separate CSS file. It is not well made as this tutorial is focused on the code, not on the style. Find at the bottom references for better styling tools.
Code and Demo
If you are the type of person that prefer to jump to the final code, here you are:
- Code: https://github.com/lucamug/elm-form-examples/blob/master/src/Example_18.elm
- Demo: http://guupa.com/elm-form-examples/Example_18.html
- Ellie: https://ellie-app.com/qc6qQG93ya1/0
History Playback
We are writing code so that the user interaction would replay well during the History Playback, for debugging. For example is possible to see which field users put the focus on and the text modification while the user is typing:
Part 1 — x-www-form-urlencoded
and json
encoding, validation
This is part 1 of a 3 parts series.
Parts
- Part 1 — Examples 1~6:
x-www-form-urlencoded
andjson
encoding, validation - Part 2 — Examples 7~12: Removing
<form>
, on-the-fly validation, Focus detection, Show/Hide the password - Part 3 — Examples 13~21: Spinner, Floating Labels, Checkboxes, Date Picker, Autocomplete
☞ Example 1 — An old-fashion form in Elm
Let’s start with a simple exercise. Build an old-fashion form, the way we were doing in ancient time, but using Elm as Html generator. This is the Elm code:
Html.form [ action Utils.urlMirrorService, method "post"]
[ label []
[ text "Email"
, input [ type_ "text", name "email" ] []
]
, label []
[ text "Password"
, input [ type_ "password", name "password" ] []
]
, button [] [ text "Submit" ]
]
And this is the resulting Html:
<form action="http://httpbin.org/post" method="post">
<label>
Email
<input type="text" name="email">
</label>
<label>
Password
<input type="password" name="password">
</label>
<button>Submit</button>
</form>
Nothing special here, it is exactly what an Html form looks like. You can just appreciate the simplicity of creating Html using Elm.
This form, as per default, send a POST request in x-www-form-urlencoded
format, where the values are percent-encoded
and concatenated in key-value tuples separated by &
, with a =
between the key and the value.
On submitting the form, this is the specific text sent in the POST body request:
email=foo&password=bar
We use httpbin.org as a service to receive the data and echo it back in a page. Unfortunately this service doesn’t return the content of the body as it is, but it converts it to Json.
In the response, among other data, you can find:
{
"data": "",
"form": {
"email": "foo",
"password": "bar"
},
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
...
},
...
}
The form
key is the one that store the parsed data that we sent to the server.
On clicking the submit button, the browser will move out from our Elm app and this is not exactly what a nice and fast Single Page Application should do.
So let’s Elm-etize it!
☞ Example 2 — Let’s change to real Elm form
There are several things to add/modify here:
1. Create a model structure where to store the data of the form (password and email in our example)
2. Create the necessary messages to handle the data flow:
type Msg
= NoOp
| SubmitForm
| SetEmail String
| SetPassword String
| Response (Result Http.Error String)
3. Create a data type for the Form field. This will make the compiler helping us during the development because it will complain every time we forget to handle one field of the form
type FormField
= Email
| Password
4. Create the update function that will handle the messages. The bold line in the SubmitForm
section will create an Http.send
command (as data) and send it to the Elm runtime to handle it.
The two Response
section will handle the result of the Http.send
command, one in case of success (Ok
), the other in case of failure (Err
).
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case Debug.log "msg" msg of
NoOp ->
( model, Cmd.none ) SubmitForm ->
( { model | response = Nothing }
, Http.send Response (postRequest model)
) SetEmail email ->
( { model | email = email }, Cmd.none ) SetPassword password ->
( { model | password = password }, Cmd.none ) Response (Ok response) ->
( { model | response = Just response }, Cmd.none ) Response (Err error) ->
( { model | response = Just (toString error) }, Cmd.none )
5. Create the function postRequest
that we used during the creating of Http.send
command. This function return an Http.request
that describe the HTTP request. We want to simulate the default behaviour of the form, sending the data as x-www-form-urlencoded
so we manually join all data together and assigned it to body
. Then we use Http.request
combined with Http.expectString
because we are only interested in receiving the result as string, without parsing it.
postRequest : Model -> Http.Request String
postRequest model =
let
body =
formUrlencoded
[ ( "email", model.email )
, ( "password", model.password )
]
|> Http.stringBody "application/x-www-form-urlencoded"
in
Http.request
{ method = "POST"
, headers = []
, url = Utils.urlMirrorService
, body = body
, expect = Http.expectString
, timeout = Nothing
, withCredentials = False
}
Where formUrlencoded
is a small helper that percent-encode and concatenate together the form values:
formUrlencoded : List ( String, String ) -> String
formUrlencoded object =
object
|> List.map
(\( name, value ) ->
Http.encodeUri name
++ "="
++ Http.encodeUri value
)
|> String.join "&"
6. Update the view removing unnecessary stuff and adding the needed one:
- Remove
action
andmethod
parameters from the<form>
element - Add
onSubmit
event so that we take control when the form is submitted - Remove the name attribute of the input fields
- Add
onInput
in each input element so that we can update the model when the content is changing - Add the
value
parameter. This is not compulsory but give the nice effect or updating the input fields while replaying the History Explorer
So the new view is now:
viewForm : Model -> Html Msg
viewForm model =
Html.form
[ onSubmit SubmitForm
, class "form-container"
]
[ label []
[ text "Email"
, input
[ type_ "text"
, placeholder "Email"
, onInput SetEmail
, value model.email
]
[]
]
, label []
[ text "Password"
, input
[ type_ "password"
, placeholder "Password"
, onInput SetPassword
, value model.password
]
[]
]
, button
[]
[ text "Submit" ]
]
Done!
The work done to convert from a standard Html form to an Elm form was not that short. From now on everything will be simpler and the benefits huge.
☞ Example 3 — Moving to Json
Unless the server is forcing you to stick to x-www-form-urlencoded
, is time to move to Json. Let’s change the body part of the post request. From:
body =
formUrlencoded
[ ( "email", model.email )
, ( "password", model.password )
]
|> Http.stringBody "application/x-www-form-urlencoded"
to:
body =
Encode.object
[ ( "email", Encode.string model.email )
, ( "password", Encode.string model.password )
]
|> Http.jsonBody
So instead of manually encoding stuff, we used Encode.object
and Encode.string
. Moreover we replace Http.stringBody
with Http.jsonBody
.
Now the response changed from
{
"data": "",
"form": {
"email": "foo",
"password": "bar"
},
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
...
},
...
}
to
{
"data": "{\"email\":\"foo\",\"password\":\"bar\"}",
"form": {},
"headers": {
"Content-Type": "application/json",
...
},
...
}
☞ Example 4 — Adding Validation
Let’s see what it takes to add some client side validation.
1. Add a list of Error in the model and a new type alias:
type alias Error =
( FormField, String )
2. Create a validate function using rtfeldman/elm-validate
validate : Model -> List Error
validate =
Validate.all
[ .email >> Validate.ifBlank ( Email, "Email can't be
blank." )
, .password >> Validate.ifBlank ( Password, "Password can't
be blank." )
]
3. Create a function that filter errors field by field so is possible to display them next to each field in the form:
viewFormErrors : FormField -> List Error -> Html msg
viewFormErrors field errors =
errors
|> List.filter (\( fieldError, _ ) -> fieldError == field)
|> List.map (\( _, error ) -> li [] [ text error ])
|> ul [ class "formErrors" ]
and add these section inside the form with
viewFormErrors Email model.errors
and
viewFormErrors Password model.errors
4. Modify the SubmitForm handling in the update function form
SubmitForm ->
( { model | response = Nothing }
, Http.send Response (postRequest model)
)
to
SubmitForm ->
case validate model of
[] ->
( { model | errors = [], response = Nothing }
, Http.send Response (postRequest model)
) errors ->
( { model | errors = errors }
, Cmd.none
)
So that if there are errors, the forms is not submitted (Cmd.none).
☞ Example 5 — Moved the field updates out of the update function
To reduce the size of the update function we can apply these modifications
1. Consolidate all messages related to update form fields into one. From:
type Msg
= NoOp
| SubmitForm
| SetEmail String
| SetPassword String
| Response (Result Http.Error String)
to
type Msg
= NoOp
| SubmitForm
| SetField FormField String
| Response (Result Http.Error String)
2. Create an helper function:
setField : Model -> FormField -> String -> Model
setField model field value =
case field of
Email ->
{ model | email = value } Password ->
{ model | password = value }
3.In the update function replace
SetEmail email ->
( { model | email = email }, Cmd.none )SetPassword password ->
( { model | password = password }, Cmd.none )
with
SetField field value ->
( setField model field value, Cmd.none )
4. In the view, replace
onInput SetEmail
onInput SetPassword
with
onInput <| SetField Email
onInput <| SetField Password
There is a way to simplify even further so to reduce the number of code but this would necessarily loose type safety. That means that the field name will be stored and passed as string so the compiler will not be able to cover you in case you are missing something os mistyping something. To move forward in this direction have a look at the excellent package etaque/elm-form
, you can find here an excellent example with client side validation.
☞ Example 6 — Replaced the <form> element with <div> and adding ”onClick SubmitForm” to the button
What is about getting rid of the <form>
element now? We can.
In the view:
1. Let’s replace Html.form
with Html.div
and remove the onSubmit SubmitForm
2. Let’s add onClick SubmitForm
to de button
Let’s also add
classList
[ ( "disabled", not <| List.isEmpty model.errors ) ]
to the button, so that when there are errors, the buttons will appear disabled.
So, now we are completely disconnected from the <form>
element. One issue that we are facing is the submission by pressing Enter is not working anymore but we can bring it back:
Consider that there are library, like mdgriffith/style-element
, that don’t even expose a form element. In this case, this is the only “clean” way of creating forms, unless you play with nodes with something like
node “form” <| el YourStyle [ onClick YourMsg ] []
Resources
- The SPA made by Richard Feldman was the main source if inspiration. Here, for example, there is the sign in form and the related source code.
- In the presentation “The life of a file” by Evan Czaplicki there are interesting cases of thinking a data structure to support forms for ordering fruits.
etaque/elm-form
by Emilien Taque is a tool to create large forms. From the package documentation: “For when the classical “a message per field” doesn’t work well for you, at the price of loosing some type-safety”. Here an example of form made with etaque/elm-form andbootstrap
, here the source code. Recently it is adding the validation onBlur.style-elements
is a library not specifically designed for creating forms but has an interesting concept of separation between layout and style. This is a form example made with style-elements.- How to do client-side form validation with Elm by Matthew Griffith, the same author of
style-elements
, with a working demo. - Official Elm documentation about forms and related demo.
rtfeldman/elm-validate
, a library for form validation by Richard Feldmanbillperegoy/elm-form-validations
is a validation library by Bill Peregoy and its related post.- Model View Update — Part 2, Building a Sign-Up Form, an easy to follow tutorial with a section about 3 way if implementing CSS in Elm. Doesn’t cover yet the submission of a form.
- Form design with elm-bootstrap.
Older resources
- Building a Live-Validated Signup Form in Elm, a NoRedInk article for Elm 0.17
- Real World Elm — Part 2 — Form Validation by Michel Rijnders for Elm 0.17
Examples
- http://rtfeldman.github.io/elm-spa-example-with-debug/#/login (code)
- http://etaque.github.io/elm-form/example/ (code)
- http://guupa.com/elm-style-elements-examples/html/Form.html (code)
- https://ellie-app.com/38v2srzgPgka1/3
- http://elm-bootstrap.info/form
- https://ellie-app.com/rbrF83qNya1/0 (code)
Styling resources
- Bulma CSS framework
- Bootstrap CSS framework
elm-mdl
, port of Google’s Material Design Lite CSS/JS implementation of the Material Design Specification.
__END_OF_PART_1__ ➡ Part 2 ➡ Part 3
Parts
- Part 1 — Examples 1~6:
x-www-form-urlencoded
andjson
encoding, validation - Part 2 — Examples 7~12: Removing
<form>
, on-the-fly validation, Focus detection, Show/Hide the password - Part 3 — Examples 13~21: Spinner, Floating Labels, Checkboxes, Date Picker, Autocomplete
Let me know if you have any suggestion or comment.
Thank you for reading.