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.
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:
- Add a new type to
Msg
to Handle Deleting a Todo Item - Update the mapping function: Add an “X” on each todo
- 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" ]
]
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
.
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" ]
]
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.