Experimental reuse of code in Elm, Part II — The counter

This is the second part of a series:

Part I

Part III

Exercise: Add several instances of the counter http://elm-lang.org/examples/buttons in one page.

First I converted the “buttons” example into a module adding this line at the top: module Buttons exposing (view, Msg, update)and I saved it asbuttons.elm

The code is:

module Buttons exposing (view, Msg, update)
import Html exposing (beginnerProgram, div, button, text)
import Html.Events exposing (onClick)
main =
beginnerProgram { model = 0, view = view, update = update }
view model =
div []
[ button [ onClick Decrement ] [ text “-” ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text “+” ]
]
type Msg
= Increment
| Decrement
update msg model =
case msg of
Increment ->
model + 1
        Decrement ->
model — 1

I then created main.elm that imports buttons.elm.

In main.elm i create a model that is a list of “buttons”. The model of buttons.elm is just an integer so the model of main.elm is a list of Integer:

main =
beginnerProgram
{ model = [ 0, 10, 20, 30, 4000, -1 ]
, view = view
, update = update
}

For each element in the model I want to create a new instance of the counter. To do so I could useList.map that apply a function to every element of a list.

List.map (\element -> Buttons.view element) model

The problem is that Buttons.view return Buttons.Msg but the view in beginnerProgram expect to receive Msg, not Buttons.Msg.

A solution is to transform the messages produced by Buttons.view usingHtml.map:

List.map
(\element -> Html.map Tag (Buttons.view element))
model

Another issue is that when we have multiple counters, main.elm need to understand which element got clicked. For this I added an index that contain the position inside the List. So I replaced List.map with List.indexedMap that is the same as List.map but the function is also applied to the index of each element

List.indexedMap
(\index element -> Html.map (Tag index) (Buttons.view element))
model

I use the index as payload for the Tag message

After adding some style, this is the final view of main.elm:

view model =
div []
(List.indexedMap
(\index element ->
Html.map
(Tag index)
(div
[ style
[ ( "border", "1px solid #aaa" )
, ( "background-color", "#eee" )
, ( "display", "inline-block" )
, ( "padding", "10px" )
, ( "margin", "10px" )
, ( "text-align", "center" )
]
]
[ Buttons.view element ]
)
)
model
)

The message type is now defined as:

type Msg
= Tag Int Buttons.Msg

Before working on the update function, I prepare couple of helpers. I need to access specific parts of the List in the model based on the position (index). The first helper is a getter to retrieve the nth element of the list

getButtons model index =
let
buttons =
Array.get index (Array.fromList model)
in
case buttons of
Nothing ->
0
            Just val ->
val

First it converts the list in an Array (Array.fromList) then it extract the element in position index using Array.get. At the end it converts the Maybe buttons to buttons (because Array.get returns a Maybe element) and return it

The second helper it a setter:

setButtons model index buttons =
let
buttonsArray =
Array.fromList model
        newButtonsArray =
Array.set index buttons buttonsArray
in
Array.toList newButtonsArray

This time we useArray.set to replace an element in the list and then we return the List of buttons, that is also the model.

Now we have all what we need to build the update function:

update msg model =
case msg of
Tag index buttons_Msg ->
let
oldButtons =
getButtons model index
                newButtons =
Buttons.update buttons_Msg oldButtons
in
setButtons model index newButtons

The message Tag has, as payload, the index of the element that received the click and the Buttons.Msg. So first, using the index, we extract the value of the counter from the model using getButtons. Then we transform this value calling Buttons.update and giving it the appropriate message that is contained in buttons_Msg. This message is either Increment or Decrement as specified in buttons.elm. With the newButtons returned by Buttons.updateI update the right element in the model, using setButtons and the same index used before.

This is all. To get and run this code, executed these commands:

$ git clone https://github.com/lucamug/elm-multiple-buttons.git
$ cd elm-multiple-buttons/
$ elm-reactor

Then access these two addresses:

http://localhost:8000/source/buttons.elm
http://localhost:8000/source/main.elm

the buttons.elm module is now able to run stand-alone or from inside main.elm.

Thank you to Chad Gilbert for quickly helping me to fix this problem.

Could this exercise being solved in a more elegant way? Let me know in the comments below.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.