Quickstart Elm 0.19, part 12

Adding a dynamic input with a button in Elm 0.19

By: Ajdin Imsirovic 30 October 2019

In this article, we’ll see how to work with input fields in Elm 0.19.

A close-up of a race track with an Elm logo overlaid

Note: Examples in this article use Elm 0.19.

Starting from the dynamic input example

Let’s start from the dynamic input example from the previous post; we’ll add comments to sections that need improvement, as follows:

module Main exposing (main)

import Browser exposing (sandbox)
import Html exposing (div, input, text)
import Html.Attributes exposing (class, value)
import Html.Events exposing (onInput)

initialModel =
    { text = "" }
    -- (1) we'll add another property on the Record; 
        -- this new property will store typed-in entries


view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        -- (2) we'll be updating the below div 
            --with the value of new property we added in (1)
        , div [] [ text model.text ] 
        ]

type Msg
    = UpdateText String



update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText } 
        -- we'll add another pattern to match here; 
            -- this one updates the model with the new property being added


main =
    sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Let’s see this starting app in Ellie:

Adding a call to the button function inside the view function

Let’s first add the button to our view:

view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ]
        -- (2) we'll be updating the below div 
            --with the value of new property we added in (1)
        , div [] [ text model.text ] 
        ]

This will throw three errors:

I cannot find a `button` variable:
...
I cannot find a `onClick` variable:
...
I cannot find a `AddTodo` constructor:

We’ll fix this by:

  • importing the button function from the Html module
  • importing the onClick event from the Html.Events module
  • Adding the AddTodo constructor to the Msg type

Once we fix these errors, we’ll be facing the next set of errors that we’ll need to fix; often, this process is repeated several times before we reach a point where our app simply compiles. This is what I call compiler-driven development.

Compiler-driven development in Elm 0.19

Let’s fix the three errors, then see what else the compiler will have for us to do.

First, we’ll expose the button function in the Html import:

import Html exposing (div, input, text, button)

Next, we’ll expose the onClick event in the Html.Events import:

import Html.Events exposing (onInput, onClick)

Finally, we’ll add the AddTodo type constructor:

type Msg
    = UpdateText String
    | AddTodo

Great! We’ve solved our errors, now we’ll hit the compile button in Ellie, and get the Missing Patterns error: A Missing Patterns error showing up in Ellie app

Adding a dynamic input with a clickable button in Elm 0.19

Let’s add that missing pattern to the update function now:

update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText } 
        AddTodo ->
            { model }

The above code will throw another error; this time, it’s a Parse Error: A Parse Error error showing up in Ellie app

Let’s listen to the suggestion and add the pipe, following it up with the record fields we want to update:

update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText } 
        AddTodo ->
            { model | text = "whatever" }

Now that we’ve pattern matched the AddTodo case, our app compiles and works like this: Pattern-matching a button click message in Elm 0.19

You can try it yourself in the updated app in Ellie:

Next, rather than just overriding the input with a hardcoded String “whatever”, we’ll need to store the input values a user types into the input field.

We’ll store these values whenever a user clicks the “Add Todo” button.

Storing values in a data structure

To store values, we need some kind of a data structure. We’ll be using a List, inside our model’s Record:

type alias Model =
    { text: String 
    , todos: List String
    }

initialModel =
    { text = "" 
    , todos = []
    }

We had to add the type alias Model so that we can define our data structure.

Then in the initialModel we give the initial values to both the text property in the Record and the todos property in the Record. Both values are initially empty: text is an empty String, and todos is an empty List.

Next, we’ll update our todos List with the hardcoded word “whatever”:

update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText }
        -- the AddTodo value branch should update the todos List
        AddTodo ->
            { model | todos = "whatever" }

This doesn’t really do much for us. Actually, it even throws an error:

Why this error?

Basically, the update function’s type annotation looks like this:

, update :
        Msg
        -> { text : String, todos : String }
        -> { text : String, todos : String }

However, the sandbox needs the update argument to be:

    , update :
          Msg
          -> { text : String, todos : List a }
          -> { text : String, todos : List a }

Let’s fix this error:

update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText }
        -- the AddTodo value branch should update the todos List
        AddTodo ->
            { model | todos = [ "whatever" ] }

Now our update function is in sync with what sandbox expects.

Here’s the fully updated app:

Let’s look at the code of our app again:

module Main exposing (main)

import Browser exposing (sandbox)
import Html exposing (div, input, text, button)
import Html.Attributes exposing (class, value)
import Html.Events exposing (onInput, onClick)

type alias Model =
    { text: String 
    , todos: List String
    }

initialModel =
    { text = "" 
    , todos = []
    }


view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ]     
        -- (2) we'll be updating the below div 
            --with the value of new property we added in (1)
        , div [] [ text model.text ] 
        ]
           

type Msg
    = UpdateText String
    | AddTodo




update msg model =
    case msg of
        -- the input value branch remains unchanged:
        UpdateText newText ->
            { model | text = newText } 
        AddTodo ->
            { model | todos = [ "whatever" ] }


main =
    sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Right now, clicking the Add Todo button doesn’t do anything. Each time we click the Add Todo button, the todos variable is set to a List of Strings, which holds a single, hardcoded String: “whatever”.

Next, we’ll see how to update the todos List, so that we can actually show it.

Showing the List of todos

Let’s try to show the List of todos.

A naive approach

Let’s try to update the view function like this:

view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ]     
        , div [] [ List.map listToString model.todos ] 
        ]

What we’re doing above is, we’re running the List.map function and giving it two parameters:

  • the mapping function (the “transformer” function), listToString
  • the model.todos List that we’ll be mapping over; this model.todos List is the source data structure for our List.map function to work on

Next, we’ll need to define the mapping function, listToString:

listToString todo =
    div [] [ text todo ]

The listToString function takes a todo and returns a div with the todo value as this div’s text node.

Let’s save our changes:

Why doesn’t this code work:

listToString todo =
    div [] [ text todo ]

view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ]     
        , div [] [ List.map listToString model.todos ] 
        ]

The error we get is this:

Type Mismatch
Line 27, Column 18
The 2nd argument to `div` is not what I expect:

27|         , div [] [ List.map listToString model.todos ] 
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This argument is a list of type:

    List (List (Html.Html msg))

But `div` needs the 2nd argument to be:

    List (Html.Html msg)

As we can see, we are nesting a List inside a List, while the compiler expects the div’s second argument to be just a regular List, not a nested one.

Here’s the fix:

, div [] ( List.map listToString model.todos )

Note: We’re using ( and ) to group code in Elm 0.19.

Here’s the updated app:

Now our app works again, but we’re not storing the todo we mapped over with List.map.

The reason is the update function’s AddTodo branch: it currently has a hard-coded String in a List:

AddTodo ->
    { model | todos = [ "whatever" ] }

Let’s update it like this:

AddTodo ->
    { model | todos = model.todos }

This update doesn’t break the app; it still compiles.

However, it doesn’t print anything to the screen now:

Why is that?

It’s becuase we’re simply assigning the starting value of model.todos to our newly updated todos variable in this line of code:

AddTodo ->
    { model | todos = model.todos }

In the code above, we’re effectively saying:

  1. Take the empty List that’s stored in model.todos: model.todos, and
  2. assign that value to the model’s todos property: model | todos =

Basically, we’re just re-assigning the existing value to itself.

Instead, let’s do this:

AddTodo ->
    { model | todos = model.todos ++ [ "whatever" ] }

What we’re doing now is this: whenever a user clicks a button, we add a String in a List to model.todos.

Here’s this most recent update:

Now, for the Add Todo button to work, the input doesn’t even have to be typed into. We can just click the button, and every time we do, we’ll get another word “whatever” printed to the bottom.

Now all that’s left to do is add the value of the input in place of the hardcoded “whatever” String.

Update model.todos with the typed-in String

This update is minimal:

AddTodo ->
    { model | todos = model.todos ++ [ model.text ] }

Here’s the updated app:

The app now works, but we need to clear the input on each button click.

Making the input reset whenever the AddTodo message is sent

This is a tiny update too:

update msg model =
    case msg of
        UpdateText newText ->
            { model | text = newText }
        -- We append the model.text value to the end of our list of todo strings.
        AddTodo ->
            { model | text = "", todos = model.todos ++ [ model.text ] }

Here’s the completed app:

That’s it for this article.

In the next one, we’ll see how to remove todos.

Feel free to check out my work here: