Forms in Elm —Validation, Tutorial and Examples — Part 1

Lucamug
10 min readDec 27, 2017

--

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:

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 and json 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

CodeDemo

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

CodeDemo

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 and method 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

CodeDemo

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

CodeDemo

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

CodeDemo

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

CodeDemo

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

Older resources

Examples

Styling resources

__END_OF_PART_1__ ➡ Part 2Part 3

Parts

  • Part 1 — Examples 1~6: x-www-form-urlencoded and json 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.

--

--