Files
elm/lessons/04-tea.md
Mark Gerrard 7d7986f3ab Initial commit: Elm tutorial for JavaScript developers
This tutorial includes:
- 7 progressive lessons covering Elm fundamentals
- Exercises with starter code
- Solutions for exercises
- Final project: Task Manager app with localStorage persistence

Topics covered:
- Basic syntax and types
- Functions and functional programming concepts
- The Elm Architecture (Model-View-Update)
- Lists and Maybe types
- HTTP requests and JSON decoding
- Ports for JavaScript interop
2026-03-11 11:07:15 +00:00

16 KiB
Raw Blame History

Lesson 4: The Elm Architecture (TEA)

Learning Goals

By the end of this lesson, you will:

  • Understand the Model-View-Update pattern
  • Handle user input events
  • Build interactive applications
  • Understand how Elm inspired Redux

What is The Elm Architecture?

The Elm Architecture (TEA) is a pattern for building web applications. It has three parts:

  1. Model - Your application's state (the data)
  2. View - A function that turns the model into HTML
  3. Update - A function that updates the model based on messages
     ┌──────────────────────────────────────────┐
     │                                          │
     │   User clicks button                     │
     │          │                               │
     │          ▼                               │
     │   ┌──────────────┐                       │
     │   │   Message    │                       │
     │   └──────────────┘                       │
     │          │                               │
     │          ▼                               │
     │   ┌──────────────┐    ┌──────────────┐   │
     │   │   Update     │───▶│    Model     │   │
     │   └──────────────┘    └──────────────┘   │
     │                              │           │
     │                              ▼           │
     │                       ┌──────────────┐   │
     │                       │    View      │   │
     │                       └──────────────┘   │
     │                              │           │
     │                              ▼           │
     │                       ┌──────────────┐   │
     │                       │    HTML      │───┘
     │                       └──────────────┘
     │
     └──────────────────────────────────────────┘

Redux Connection

If you've used Redux, you already know TEA!

Elm Redux
Model State/Store
Msg Action
Update Reducer
View React Component

Your First Interactive App: Counter

Let's build a counter that you can increment and decrement.

Create a new file src/Counter.elm:

module Counter exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


-- MODEL

type alias Model =
    { count : Int
    }


init : Model
init =
    { count = 0
    }


-- UPDATE

type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }


-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        ]


-- MAIN

main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Run it with:

elm reactor

Then open http://localhost:8000/src/Counter.elm

Breaking It Down

1. The Model

type alias Model =
    { count : Int
    }

init : Model
init =
    { count = 0
    }

The model is just a record holding your state. init is the initial state.

JavaScript/Redux equivalent:

const initialState = { count: 0 };

2. Messages (Msg)

type Msg
    = Increment
    | Decrement

Messages describe things that can happen. They're like Redux action types, but type-safe.

JavaScript/Redux equivalent:

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

3. The Update Function

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

Takes a message and the current model, returns a new model. This is a pure function - no side effects!

JavaScript/Redux equivalent:

function reducer(state, action) {
    switch (action.type) {
        case INCREMENT:
            return { ...state, count: state.count + 1 };
        case DECREMENT:
            return { ...state, count: state.count - 1 };
        default:
            return state;
    }
}

4. The View Function

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        ]

A pure function that takes the model and returns HTML. The Html Msg type means "HTML that can produce Msg values."

React equivalent:

function Counter({ count, dispatch }) {
    return (
        <div>
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
            <div>{count}</div>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
        </div>
    );
}

5. Wiring It Together

main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Browser.sandbox connects everything. Elm handles:

  • Calling view whenever the model changes
  • Routing user events to update
  • Re-rendering efficiently (virtual DOM)

HTML in Elm

Every HTML element follows this pattern:

element attributes children
-- <div class="container">Hello</div>
div [ class "container" ] [ text "Hello" ]

-- <button id="submit" disabled>Submit</button>
button [ id "submit", disabled True ] [ text "Submit" ]

-- <input type="text" value="hello" />
input [ type_ "text", value "hello" ] []

Note: Some attributes like type use underscores (type_) because they conflict with Elm keywords.

Common Attributes

import Html.Attributes exposing (..)

-- Styling
class "my-class"
style "color" "red"
id "my-id"

-- Form inputs
value "text"
placeholder "Enter text..."
disabled True
checked True

-- Links and images
href "https://elm-lang.org"
src "image.png"
alt "Description"

Common Events

import Html.Events exposing (..)

onClick Msg              -- Button clicks
onInput (String -> Msg)  -- Text input
onSubmit Msg             -- Form submission
onMouseEnter Msg         -- Mouse enter
onMouseLeave Msg         -- Mouse leave

Example 2: Text Input

module TextInput exposing (main)

import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)


-- MODEL

type alias Model =
    { content : String
    }


init : Model
init =
    { content = ""
    }


-- UPDATE

type Msg
    = Change String


update : Msg -> Model -> Model
update msg model =
    case msg of
        Change newContent ->
            { model | content = newContent }


-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input 
            [ placeholder "Type something..."
            , value model.content
            , onInput Change
            ] 
            []
        , div [] [ text ("You typed: " ++ model.content) ]
        , div [] [ text ("Length: " ++ String.fromInt (String.length model.content)) ]
        ]


main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Key Point: Messages with Data

type Msg
    = Change String  -- This message carries a String!

-- onInput sends the input's value with the message
onInput Change  -- When input changes, send: Change "new value"

-- In update, extract the data
Change newContent ->
    { model | content = newContent }

Example 3: Todo List

A more complex example with a list of items:

module TodoList exposing (main)

import Browser
import Html exposing (Html, button, div, input, li, text, ul)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onClick, onInput)


-- MODEL

type alias Model =
    { todos : List String
    , newTodo : String
    }


init : Model
init =
    { todos = []
    , newTodo = ""
    }


-- UPDATE

type Msg
    = UpdateNewTodo String
    | AddTodo
    | RemoveTodo Int


update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateNewTodo text ->
            { model | newTodo = text }

        AddTodo ->
            if String.isEmpty model.newTodo then
                model
            else
                { model
                    | todos = model.todos ++ [ model.newTodo ]
                    , newTodo = ""
                }

        RemoveTodo index ->
            { model
                | todos = removeAt index model.todos
            }


removeAt : Int -> List a -> List a
removeAt index list =
    List.take index list ++ List.drop (index + 1) list


-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ div []
            [ input
                [ placeholder "Add a todo..."
                , value model.newTodo
                , onInput UpdateNewTodo
                ]
                []
            , button [ onClick AddTodo ] [ text "Add" ]
            ]
        , ul [] (List.indexedMap viewTodo model.todos)
        ]


viewTodo : Int -> String -> Html Msg
viewTodo index todo =
    li []
        [ text todo
        , button [ onClick (RemoveTodo index) ] [ text "x" ]
        ]


main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Key Concepts

  1. List.indexedMap - Like map, but gives you the index too
  2. Messages with multiple types of data - RemoveTodo Int carries which item to remove
  3. Helper functions - removeAt and viewTodo keep code organized

Exercise 4.1: Add Features to Counter

Enhance the counter with:

  1. A "Reset" button that sets count to 0
  2. A "Double" button that doubles the count
Solution
type Msg
    = Increment
    | Decrement
    | Reset
    | Double


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        Reset ->
            { model | count = 0 }

        Double ->
            { model | count = model.count * 2 }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        , button [ onClick Reset ] [ text "Reset" ]
        , button [ onClick Double ] [ text "Double" ]
        ]

Exercise 4.2: Temperature Converter

Build a Celsius to Fahrenheit converter:

  • Input field for Celsius
  • Display the Fahrenheit equivalent
  • Formula: F = C × 9/5 + 32
Solution
module TempConverter exposing (main)

import Browser
import Html exposing (Html, div, input, text)
import Html.Attributes exposing (placeholder, value)
import Html.Events exposing (onInput)


type alias Model =
    { celsius : String
    }


init : Model
init =
    { celsius = ""
    }


type Msg
    = UpdateCelsius String


update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateCelsius value ->
            { model | celsius = value }


celsiusToFahrenheit : Float -> Float
celsiusToFahrenheit c =
    c * 9 / 5 + 32


view : Model -> Html Msg
view model =
    let
        fahrenheit =
            case String.toFloat model.celsius of
                Just c ->
                    String.fromFloat (celsiusToFahrenheit c) ++ "°F"

                Nothing ->
                    "Enter a valid number"
    in
    div []
        [ input
            [ placeholder "Celsius"
            , value model.celsius
            , onInput UpdateCelsius
            ]
            []
        , text " °C = "
        , text fahrenheit
        ]


main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Exercise 4.3: Signup Form

Build a signup form with:

  • Name field
  • Email field
  • Password field
  • "Sign Up" button
  • Display "Welcome, {name}!" after clicking Sign Up
Solution
module Signup exposing (main)

import Browser
import Html exposing (Html, button, div, input, text)
import Html.Attributes exposing (placeholder, type_, value)
import Html.Events exposing (onClick, onInput)


type alias Model =
    { name : String
    , email : String
    , password : String
    , submitted : Bool
    }


init : Model
init =
    { name = ""
    , email = ""
    , password = ""
    , submitted = False
    }


type Msg
    = UpdateName String
    | UpdateEmail String
    | UpdatePassword String
    | Submit


update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateName value ->
            { model | name = value }

        UpdateEmail value ->
            { model | email = value }

        UpdatePassword value ->
            { model | password = value }

        Submit ->
            { model | submitted = True }


view : Model -> Html Msg
view model =
    if model.submitted then
        div [] [ text ("Welcome, " ++ model.name ++ "!") ]
    else
        div []
            [ div []
                [ input
                    [ placeholder "Name"
                    , value model.name
                    , onInput UpdateName
                    ]
                    []
                ]
            , div []
                [ input
                    [ placeholder "Email"
                    , type_ "email"
                    , value model.email
                    , onInput UpdateEmail
                    ]
                    []
                ]
            , div []
                [ input
                    [ placeholder "Password"
                    , type_ "password"
                    , value model.password
                    , onInput UpdatePassword
                    ]
                    []
                ]
            , button [ onClick Submit ] [ text "Sign Up" ]
            ]


main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Key Takeaways

  1. TEA has three parts: Model, View, Update
  2. Model holds all your application state
  3. View is a pure function: Model → HTML
  4. Update is a pure function: Msg → Model → Model
  5. Messages describe events and can carry data
  6. Browser.sandbox wires everything together
  7. HTML is just functions with attributes and children

The Elm Architecture Benefits

  1. No unexpected state changes - All updates go through update
  2. Time-travel debugging - Elm debugger lets you replay messages
  3. Easy testing - Pure functions with no side effects
  4. Predictable - Given a model and message, you know the result

What's Next?

In Lesson 5, we'll dive deeper into:

  • Working with Lists
  • The Maybe type for handling missing values
  • Pattern matching techniques
  • Common List operations

← Previous: Lesson 3 | Next: Lesson 5 - Lists & Maybe →