# Lesson 7: Final Project - Building a Complete App ## Learning Goals In this final lesson, you will: - Apply everything you've learned - Build a complete, functional application - Understand how to structure larger Elm projects - Learn about ports for JavaScript interop ## Project: Task Manager We'll build a task manager with: - Add, complete, and delete tasks - Filter tasks (all, active, completed) - Persist tasks to localStorage (using ports) - Clean, modular code structure ## Setting Up ```bash mkdir task-manager cd task-manager elm init ``` ## The Complete Application Create `src/Main.elm`: ```elm port module Main exposing (main) import Browser import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Json.Decode as Decode exposing (Decoder) import Json.Encode as Encode -- PORTS port saveTasks : Encode.Value -> Cmd msg port loadTasks : (Decode.Value -> msg) -> Sub msg -- MODEL type alias Task = { id : Int , description : String , completed : Bool } type Filter = All | Active | Completed type alias Model = { tasks : List Task , newTask : String , nextId : Int , filter : Filter } init : Decode.Value -> ( Model, Cmd Msg ) init flags = let loadedTasks = case Decode.decodeValue tasksDecoder flags of Ok tasks -> tasks Err _ -> [] nextId = case List.maximum (List.map .id loadedTasks) of Just maxId -> maxId + 1 Nothing -> 1 in ( { tasks = loadedTasks , newTask = "" , nextId = nextId , filter = All } , Cmd.none ) -- DECODERS & ENCODERS taskDecoder : Decoder Task taskDecoder = Decode.map3 Task (Decode.field "id" Decode.int) (Decode.field "description" Decode.string) (Decode.field "completed" Decode.bool) tasksDecoder : Decoder (List Task) tasksDecoder = Decode.list taskDecoder encodeTask : Task -> Encode.Value encodeTask task = Encode.object [ ( "id", Encode.int task.id ) , ( "description", Encode.string task.description ) , ( "completed", Encode.bool task.completed ) ] encodeTasks : List Task -> Encode.Value encodeTasks tasks = Encode.list encodeTask tasks -- UPDATE type Msg = UpdateNewTask String | AddTask | ToggleTask Int | DeleteTask Int | SetFilter Filter | ClearCompleted | LoadedTasks Decode.Value update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of UpdateNewTask text -> ( { model | newTask = text }, Cmd.none ) AddTask -> if String.isEmpty (String.trim model.newTask) then ( model, Cmd.none ) else let newTask = { id = model.nextId , description = String.trim model.newTask , completed = False } newTasks = model.tasks ++ [ newTask ] in ( { model | tasks = newTasks , newTask = "" , nextId = model.nextId + 1 } , saveTasks (encodeTasks newTasks) ) ToggleTask id -> let toggleTask task = if task.id == id then { task | completed = not task.completed } else task newTasks = List.map toggleTask model.tasks in ( { model | tasks = newTasks } , saveTasks (encodeTasks newTasks) ) DeleteTask id -> let newTasks = List.filter (\task -> task.id /= id) model.tasks in ( { model | tasks = newTasks } , saveTasks (encodeTasks newTasks) ) SetFilter filter -> ( { model | filter = filter }, Cmd.none ) ClearCompleted -> let newTasks = List.filter (\task -> not task.completed) model.tasks in ( { model | tasks = newTasks } , saveTasks (encodeTasks newTasks) ) LoadedTasks value -> case Decode.decodeValue tasksDecoder value of Ok tasks -> ( { model | tasks = tasks }, Cmd.none ) Err _ -> ( model, Cmd.none ) -- VIEW view : Model -> Html Msg view model = div [ class "container" ] [ h1 [] [ text "Task Manager" ] , viewInput model.newTask , viewFilters model.filter , viewTasks model , viewFooter model ] viewInput : String -> Html Msg viewInput newTask = div [ class "input-section" ] [ input [ placeholder "What needs to be done?" , value newTask , onInput UpdateNewTask , onEnter AddTask ] [] , button [ onClick AddTask ] [ text "Add" ] ] onEnter : Msg -> Attribute Msg onEnter msg = let isEnter code = if code == 13 then Decode.succeed msg else Decode.fail "not ENTER" in on "keydown" (Decode.andThen isEnter (Decode.field "keyCode" Decode.int)) viewFilters : Filter -> Html Msg viewFilters currentFilter = div [ class "filters" ] [ filterButton All currentFilter "All" , filterButton Active currentFilter "Active" , filterButton Completed currentFilter "Completed" ] filterButton : Filter -> Filter -> String -> Html Msg filterButton filter currentFilter label = button [ classList [ ( "active", filter == currentFilter ) ] , onClick (SetFilter filter) ] [ text label ] viewTasks : Model -> Html Msg viewTasks model = let filteredTasks = filterTasks model.filter model.tasks in if List.isEmpty model.tasks then p [ class "empty-message" ] [ text "No tasks yet. Add one above!" ] else if List.isEmpty filteredTasks then p [ class "empty-message" ] [ text "No tasks match this filter." ] else ul [ class "task-list" ] (List.map viewTask filteredTasks) filterTasks : Filter -> List Task -> List Task filterTasks filter tasks = case filter of All -> tasks Active -> List.filter (\t -> not t.completed) tasks Completed -> List.filter .completed tasks viewTask : Task -> Html Msg viewTask task = li [ classList [ ( "completed", task.completed ) ] ] [ input [ type_ "checkbox" , checked task.completed , onClick (ToggleTask task.id) ] [] , span [ class "task-description" ] [ text task.description ] , button [ class "delete-btn" , onClick (DeleteTask task.id) ] [ text "x" ] ] viewFooter : Model -> Html Msg viewFooter model = let activeCount = List.length (List.filter (\t -> not t.completed) model.tasks) completedCount = List.length (List.filter .completed model.tasks) itemWord = if activeCount == 1 then "item" else "items" in if List.isEmpty model.tasks then text "" else div [ class "footer" ] [ span [] [ text (String.fromInt activeCount ++ " " ++ itemWord ++ " left") ] , if completedCount > 0 then button [ onClick ClearCompleted ] [ text "Clear completed" ] else text "" ] -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions _ = loadTasks LoadedTasks -- MAIN main : Program Decode.Value Model Msg main = Browser.element { init = init , update = update , view = view , subscriptions = subscriptions } ``` ## The HTML File with JavaScript Create `index.html`: ```html Task Manager
``` ## Building and Running ```bash # Compile Elm to JavaScript elm make src/Main.elm --output=elm.js # Open index.html in your browser # On macOS: open index.html # On Linux: xdg-open index.html # On Windows: start index.html # Or use a local server python -m http.server 8000 # Then open http://localhost:8000 ``` ## Understanding Ports Ports allow Elm to communicate with JavaScript for side effects Elm can't handle natively. ### Defining Ports ```elm port module Main exposing (main) -- Outgoing: Elm -> JavaScript port saveTasks : Encode.Value -> Cmd msg -- Incoming: JavaScript -> Elm port loadTasks : (Decode.Value -> msg) -> Sub msg ``` ### JavaScript Side ```javascript // Subscribe to Elm's outgoing port app.ports.saveTasks.subscribe(function(tasks) { localStorage.setItem('elm-tasks', JSON.stringify(tasks)); }); // Send data to Elm's incoming port app.ports.loadTasks.send(someData); ``` ### Flags: Initial Data ```elm -- Elm receives flags in init init : Decode.Value -> ( Model, Cmd Msg ) init flags = -- Decode flags (initial tasks from localStorage) ``` ```javascript // JavaScript passes flags at startup Elm.Main.init({ node: document.getElementById('app'), flags: initialTasks }); ``` ## Code Organization Tips ### For Larger Apps ``` src/ ├── Main.elm # Entry point ├── Model.elm # Model type and init ├── Update.elm # Update function and messages ├── View.elm # View functions ├── Types.elm # Shared types ├── Api.elm # HTTP requests └── Ports.elm # Port declarations ``` ### Splitting the Model ```elm -- Types.elm module Types exposing (..) type alias Task = { id : Int , description : String , completed : Bool } type Filter = All | Active | Completed ``` ### Helper Functions Extract common patterns: ```elm -- Helpers.elm module Helpers exposing (..) updateTask : Int -> (Task -> Task) -> List Task -> List Task updateTask targetId transform tasks = List.map (\task -> if task.id == targetId then transform task else task ) tasks ``` ## Testing Your Elm Code ### Install elm-test ```bash npm install -g elm-test elm-test init ``` ### Writing Tests ```elm -- tests/TaskTests.elm module TaskTests exposing (..) import Expect import Test exposing (..) import Main exposing (filterTasks, Filter(..)) suite : Test suite = describe "Task filtering" [ test "All filter shows all tasks" <| \_ -> let tasks = [ { id = 1, description = "Task 1", completed = False } , { id = 2, description = "Task 2", completed = True } ] in filterTasks All tasks |> List.length |> Expect.equal 2 , test "Active filter shows only incomplete" <| \_ -> let tasks = [ { id = 1, description = "Task 1", completed = False } , { id = 2, description = "Task 2", completed = True } ] in filterTasks Active tasks |> List.length |> Expect.equal 1 ] ``` ### Running Tests ```bash elm-test ``` ## Exercises ### Exercise 7.1: Add Edit Functionality Add the ability to edit task descriptions: 1. Double-click a task to edit it 2. Press Enter to save 3. Press Escape to cancel
Hint Add a new field to the model: ```elm type alias Model = { tasks : List Task , newTask : String , nextId : Int , filter : Filter , editing : Maybe Int -- ID of task being edited , editText : String } ``` Add new messages: ```elm type Msg = ... | StartEditing Int String | UpdateEditText String | SaveEdit | CancelEdit ```
### Exercise 7.2: Add Due Dates Extend tasks to have optional due dates: 1. Add a date input when creating tasks 2. Display due dates with tasks 3. Highlight overdue tasks ### Exercise 7.3: Add Categories Add task categories: 1. Tasks can be tagged with a category 2. Filter by category 3. Add color coding ## Key Takeaways from This Tutorial 1. **Elm is purely functional** - All functions are pure, side effects are managed 2. **Types are your friend** - They catch bugs at compile time 3. **The Elm Architecture** - Model, View, Update provides clean structure 4. **Maybe eliminates nulls** - No more "undefined is not a function" 5. **JSON decoders** ensure type-safe API integration 6. **Ports** bridge Elm and JavaScript for browser APIs ## Where to Go From Here ### Resources - [Official Elm Guide](https://guide.elm-lang.org/) - The canonical resource - [Elm in Action](https://www.manning.com/books/elm-in-action) - Excellent book - [Elm Packages](https://package.elm-lang.org/) - Browse available libraries - [Elm Slack](https://elmlang.herokuapp.com/) - Friendly community ### Practice Projects 1. **Weather App** - Fetch and display weather data 2. **Markdown Preview** - Live markdown editor with preview 3. **Kanban Board** - Drag-and-drop task management 4. **Chat Application** - WebSocket integration with ports ### Advanced Topics to Explore - **elm-ui** - Layout without CSS - **elm-spa** - Single Page Applications - **GraphQL with elm-graphql** - **Web Components interop** ## Congratulations! You've completed this Elm tutorial! You now have: - A solid understanding of functional programming concepts - Knowledge of Elm's type system - Experience with The Elm Architecture - Skills to build real-world applications The best way to learn more is to build something. Pick a project and start coding! --- [← Previous: Lesson 6](06-http-json.md) | [Back to README](../README.md)