# 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 (
{count}
); } ``` #### 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 --
Hello
div [ class "container" ] [ text "Hello" ] -- button [ id "submit", disabled True ] [ text "Submit" ] -- 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
Solution ```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" ] ] ```
## 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 ```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 } ```
## 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 ```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 } ```
## 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)