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

730 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```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:
```bash
elm reactor
```
Then open http://localhost:8000/src/Counter.elm
### Breaking It Down
#### 1. The Model
```elm
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:**
```javascript
const initialState = { count: 0 };
```
#### 2. Messages (Msg)
```elm
type Msg
= Increment
| Decrement
```
Messages describe things that can happen. They're like Redux action types, but type-safe.
**JavaScript/Redux equivalent:**
```javascript
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
```
#### 3. The Update Function
```elm
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:**
```javascript
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
```elm
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:**
```jsx
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
```elm
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:
```elm
element attributes children
```
```elm
-- <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
```elm
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
```elm
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
```elm
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
```elm
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:
```elm
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
<details>
<summary>Solution</summary>
```elm
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" ]
]
```
</details>
## 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
<details>
<summary>Solution</summary>
```elm
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
}
```
</details>
## 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
<details>
<summary>Solution</summary>
```elm
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
}
```
</details>
## 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](05-lists-maybe.md), we'll dive deeper into:
- Working with Lists
- The Maybe type for handling missing values
- Pattern matching techniques
- Common List operations
---
[← Previous: Lesson 3](03-functions.md) | [Next: Lesson 5 - Lists & Maybe →](05-lists-maybe.md)