In the first part of this series we’ve build a simple index page populated with data retrieved using a GET request (remember that our example is a music database).

Originally this part was going to be about adding a second page to the application, namely a page that displays an artist’s details. But we’ve been busy rolling out our new release to the first customers (yay!), so that post isn’t finished yet.

As an interlude (and because I’ve seen and had quite a few questions about it) this post is going to be about form validation.

Again, please leave a comment if anything is unclear

Table of Contents

Example

We will be using a somewhat contrived example of a form where somebody has to enter their name and age, both fields are required.

UX

Naturally the implementation depends a lot on UX choices. For our application we’ve made the following choices regarding form validation:

  • initially the form should not display any feedback, until the submit button has been clicked
  • after the submit button has been clicked, feedback must be displayed using “double visual emphasis” for errors (i.e. red label, icon, and error message)
  • the feedback should not change until the submit button gets clicked again

Implementation

We use the App module from the previous post, remove all unnecessary code and stub the form in the view function:

module App where

import Html exposing (..)
import Html.Attributes exposing (action, attribute, class, for, id, type')


-- MODEL

type Model =
  Undefined


init : Model
init =
  Undefined


-- UPDATE

type Action
  = NoOp


update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      model


-- SIGNALS

main : Signal Html
main =
  Signal.map view model


model : Signal Model
model = Signal.foldp update init actions.signal


actions : Signal.Mailbox Action
actions =
  Signal.mailbox NoOp


-- VIEW

view : Model -> Html
view model =
  div [ class "container" ]
  [ div [ attribute "role" "form" ]
    [ div [ class "form-group" ]
      [ label [ for "name" ]
        [ text "name" ]
      , input [ id "name", type' "text" , class "form-control" ]
        []
      ]
    , div [ class "form-group" ]
      [ label [ for "age" ]
        [ text "age" ]
      , input [ id "age", type' "text" , class "form-control" ]
        []
      ]
    , button [ class "btn btn-default" ]
      [ text "Submit" ]
    ]
  ]

Representing Input State

First we add a type to represent the state of input elements:

type InputState
  = Initial
  | HasError String
  | IsOkay

Second we change the definition of Model to a record with the following three fields:

  • name: the value of the name input
  • age: the value of the age input
  • inputState: a dictionary that maps the IDs of the inputs to their state
type alias Model =
  { name: String
  , age: String
  , inputState: Dict String InputState
  }


init : Model
init =
  { name = ""
  , age = ""
  , inputState = Dict.empty
  }

We need some more scaffolding, namely actions to set the values of the name and age model fields:

type Action
  = NoOp
  | SetName String
  | SetAge String


update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      model

    SetName name' ->
      { model | name <- name' }

    SetAge age' ->
      { model | age <- age' }

Adding Event Handlers

For the actions above to work we need to add event handlers to the inputs using the Html.Events package. The event handler for the name input looks as follows:

on "input" targetValue (Signal.message actions.address << SetName)

What’s going on here? The type of Html.Events.on is:

on : String -> Json.Decoder a -> (a -> Signal.Message) -> Attribute

The first parameter (type String) is the name of the event you want the handle.

The second parameter (type Json.Decoder a) is a JSON decoder for extracting a value of type a from the event. Here we use Html.Events.targetValue which is defined as:

targetValue : Json.Decoder String
targetValue =
  Json.Decode.at ["target", "value"] Json.Decode.string

The third parameter (type a -> Sigal.Message) is a function that gets called if the decoder is successful and should turn the value of type a into a Signal.Message. We use “backward” function composition (<<) to wrap the value in a SetName action and then send it the actions address. Without function composition (and without pointfree style) that part would look as follows:

\value -> Signal.message actions.address (SetName value)

After we add a similar event handler to the age input, the view function looks as follows:

view : Model -> Html
view model =
  div [ class "container" ]
  [ div [ attribute "role" "form" ]
    [ div [ class "form-group" ]
      [ label [ for "name" ]
        [ text "name" ]
      , input [ id "name", type' "text" , class "form-control"
              , on "input" targetValue
                     (Signal.message actions.address << SetName)
              ]
        []
      ]
    , div [ class "form-group" ]
      [ label [ for "age" ]
        [ text "age" ]
      , input [ id "age", type' "text" , class "form-control"
              , on "input" targetValue
                     (Signal.message actions.address << SetAge)
              ]
             []
      ]
    , button [ class "btn btn-default" ]
      [ text "Submit" ]
    ]
  ]

Submitting the Form

Note that the submit button does not have an onClick handler yet, i.e. we are still not submitting the form or showing validation errors. Time to fix that.

The trick here is to decide beforehand what to do. If the model is valid we submit the form, else we set (and subsequently show) the validation errors.

We add the following helper functions:

isValidName : String -> Bool
isValidName =
  not << String.isEmpty

isValidAge : String -> Bool
isValidAge value =
  case String.toInt value of
    Ok int ->
      int >= 0
    Err _ ->
      False

isValid : Model -> Bool
isValid model =
  isValidName model.name && isValidAge model.age

We also add the SetInputState action, stub the Submit action, and add corresponding case clauses to the update function:

type Action
  = NoOp
  | SetName String
  | SetAge String
  | SetInputState
  | Submit


update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      model

    SetName name' ->
      { model | name <- name' }

    SetAge age' ->
      { model | age <- age' }

    SetInputState ->
      let name = if isValidName model.name
                 then IsOkay
                 else HasError "Please enter your name"
          age = if isValidAge model.age
                then IsOkay
                else HasError "Please enter your age as a whole number"
          inputState' = Dict.fromList [("name", name), ("age", age)]
      in
        { model | inputState <- inputState' }

    Submit ->
      model

We’re now ready to wire up the submit button using Html.Events.onClick:

button [ class "btn btn-default"
       , onClick actions.address
           <| if isValid model then Submit else SetInputState
       ]
[ text "Submit" ]

So, if the model is valid we send a Submit message to the actions mailbox, else we send a SetInputState message.

Showing Validation Errors

We’re now ready to show the validation errors using Bootstrap’s validation states. We start with refactoring the view function, where we give each input its own function:

view : Model -> Html
view model =
  div [ class "container" ]
  [ div [ attribute "role" "form" ]
    [ nameInput model
    , ageInput model
    , button [ class "btn btn-default"
             , onClick actions.address <| if isValid model then Submit else SetInputState
             ]
      [ text "Submit" ]
    ]
  ]


nameInput : Model -> Html
nameInput model =
  div [ class "form-group" ]
  [ label [ for "name" ]
    [ text "name" ]
  , input [ id "name", type' "text" , class "form-control"
          , on "input" targetValue (Signal.message actions.address << SetName)
          ]
    []
  ]


ageInput : Model -> Html
ageInput model =
  div [ class "form-group" ]
  [ label [ for "age" ]
    [ text "age" ]
  , input [ id "age", type' "text" , class "form-control"
          , on "input" targetValue (Signal.message actions.address << SetAge)
          ]
    []
  ]

Hmm, too much duplication. Let’s introduce a generic input function. Its parameters:

  • id: the ID of the input, type String
  • label: the text of the label, type String
  • type': the type of input (text, email, etc.), type String (note that type is a keyword)
  • action: a function that takes the value of the input and turns it into a action, type String -> Action (e.g. SetName)

If a function has more than two parameters without an obvious order I like to use a dedicated record type:

type alias InputParams =
  { id: String
  , label: String
  , type': String
  , action: String -> Action
  }

And the generic input function input' which uses that record type:

input' : InputParams -> Model -> Html
input' params model =
  div [ class "form-group" ]
  [ label [ for params.id ]
    [ text params.label ]
  , input [ id params.id, type' params.type' , class "form-control"
          , on "input" targetValue
                 (Signal.message actions.address << params.action)
          ]
    []
  ]

Now we can replace nameInput and ageInput with the following:

nameInput : Model -> Html
nameInput =
  input' { id = "name"
         , label = "name"
         , type' = "text"
         , action = SetName
         }


ageInput : Model -> Html
ageInput =
  input' { id = "age"
         , label = "age"
         , type' = "text"
         , action = SetAge
         }

And use the inputState dictionary to (finally) show the user feedback:

input' : InputParams -> Model -> Html
input' params model =
  let state = case Dict.get params.id model.inputState of
                Nothing -> Initial
                Just value -> value
  in
    div [ class <| case state of
                     Initial ->
                       "form-group"
                     HasError _ ->
                       "form-group has-feedback has-error"
                     IsOkay ->
                       "form-group has-feedback has-success"
        ]
    [ label [ for params.id ] [ text params.label ]
    , input [ id params.id, type' params.type' , class "form-control"
            , on "input" targetValue
                   (Signal.message actions.address << params.action)
            ]
      []
    , span [ class <| case state of
                        Initial ->
                          "glyphicon glyphicon-blank form-control-feedback"
                        HasError _ ->
                          "glyphicon glyphicon-remove form-control-feedback"
                        IsOkay ->
                          "glyphicon glyphicon-ok form-control-feedback"
           ]
      []
    , span [ class "help-block" ]
      [ case state of
          HasError error -> text error
          _ -> text ""
      ]
    ]

If you’re familiar with Bootstap this is pretty straightforward. In the let expression we use the input’s ID to lookup its state with Dict.get. We then use that state to select the right CSS classes and to show the error text (if necessary).

Submit Feedback

To wrap things up let’s add feedback on submit1. First we add a submit field to the model and initialize it to False:

type alias Model =
  { name: String
  , age: String
  , inputState: Dict String InputState
  , submit: Bool
  }


init : Model
init =
  { name = ""
  , age = ""
  , inputState = Dict.empty
  , submit = False
  }

When update gets called with a Submit action we set the field to True:

update : Action -> Model -> Model
update action model =
  case action of
    NoOp ->
      ...

    SetName name' ->
      ...

    SetAge age' ->
      ...

    SetInputState ->
      ...

    Submit ->
      { model | submit <- True }

And in the view function we use it to decide if we show the submit feedback or the form:

view : Model -> Html
view model =
  div [ class "container" ]
  <| if model.submit
     then
       [ div [ class "alert alert-success"
             , attribute "role" "alert"
             ]
         [ text "The form has been submitted successfully"]
       ]
     else
       [ div [ attribute "role" "form" ]
         [ nameInput model
         , ageInput model
         , button [ class "btn btn-default"
                  , onClick actions.address
                    <| if isValid model then Submit else SetInputState
                  ]
           [ text "Submit" ]
         ]
       ]

Code

The full code is on GitHub:


  1. Note that actual form submission (i.e. POSTing the data to the back-end) will be the subject of part 4 of this series.