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.
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 theHtml
module - importing the
onClick
event from theHtml.Events
module - Adding the
AddTodo
constructor to theMsg
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:
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
:
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:
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; thismodel.todos
List is the source data structure for ourList.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.
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.
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:
- Take the empty List thatâs stored in model.todos:
model.todos
, and - 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 ] }
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 ] }
Thatâs it for this article.
In the next one, weâll see how to remove todos.