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:
856
lessons/07-final-project.md
Normal file
856
lessons/07-final-project.md
Normal file
@@ -0,0 +1,856 @@
|
||||
# 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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Task Manager</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-section input {
|
||||
flex: 1;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input-section input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.input-section button {
|
||||
padding: 12px 24px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.input-section button:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filters button {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filters button:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.filters button.active {
|
||||
background: #667eea;
|
||||
border-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.task-list li:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.task-list li.completed .task-description {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.task-list input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff6b6b;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.task-list li:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer button:hover {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="elm.js"></script>
|
||||
<script>
|
||||
// Load saved tasks from localStorage
|
||||
const savedTasks = localStorage.getItem('elm-tasks');
|
||||
const initialTasks = savedTasks ? JSON.parse(savedTasks) : [];
|
||||
|
||||
// Initialize Elm app with saved tasks
|
||||
const app = Elm.Main.init({
|
||||
node: document.getElementById('app'),
|
||||
flags: initialTasks
|
||||
});
|
||||
|
||||
// Listen for save commands from Elm
|
||||
app.ports.saveTasks.subscribe(function(tasks) {
|
||||
localStorage.setItem('elm-tasks', JSON.stringify(tasks));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
<details>
|
||||
<summary>Hint</summary>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 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)
|
||||
Reference in New Issue
Block a user