Skip to content

Instantly share code, notes, and snippets.

@ravicious
Last active February 4, 2020 09:41
Show Gist options
  • Save ravicious/318a66a94e461524a39552c5d40b724a to your computer and use it in GitHub Desktop.
Save ravicious/318a66a94e461524a39552c5d40b724a to your computer and use it in GitHub Desktop.
Elm workshop

Before you start

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.

Session 1

Setup

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.

Goals

  1. Make the minus button work. Start by adding either an onClick handler to the button or extending the Msg type with a new value.
    • If you get lost, check out Buttons chapter from the Elm guide.
  2. Add a reset button for setting the counter back to 0.

End result

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" ]
        ]

Session 2

Goals

  1. 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 change Increase to Increase Int and so on.

End result

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" ]
        ]

Session 3

Goals

  1. 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 the Model 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.

End result

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" ]
                ]

Session 4

Goals

  1. 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.

End result

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" ]
                ]

Session 5

Goals

  1. Rewrite the branch for the Initialize message from the update 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 }
  2. 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.

Additional materials

End result (work in progress)

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" ]
                ]

Session 6

Goals

  1. Finish the work from the previous session.

End result (work in progress)

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
        ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment