This document assumes you have some basic knowledge of Elm. I recommend reading "Welcome to Elm", the first chapter from "Elm in Action" by Richard Feldman.
Create a folder for the project and create package.json
with the following contents.
package.json
{
"private": true,
"dependencies": {
"elm": "^0.19.1-3",
"elm-live": "^4.0.1"
},
"scripts": {
"dev-server": "elm-live src/Main.elm -- --debug"
}
}
elm-live
is just a handy web server which displays Elm compiler errors in the browser. We're
adding a simple dev-server
script so that we can run elm-live
in debug mode.
Then from within that folder run yarn
(or npm
if you don't use Yarn) and yarn run elm init
(or
npm run elm init
). Create a src
folder and put create Main.elm
there.
src/Main.elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
Int
type Msg
= Increase
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
0
update : Msg -> Model -> Model
update msg model =
case msg of
Increase ->
model + 1
view : Model -> Html Msg
view count =
div []
[ button [ type_ "button" ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick Increase ] [ text "+" ]
]
Now run yarn run dev-server
from the project folder. Go under the address that elm-live wants you
to go and you should see the counter.
- Make the minus button work. Start by adding either an
onClick
handler to the button or extending theMsg
type with a new value.- If you get lost, check out Buttons chapter from the Elm guide.
- Add a reset button for setting the counter back to 0.
src/Main.elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
Int
type Msg
= Increase
| Decrease
| Reset
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
0
update : Msg -> Model -> Model
update msg model =
case msg of
Increase ->
model + 1
Decrease ->
model - 1
Reset ->
init
view : Model -> Html Msg
view count =
div []
[ button [ type_ "button", onClick Decrease ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick Increase ] [ text "+" ]
, button [ type_ "button", onClick Reset ] [ text "Reset" ]
]
- Add buttons for incrementing & decrementing the counter by 5. Rather than adding new messages,
extend the existing ones so that they can accept an integer, that is in type
Msg
changeIncrease
toIncrease Int
and so on.- If you get lost, check out Custom Types.
src/Main.elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
Int
type Msg
= Increase Int
| Decrease Int
| Reset
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
0
update : Msg -> Model -> Model
update msg model =
case msg of
Increase n ->
model + n
Decrease n ->
model - n
Reset ->
init
view : Model -> Html Msg
view count =
div []
[ button [ type_ "button", onClick (Decrease 5) ] [ text "--" ]
, button [ type_ "button", onClick (Decrease 1) ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick (Increase 1) ] [ text "+" ]
, button [ type_ "button", onClick (Increase 5) ] [ text "++" ]
, button [ type_ "button", onClick Reset ] [ text "Reset" ]
]
- Make it so that at the start of the app, the only thing on the page is an "initialize" button.
Clicking it should show the counter with the buttons.
- As with most of things in Elm, it's best to start with changing types so that they represent what you have in your head.
- In this case, we can represent the absence of the counter with the
Maybe Int
type – try changing theModel
type to this form and then follow the compiler messages. - Before you start, it's best to read the chapter on Maybe from the official guide.
src/Main.elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
Maybe Int
type Msg
= Increase Int
| Decrease Int
| Reset
| Initialize
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
Nothing
update : Msg -> Model -> Model
update msg model =
case msg of
Increase n ->
Maybe.map ((+) n) model
Decrease n ->
Maybe.map (\count -> count - n) model
Reset ->
Just 0
Initialize ->
Just 0
view : Model -> Html Msg
view model =
case model of
Nothing ->
div []
[ button [ type_ "button", onClick Initialize ] [ text "initialize" ] ]
Just count ->
div []
[ button [ type_ "button", onClick (Decrease 5) ] [ text "--" ]
, button [ type_ "button", onClick (Decrease 1) ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick (Increase 1) ] [ text "+" ]
, button [ type_ "button", onClick (Increase 5) ] [ text "++" ]
, button [ type_ "button", onClick Reset ] [ text "reset" ]
]
- Add a number input field next to the initialize button. When the button gets clicked, the initial
value of the counter should be equal to the value that was in the input.
- To hold more than just the state of the counter in our model, we need a record.
- We'll persist in the model the value of the input each time the input changes.
src/Main.elm
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
{ initialValue : String, counter : Maybe Int }
type Msg
= Increase Int
| Decrease Int
| Reset
| Initialize
| InitialValueChanged String
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
{ initialValue = "", counter = Nothing }
update : Msg -> Model -> Model
update msg model =
case msg of
Increase n ->
{ model | counter = Maybe.map ((+) n) model.counter }
Decrease n ->
{ model | counter = Maybe.map (\count -> count - n) model.counter }
Reset ->
init
Initialize ->
let
parsedInitialValue =
String.toInt model.initialValue
in
-- TODO: What happened here? Can we make the code more clear?
{ model | counter = Just (Maybe.withDefault 0 parsedInitialValue) }
InitialValueChanged value ->
{ model | initialValue = value }
view : Model -> Html Msg
view model =
case model.counter of
Nothing ->
div []
[ input [ type_ "number", onInput InitialValueChanged ] []
, button [ type_ "button", onClick Initialize ] [ text "initialize" ]
]
Just count ->
div []
[ button [ type_ "button", onClick (Decrease 5) ] [ text "--" ]
, button [ type_ "button", onClick (Decrease 1) ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick (Increase 1) ] [ text "+" ]
, button [ type_ "button", onClick (Increase 5) ] [ text "++" ]
, button [ type_ "button", onClick Reset ] [ text "reset" ]
]
-
Rewrite the branch for the
Initialize
message from theupdate
function so that it's more clear what's going on.- A more explicit approach:
Code snippet
Initialize -> let newCounter = case String.toInt model.initialValue of Just x -> Just x Nothing -> Just 0 in { model | counter = newCounter }
-
Add dynamic number of counters. The "initialize" button should say "add" now and add new counters to the bottom of the list. Each counter should have its own +/- buttons as well as "remove" button which removes this particular counter. When adding a new counter, the value from the input field should be still taken into account.
src/Main.elm
module Main exposing (main)
import Array exposing (Array)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
{ initialValue : String, counters : Array Int }
type Msg
= Increase Int Int
| Decrease Int Int
| Reset
| Initialize
| InitialValueChanged String
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
{ initialValue = "", counters = Array.empty }
update : Msg -> Model -> Model
update msg model =
case msg of
Increase index n ->
let
updatedCounters =
case Array.get index model.counters of
Just currentValue ->
Array.set index (currentValue + 1) model.counters
Nothing ->
model.counters
in
{ model | counters = updatedCounters }
Decrease n ->
{ model | counter = Maybe.map (\count -> count - n) model.counter }
Reset ->
init
Initialize ->
let
newCounter =
case String.toInt model.initialValue of
Just x ->
Just x
Nothing ->
Just 0
in
{ model | counter = newCounter }
InitialValueChanged value ->
{ model | initialValue = value }
view : Model -> Html Msg
view model =
case model.counter of
Nothing ->
div []
[ input [ type_ "number", onInput InitialValueChanged ] []
, button [ type_ "button", onClick Initialize ] [ text "initialize" ]
]
Just count ->
div []
[ button [ type_ "button", onClick (Decrease 5) ] [ text "--" ]
, button [ type_ "button", onClick (Decrease 1) ] [ text "-" ]
, text (String.fromInt count)
, button [ type_ "button", onClick (Increase 1) ] [ text "+" ]
, button [ type_ "button", onClick (Increase 5) ] [ text "++" ]
, button [ type_ "button", onClick Reset ] [ text "reset" ]
]
- Finish the work from the previous session.
src/Main.elm
module Main exposing (main)
import Array exposing (Array)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
{ initialValue : String, counters : Array Int }
type Msg
= Increase Int Int
| Decrease Int Int
| Reset
| AddNewCounter
| InitialValueChanged String
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
init : Model
init =
{ initialValue = "", counters = Array.empty }
update : Msg -> Model -> Model
update msg model =
case msg of
Increase index n ->
let
updatedCounters =
case Array.get index model.counters of
Just currentValue ->
Array.set index (currentValue + n) model.counters
Nothing ->
model.counters
in
{ model | counters = updatedCounters }
Decrease index n ->
let
updatedCounters =
case Array.get index model.counters of
Just currentValue ->
Array.set index (currentValue - n) model.counters
Nothing ->
model.counters
in
{ model | counters = updatedCounters }
Reset ->
init
AddNewCounter ->
let
newCounter =
Maybe.withDefault 0 <| String.toInt model.initialValue
in
{ model | counters = Array.push newCounter model.counters }
InitialValueChanged value ->
{ model | initialValue = value }
viewCounter : Int -> Int -> Html Msg
viewCounter index counter =
li []
[ button [ type_ "button", onClick (Decrease index 5) ] [ text "--" ]
, button [ type_ "button", onClick (Decrease index 1) ] [ text "-" ]
, text (String.fromInt counter)
, button [ type_ "button", onClick (Increase index 1) ] [ text "+" ]
, button [ type_ "button", onClick (Increase index 5) ] [ text "++" ]
, button [ type_ "button", onClick Reset ] [ text "reset" ]
]
view : Model -> Html Msg
view model =
div []
[ input [ type_ "number", onInput InitialValueChanged ] []
, button [ type_ "button", onClick AddNewCounter ] [ text "add counter" ]
, ul [] <| Array.toList <| Array.indexedMap viewCounter model.counters
]