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
onClickhandler to the button or extending theMsgtype 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
MsgchangeIncreasetoIncrease Intand 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 Inttype – try changing theModeltype 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
Initializemessage from theupdatefunction 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
]