Quickstart Elm 0.19, part 13

Removing todos in todo app in Elm 0.19

By: Ajdin Imsirovic 30 October 2019

In this article, we’ll see how to remove todos in Elm 0.19. Among other things, in this article, we’ll introduce the let expression.

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:

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 Msg
    = UpdateText String
    | AddTodo
    -- Add a Message to Handle Deleting a Todo Item

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

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

listToString todo =
    div [] [ text todo ] -- update the mapping function: Add an "X" on each 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 )
        ]


update msg model =
    case msg of
        UpdateText newText ->
            { model | text = newText } 
        AddTodo ->
            { model | text = "", todos = model.todos ++ [ model.text ] }
        -- handle the DeleteTodo message


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

The above code contains all the code from the previous article, plus comments added in places where we need to update our project.

Requirements

The requirements for this update are:

  1. Add a new type to Msg to Handle Deleting a Todo Item
  2. Update the mapping function: Add an “X” on each todo
  3. Handle the DeleteTodo message in the update function

Let’s start!

We’ll solve the first requirement easily, by simply adding a new Msg type constructor, which takes an int.

type Msg
    = UpdateText String
    | AddTodo
    | RemoveTodo Int

Let’s update our app with the above code.

Of course, this update breaks our app:

The compiler reports the following issue:

This `case` does not have branches for all possibilities:

41|>    case msg of
42|>        -- the input value branch remains unchanged:
43|>        UpdateText newText ->
44|>            { model | text = newText } 
45|>        AddTodo ->
46|>            { model | text = "", todos = model.todos ++ [ model.text ] }

Missing possibilities include:

    RemoveTodo _

So, let’s quickly fix this by adding the RemoveTodo branch to update function.

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

The app now compiles, but there’s no way to remove a todo:

To fix this, we’ll need to update the mapping function, next.

Updating the mapping function with List.indexedMap

We already worked with List.indexedMap in this article series.

We’ll change the function in the view from List.map to List.indexedMap:

view model =
    div [ class "text-center" ]
        [ input [ onInput UpdateText, value model.text ] []
        , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ]
        , div [] ( List.indexedMap listToString model.todos ) -- this line updated!
        ]

This improvement now throws an error:

If we read the official documentation on List.indexedMap, it says:

Same as map but the function is also applied to the index of each element (starting at zero).

What this means in practice is: we need to pass another argument variable to our listToString mapping function.

listToString index todo =
    div [] [ text <| Debug.toString(index) ++ " " ++ todo ]

Obviously, we need to also convert the index argument variable, which is an Int, into a String value, using Debug.toString(index).

Now our app compiles and adds numbers to the beginning of each added todo.

Here’s the embedded app:

Next, we’ll add a little X button next to each todo item.

Here’s the updated listToString function:

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

Here’s the updated app:

Now we need to make the span clickable; we’ll need to add an onClick event, and send a message, just like the one we have on the Add Todo button.

Here’s the updated code:

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

Now, whenever a user clicks on an “X”, we’ll erase the one it was clicked on. How do we know that? By passing the index of the “X” that was clicked on.

Now we can improve the RemoveTodo pattern in the update function.

Here’s the improved update function.

update : Msg -> Model -> Model
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 }

Here’s a concept that we haven’t seen before: a let expression.

Let’s inspect the definition of a let expression from the Elm lang site:

Let Expressions: let these values be defined in this specific expression.

Let’s look at the RemoveTodo pattern again:

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

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

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

The docs say that List.take will take the first n members of a list.

Thus, in beforeTodos, we use the value stored in index (that is, the value stored in the index argument variable we passed with the RemoveTodo message). We’re using this index value as the first argument of List.take. The second argument is model.todos.

The beforeTodos will take all the model.todos whose index is lower than the one that the user clicked on when they sent the RemoveTodo message.

In afterTodos we’re using List.drop.

We’re dropping the first n members of the list, with n being index + 1.

So effectively, what’s happening is this: in beforeTodos, we’re storing n members of the List of todos, while in afterTodos, we’re erasing n + 1 members.

Finally, in newTodos, we’re concatenating the two Lists together: beforeTodos ++ afterTodos.

All the three temporary variables are saved in the let part of the let in expression.

Finally, in the in part of the let in expression, we update our model’s todos with newTodos.

Here’s the newly updated app:

Now all that’s left to make this work is send the message in the span:

onClick (RemoveTodo index)

Actually, let’s convert it from span to a button, i.e, from this:

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

… to this:

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

Here’s the updated app:

That’s it for this article. In the next one, we’ll make it possible to add a todo without having to click the Add Todo button; that is, by pressing the ENTER key after typing a todo into the input field.

Feel free to check out my work here: