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
This commit is contained in:
2026-03-11 11:07:15 +00:00
commit 7d7986f3ab
16 changed files with 5342 additions and 0 deletions

729
lessons/04-tea.md Normal file
View File

@@ -0,0 +1,729 @@
# 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)