Quickstart Elm 0.19, part 14

Adding ENTER keypress functionality in a todo app in Elm 0.19

By: Ajdin Imsirovic 31 October 2019

In this article, we’ll improve the addition of new todos with the ENTER keypress. As a bonus, we’ll see a little trick on how to write correct function signatures, and why that is important.

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

Note: Examples in this article use Elm 0.19.

Starting from the previous article’s example

Let’s begin from previous article’s finished example, with comments added in to have an overview of improvements that need to be added:

module Main exposing (main)

import Browser exposing (sandbox)
import Html exposing (..)
import Html.Attributes exposing (class, value)
import Html.Events exposing (onInput, onClick)


type Msg
    = UpdateText String
    | AddTodo
    | RemoveTodo Int


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


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


listToString index todo =
    div [] [ text <| Debug.toString(index) ++ " " ++ todo
           , button [ onClick (RemoveTodo index) ] [ text "X" ]
           ]


view model =
    div [ class "text-center" ]
        -- add a form here which will listen for onSubmit event instead of the onInput
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ] 
        , div [] ( List.indexedMap listToString model.todos )
        ]


update msg model =
    case msg of
        UpdateText newText ->
            { model | text = newText }

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

        RemoveTodo index ->
            let
                beforeTodos =
                    List.take index model.todos

                afterTodos =
                    List.drop (index + 1) model.todos

                newTodos =
                    beforeTodos ++ afterTodos
            in
            { model | todos = newTodos }


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

As we can see above, there’s litterally only a single improvement we need to add.

Here it is:

view : Model -> Html Msg
view model =
    div [ class "col-12 col-sm-6 offset-sm-3" ]
        [ form [ class "row", onSubmit AddTodo ]
            [ div [ class "col-9" ]
                [ input
                    [ onInput UpdateText
                    , value model.text
                    , autofocus True
                    , class "form-control"
                    , placeholder "Enter a todo"
                    ]
                    []
                ]
            , div [ class "col-3" ]
                [ button
                    [ class "btn btn-primary form-control" ]
                    [ text "+" ]
                ]
            ]
        , div [] (List.indexedMap listToString model.todos)
        ]

To make the onSubmit event on the form work, we need to expose it:

import Html.Events exposing (onInput, onClick, onSubmit)

Also, we need to expose additional attributes we’re using. Here they are:

import Html.Attributes exposing (autofocus, class, placeholder, value)

We can also make the styling in the listToString function look a bit better:

listToString index todo =
    div [ class "card-body border d-flex justify-content-between align-items-center my-3" ] 
        [ div [ class "text-uppercase" ] 
              [ text <| Debug.toString(index) ++ " " ++ todo ]
        , button [ class "btn btn-secondary"
                 , onClick (RemoveTodo index)
                 ] [ text "X" ]
        ]

That’s it!

Here’s the updated app:

The full code for the app now looks like this:

module Main exposing (main)

import Browser exposing (sandbox)
import Html exposing (..)
import Html.Attributes exposing (autofocus, class, placeholder, value)
import Html.Events exposing (onInput, onClick, onSubmit)


type Msg
    = UpdateText String
    | AddTodo
    | RemoveTodo Int


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


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


listToString index todo =
    div [ class "card-body border d-flex justify-content-between align-items-center my-3" ] 
        [ div [ class "text-uppercase" ] 
              [ text <| Debug.toString(index) ++ " " ++ todo ]
        , button [ class "btn btn-secondary"
                 , onClick (RemoveTodo index)
                 ] [ text "X" ]
        ]

{- OLD VERSION:
view model =
    div [ class "text-center" ]
        -- add a form here which will listen for onSubmit event instead of the onInput
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ] 
        , div [] ( List.indexedMap listToString model.todos )
        ]
-}      
view : Model -> Html Msg
view model =
    div [ class "col-12 col-sm-6 offset-sm-3" ]
        [ form [ class "row", onSubmit AddTodo ]
            [ div [ class "col-9" ]
                [ input
                    [ onInput UpdateText
                    , value model.text
                    , autofocus True
                    , class "form-control"
                    , placeholder "Enter a todo"
                    ]
                    []
                ]
            , div [ class "col-3" ]
                [ button
                    [ class "btn btn-primary form-control" ]
                    [ text "+" ]
                ]
            ]
        , div [] (List.indexedMap listToString model.todos)
        ]        


update msg model =
    case msg of
        UpdateText newText ->
            { model | text = newText }

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

        RemoveTodo index ->
            let
                beforeTodos =
                    List.take index model.todos

                afterTodos =
                    List.drop (index + 1) model.todos

                newTodos =
                    beforeTodos ++ afterTodos
            in
            { model | todos = newTodos }


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

Of course, we should also include the index.html file, into which the entire Elm app is mounted:

<html>
<head>
  <link 
    rel="stylesheet" 
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" 
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
    crossorigin="anonymous">
  <style>
    /* you can style your program here */
  </style>
</head>
<body class="p-5">
  <main></main>
  <script>
    var app = Elm.Main.init({ node: document.querySelector('main') })
    // you can use ports and stuff here
  </script>
</body>
</html>

Next, as a bonus, we’ll look at improving our function signatures.

Writing function signatures in Elm 0.19

Here’s a pop quiz: What’s the function signature of the following function?

listToString index todo =
    div [ class "card-body border d-flex justify-content-between align-items-center my-3" ] 
        [ div [ class "text-uppercase" ] 
              [ text <| Debug.toString(index) ++ " " ++ todo ]
        , button [ class "btn btn-secondary"
                 , onClick (RemoveTodo index)
                 ] [ text "X" ]
        ]

The answer is: Let the compiler help you out!

We’ll begin by giving our function a signature that we know is wrong:

listToString: String -> String

We know that this will produce a Bad Type Annotation error. That’s what we want to do, because this way, the compiler will guide us to the solution. Plus we’ll learn how the compiler “thinks” - which will make us write better Elm code.

Here’s the error:

Bad Type Annotation
Line 27, Column 20
The type annotation for `listToString` says it can accept 1 argument, but the
definition says it has 2 arguments:

27| listToString index todo =
                       ^^^^
Is the type annotation missing something? Should some argument be deleted? Maybe
some parentheses are missing?

Ok, then let’s give it 2 arguments:

Type Mismatch
Line 65, Column 35
The 1st argument to `indexedMap` is not what I expect:

65|         , div [] (List.indexedMap listToString model.todos)
                                      ^^^^^^^^^^^^
This `listToString` value is a:

    String -> String -> String

But `indexedMap` needs the 1st argument to be:

    Int -> a -> b

Hint: Want to convert a String into an Int? Use the String.toInt function!

Great, now we’re getting a Type Mismatch error!

So let’s just change it to Int -> a -> b as mentioned above:

Type Mismatch
Line 28, Column 5
Something is off with the body of the `listToString` definition:

28|>    div [ class "card-body border d-flex justify-content-between align-items-center my-3" ] 
29|>        [ div [ class "text-uppercase" ] 
30|>              [ text <| Debug.toString(index) ++ " " ++ todo ]
31|>        , button [ class "btn btn-secondary"
32|>                 , onClick (RemoveTodo index)
33|>                 ] [ text "X" ]
34|>        ]

This `div` call produces:

    Html Msg

But the type annotation on `listToString` says it should be:

    b

Awesome, we’re getting closer. Now let’s oblige and say that the function returns an Html Msg:

listToString: Int -> a -> Html Msg

And the error this time is:

Type Mismatch
Line 30, Column 57
The (++) operator cannot append these two values:

30|               [ text <| Debug.toString(index) ++ " " ++ todo ]
                                                     ^^^^^^^^^^^
I already figured out that the left side of (++) is:

    String

This `todo` value is a:

    a

Hint: Your type annotation uses type variable `a` which means ANY type of value
can flow through, but your code is saying it specifically wants a `String`
value. Maybe change your type annotation to be more specific? Maybe change the
code to be more general?

Finally, we can give it a String as the second argument:

listToString: Int -> String -> Html Msg

And the app now compiles.

Now let’s look at our function signature. It takes an Int and a String, and returns an Html Msg.

That means the function is not named correctly. It can’t be listToString: there’s no List here.

Thus, in the next article, we’ll call it viewTodo. It’s more descriptive of what it actually does.

That’s it for this article. In the next one, we’ll learn about nullable values in Elm 0.19 with Maybe.

Feel free to check out my work here: