├── .gitignore ├── elm.json ├── 04-hello-world.elm ├── 01-hello-world.elm ├── 21-localstorage-editable-todos.html ├── 02-hello-world.elm ├── 23-filter-todos.html ├── 22-localstorage-editable-todos.html ├── 25-navigation-todos.html ├── 24-filter-todos.html ├── 03-hello-world.elm ├── 06-counter.elm ├── 08-counter.elm ├── 07-counter.elm ├── 13-input-box.elm ├── 14-todos.elm ├── 10-counters.elm ├── 11-counters.elm ├── 15-todos.elm ├── 16-todos.elm ├── README.md ├── 09-counters.elm ├── 17-todos.elm ├── 05-counter.elm ├── 12-counters.elm ├── 20-editable-todos.elm ├── 21-localstorage-editable-todos.elm ├── 22-localstorage-editable-todos.elm ├── 19-editable-todos.elm ├── 18-editable-todos.elm ├── 23-filter-todos.elm ├── 24-filter-todos.elm └── 25-navigation-todos.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js 3 | index.html 4 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "." 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.1", 10 | "elm/core": "1.0.2", 11 | "elm/html": "1.0.0", 12 | "elm/random": "1.0.0", 13 | "elm/url": "1.0.0" 14 | }, 15 | "indirect": { 16 | "elm/json": "1.0.0", 17 | "elm/time": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } -------------------------------------------------------------------------------- /04-hello-world.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- I'm importing the Html.Attributes module, which has all the HTML attributes 4 | -- we need. I'm exposing the class attribute, which we can use for adding classes 5 | -- to HTML elements. 6 | 7 | import Html exposing (Html, div, text) 8 | import Html.Attributes exposing (class) 9 | 10 | 11 | 12 | -- Now the main value has a div element which has a class of "text-center". Since 13 | -- we're using Bootstrap, this will make it so the child text node is centered. So 14 | -- now the "Hello, World!" message is centered. 15 | 16 | 17 | main : Html msg 18 | main = 19 | div [ class "text-center" ] [ text "Hello, World!" ] 20 | -------------------------------------------------------------------------------- /01-hello-world.elm: -------------------------------------------------------------------------------- 1 | -- This is how you write single-line comments in Elm. 2 | {- 3 | This is how you 4 | write multi-line comments 5 | in Elm. 6 | -} 7 | -- This is how you declare what your module name is and what values it exports. 8 | -- We've chosen to name our module Main and we are exporting the value main that 9 | -- we have defined below. 10 | 11 | 12 | module Main exposing (main) 13 | 14 | -- We're importing the Html module the text value available in our file, so we 15 | -- can just reference it if we want. 16 | 17 | import Html exposing (text) 18 | 19 | 20 | 21 | -- The main value manages what gets displayed on the page. If we set the main 22 | -- value to (text "Hello, World!"), then a text node with the string "Hello, World!" 23 | -- will display on the page. 24 | 25 | 26 | main = 27 | text "Hello, World!" 28 | -------------------------------------------------------------------------------- /21-localstorage-editable-todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /02-hello-world.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- We are now importing the Html type constructor from the Html module so that 4 | -- we can use it to annotate the main value's type. 5 | 6 | import Html exposing (Html, text) 7 | 8 | 9 | 10 | -- In Elm, we can explicitly say what any value's type is. Since the main value 11 | -- is just an HTML text node, it has the type (Html msg). If the value isn't 12 | -- that type, then we will get a compile error, which is helpful for 13 | -- guaranteeing our program is correct. Type declarations aren't required, but 14 | -- most people like writing type declarations so their code is easier to understand. 15 | 16 | 17 | main : Html msg 18 | main = 19 | text "Hello, World!" 20 | 21 | -- Exercises to get to Lesson 3 22 | 23 | {- 24 | Elm provides a variety of functions. For example, you can use the div function 25 | to add a html div tag: https://package.elm-lang.org/packages/elm/html/latest/Html#div 26 | 27 | Why not try to wrap the text above with a div tag? See you in the next lesson! 28 | -} -------------------------------------------------------------------------------- /23-filter-todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /22-localstorage-editable-todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /25-navigation-todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /24-filter-todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /03-hello-world.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- We are now exposing the div function. 4 | 5 | import Html exposing (Html, div, text) 6 | 7 | 8 | 9 | -- We've made it so the main value isn't just a text node anymore. It's now 10 | -- a div element with a text node as a child. The Html module has all the 11 | -- HTML elements you need. Each HTML element takes 2 arguments which are both 12 | -- lists. The first list is a list of attributes, the second list is a list of 13 | -- child HTML elements. We can nest elements the same way we normally do with 14 | -- HTML. 15 | 16 | 17 | main : Html msg 18 | main = 19 | div [] [ text "Hello, World!" ] 20 | 21 | 22 | -- Instructions on how to get to lesson 4: 23 | {- 24 | In the html functions provided by elm, you can often add html attributes. 25 | See here: https://package.elm-lang.org/packages/elm/html/latest/Html-Attributes#class 26 | 27 | The div method accepts a list of attributes as its first parameter. With that in mind, 28 | let us give this div a particular class: "text-center" - because we want to center that text. 29 | 30 | Please review the docoumentation pasted to ensure you understand what is going on, 31 | and we'll see you in lesson 4. 32 | -} 33 | 34 | 35 | -------------------------------------------------------------------------------- /06-counter.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | type Msg 10 | = Increment 11 | 12 | 13 | type alias Model = 14 | Int 15 | 16 | 17 | 18 | -- I added the type declaration to the view function so it's easier to understand. 19 | -- The view function takes a Model type and returns HTML. 20 | 21 | 22 | view : Model -> Html Msg 23 | view model = 24 | div [ class "text-center" ] 25 | [ div [] [ text (String.fromInt model) ] 26 | , button 27 | [ class "btn btn-primary", onClick Increment ] 28 | [ text "+" ] 29 | ] 30 | 31 | 32 | 33 | -- The type declaration for the update function means that the update function takes 34 | -- a Msg type and a Model type and then returns a Model type. 35 | 36 | 37 | update : Msg -> Model -> Model 38 | update msg model = 39 | case msg of 40 | Increment -> 41 | model + 1 42 | 43 | 44 | 45 | -- The main value is a Program type which doesn't have any flags, so the flags value 46 | -- is of type (). We'll talk more about flags later. For now, just know that we 47 | -- aren't using them, so we give them type (). Our model is of type Model and our 48 | -- message type is of type Msg. It's okay if this doesn't make much sense right now, 49 | -- you'll get used to these type declarations as you write more Elm code. 50 | 51 | 52 | main : Program () Model Msg 53 | main = 54 | sandbox 55 | { init = 0 56 | , view = view 57 | , update = update 58 | } 59 | -------------------------------------------------------------------------------- /08-counter.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- We've added another new Msg value that we're going to call Reset. 11 | 12 | 13 | type Msg 14 | = Increment 15 | | Decrement 16 | | Reset 17 | 18 | 19 | type alias Model = 20 | Int 21 | 22 | 23 | view : Model -> Html Msg 24 | view model = 25 | div [ class "text-center" ] 26 | [ div [] [ text (String.fromInt model) ] 27 | , div [ class "btn-group" ] 28 | [ button 29 | [ class "btn btn-primary", onClick Increment ] 30 | [ text "+" ] 31 | , button 32 | [ class "btn btn-danger", onClick Decrement ] 33 | [ text "-" ] 34 | 35 | -- We added a new button that will trigger an event 36 | -- that will pass the Reset value as a message to the 37 | -- update function. 38 | , button 39 | [ class "btn btn-default", onClick Reset ] 40 | [ text "Reset" ] 41 | ] 42 | ] 43 | 44 | 45 | 46 | -- We added a new entry in the case expression that checks for if the message 47 | -- is Reset. If it is, then the new model value will be 0. 48 | 49 | 50 | update : Msg -> Model -> Model 51 | update msg model = 52 | case msg of 53 | Increment -> 54 | model + 1 55 | 56 | Decrement -> 57 | model - 1 58 | 59 | Reset -> 60 | 0 61 | 62 | 63 | main : Program () Model Msg 64 | main = 65 | sandbox 66 | { init = 0 67 | , view = view 68 | , update = update 69 | } 70 | -------------------------------------------------------------------------------- /07-counter.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- We've added a new value called Decrement that is type Msg. 11 | -- Think of the Msg type as a type that can either be Increment 12 | -- or it can be Decrement. We use "|" between all the possible values a Msg type can be. 13 | 14 | 15 | type Msg 16 | = Increment 17 | | Decrement 18 | 19 | 20 | type alias Model = 21 | Int 22 | 23 | 24 | view : Model -> Html Msg 25 | view model = 26 | div [ class "text-center" ] 27 | [ div [] [ text (String.fromInt model) ] 28 | , div [ class "btn-group" ] 29 | [ button 30 | [ class "btn btn-primary", onClick Increment ] 31 | [ text "+" ] 32 | 33 | -- We added a new button that will trigger a Decrement 34 | -- value as the message when the button is clicked. The 35 | -- Decrement value will get passed into the update function 36 | -- whenever this button gets clicked. 37 | , button 38 | [ class "btn btn-danger", onClick Decrement ] 39 | [ text "-" ] 40 | ] 41 | ] 42 | 43 | 44 | 45 | -- Now that there are 2 possible Msg values, we added a new entry to the case 46 | -- expression that deals with messages that are equal to the Decrement value. 47 | -- When the message is a Decrement value, the new model value that's returned 48 | -- is one less than what it was. After the new model state is returned, the view 49 | -- function will get passed the new model value and return the new HTML, which 50 | -- will get displayed for the user to see. 51 | 52 | 53 | update : Msg -> Model -> Model 54 | update msg model = 55 | case msg of 56 | Increment -> 57 | model + 1 58 | 59 | Decrement -> 60 | model - 1 61 | 62 | 63 | main : Program () Model Msg 64 | main = 65 | sandbox 66 | { init = 0 67 | , view = view 68 | , update = update 69 | } 70 | -------------------------------------------------------------------------------- /13-input-box.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- We're exposing the value attribute. 4 | -- We're exposing the onInput event type. 5 | 6 | import Browser exposing (sandbox) 7 | import Html exposing (Html, div, input, text) 8 | import Html.Attributes exposing (class, value) 9 | import Html.Events exposing (onInput) 10 | 11 | 12 | 13 | -- We have a Msg type that can be the value (UpdateText String). 14 | 15 | 16 | type Msg 17 | = UpdateText String 18 | 19 | 20 | 21 | -- We made our model type a record that has a property called text. 22 | -- The text property has to be a String type. 23 | -- For example, our model can be the value { text = "hello" }. 24 | -- Records are similar to objects in JavaScript. 25 | 26 | 27 | type alias Model = 28 | { text : String } 29 | 30 | 31 | 32 | -- We have an input box that listens for an onInput event. When a user 33 | -- types in the input box, an onInput event will get triggered and the input 34 | -- box's text value will get passed with the UpdateText as a string. That's 35 | -- why the Msg type has the value (UpdateText String). The string that gets 36 | -- passed along with the message to the update function is the string of 37 | -- text that's in the input box. 38 | -- We display the model.text value in a div element underneath the input box. 39 | 40 | 41 | view : Model -> Html Msg 42 | view model = 43 | div [ class "text-center" ] 44 | [ input [ onInput UpdateText, value model.text ] [] 45 | , div [] [ text model.text ] 46 | ] 47 | 48 | 49 | 50 | -- We just have to handle one case for our message. All we do is set the 51 | -- text property in the model to the string that is currently in the input box. 52 | 53 | 54 | update : Msg -> Model -> Model 55 | update msg model = 56 | case msg of 57 | UpdateText newText -> 58 | { model | text = newText } 59 | 60 | 61 | 62 | -- We set the initial model value to { text = "" }, so the input box value 63 | -- is initially an empty string. 64 | 65 | 66 | main : Program () Model Msg 67 | main = 68 | sandbox 69 | { init = { text = "" } 70 | , view = view 71 | , update = update 72 | } 73 | -------------------------------------------------------------------------------- /14-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, input, text) 5 | import Html.Attributes exposing (autofocus, class, value) 6 | import Html.Events exposing (onClick, onInput) 7 | 8 | 9 | 10 | -- We added a new AddTodo message type. 11 | 12 | 13 | type Msg 14 | = UpdateText String 15 | | AddTodo 16 | 17 | 18 | 19 | -- We added a new property called todos, which is a list of strings. 20 | 21 | 22 | type alias Model = 23 | { text : String 24 | , todos : List String 25 | } 26 | 27 | 28 | 29 | -- We added (autofocus True), which is like the native HTML autofocus attribute. 30 | -- We also added a button that triggers an onClick event when clicked which 31 | -- passes an AddTodo message to the update function. 32 | 33 | 34 | view : Model -> Html Msg 35 | view model = 36 | div [ class "text-center" ] 37 | [ input [ onInput UpdateText, value model.text, autofocus True ] [] 38 | , button [ onClick AddTodo, class "btn btn-primary" ] [ text "Add Todo" ] 39 | , div [] (List.map (\todo -> div [] [ text todo ]) model.todos) 40 | ] 41 | 42 | 43 | update : Msg -> Model -> Model 44 | update msg model = 45 | case msg of 46 | UpdateText newText -> 47 | { model | text = newText } 48 | 49 | -- We append the model.text value to the end of our list of todo strings. 50 | AddTodo -> 51 | { model | text = "", todos = model.todos ++ [ model.text ] } 52 | 53 | 54 | 55 | -- We set the todos property so that it's initially an empty list. 56 | 57 | 58 | main : Program () Model Msg 59 | main = 60 | sandbox 61 | { init = { text = "", todos = [] } 62 | , view = view 63 | , update = update 64 | } 65 | 66 | -- Exercises to 15: 67 | {- 68 | Right now you cannot remove an item from the list. 69 | Let us add that ability. If we want to get rid of an item 70 | We should be able to click a button and remove it from the list. 71 | Hint: add a message called RemoveToDo (or something equivalent) 72 | and use the update function to remove that particular to do item 73 | from the model. In the view function, you can add a little 'X' 74 | button such that when the user clicks it, the RemoveToDo message 75 | is sent to the update fucntion. Good luck and see you in lesson 15! 76 | 77 | -} -------------------------------------------------------------------------------- /10-counters.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- We added an AddCount value as our Msg type, which we'll use to indicate 11 | -- that we want to add a counter to our list of counters. 12 | 13 | 14 | type Msg 15 | = Increment Int 16 | | AddCount 17 | 18 | 19 | type alias Model = 20 | List Int 21 | 22 | 23 | viewCount : Int -> Int -> Html Msg 24 | viewCount index count = 25 | div [ class "mb-2" ] 26 | [ text (String.fromInt count) 27 | , button 28 | [ class "btn btn-primary ml-2", onClick (Increment index) ] 29 | [ text "+" ] 30 | ] 31 | 32 | 33 | view : Model -> Html Msg 34 | view model = 35 | div [ class "text-center" ] 36 | [ div [ class "mb-2" ] 37 | -- We added a new button that we can click and it will make it 38 | -- so that we add a new counter to the list. 39 | -- When the user clicks on the "Add Count" button, it triggers 40 | -- an onClick event, which will pass the AddCount value into the 41 | -- update function. The update function will return a list which 42 | -- has an extra integer value at the end of the list, which 43 | -- represents the new counter. 44 | [ button 45 | [ class "btn btn-primary", onClick AddCount ] 46 | [ text "Add Count" ] 47 | ] 48 | , div [] (List.indexedMap viewCount model) 49 | ] 50 | 51 | 52 | update : Msg -> Model -> Model 53 | update msg model = 54 | case msg of 55 | Increment index -> 56 | List.indexedMap 57 | (\i count -> 58 | if i == index then 59 | count + 1 60 | 61 | else 62 | count 63 | ) 64 | model 65 | 66 | -- When the message type is AddCount, we will append a 0 to the end of 67 | -- the list. We can get this effect by concatenating 2 lists together. 68 | -- The ++ operator is the concatenate operator, which takes 2 lists and 69 | -- puts them together. 70 | -- For example: [ 1, 2 ] ++ [ 0 ] will be [ 1, 2, 0 ] 71 | AddCount -> 72 | model ++ [ 0 ] 73 | 74 | 75 | main : Program () Model Msg 76 | main = 77 | sandbox 78 | { init = [ 0, 0 ] 79 | , view = view 80 | , update = update 81 | } 82 | -------------------------------------------------------------------------------- /11-counters.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- We've added the (Decrement Int) value to the Msg union type. 11 | -- (Decrement Int) will work in a similar way that (Increment Int) works 12 | -- except it will decrement the counter at the specified index instead of 13 | -- incrementing it. 14 | 15 | 16 | type Msg 17 | = Increment Int 18 | | Decrement Int 19 | | AddCount 20 | 21 | 22 | type alias Model = 23 | List Int 24 | 25 | 26 | viewCount : Int -> Int -> Html Msg 27 | viewCount index count = 28 | div [ class "mb-2" ] 29 | [ text (String.fromInt count) 30 | , button 31 | [ class "btn btn-primary ml-2", onClick (Increment index) ] 32 | [ text "+" ] 33 | 34 | -- We added a button that will trigger pass a (Decrement Int) message 35 | -- to the update function when it's clicked. 36 | , button 37 | [ class "btn btn-primary ml-2", onClick (Decrement index) ] 38 | [ text "-" ] 39 | ] 40 | 41 | 42 | view : Model -> Html Msg 43 | view model = 44 | div [ class "text-center" ] 45 | [ div [ class "mb-2" ] 46 | [ button 47 | [ class "btn btn-primary", onClick AddCount ] 48 | [ text "Add Count" ] 49 | ] 50 | , div [] (List.indexedMap viewCount model) 51 | ] 52 | 53 | 54 | update : Msg -> Model -> Model 55 | update msg model = 56 | case msg of 57 | Increment index -> 58 | List.indexedMap 59 | (\i count -> 60 | if i == index then 61 | count + 1 62 | 63 | else 64 | count 65 | ) 66 | model 67 | 68 | -- We added an expression that handles the (Decrement Int) message value, 69 | -- which decrements the counter at the index that we care about. 70 | Decrement index -> 71 | List.indexedMap 72 | (\i count -> 73 | if i == index then 74 | count - 1 75 | 76 | else 77 | count 78 | ) 79 | model 80 | 81 | AddCount -> 82 | model ++ [ 0 ] 83 | 84 | 85 | main : Program () Model Msg 86 | main = 87 | sandbox 88 | { init = [ 0, 0 ] 89 | , view = view 90 | , update = update 91 | } 92 | -------------------------------------------------------------------------------- /15-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, input, span, text) 5 | import Html.Attributes exposing (autofocus, class, value) 6 | import Html.Events exposing (onClick, onInput) 7 | 8 | 9 | 10 | -- We added a (RemoveTodo Int) value to the Msg type, which will allow us 11 | -- to remove a todo by index. 12 | 13 | 14 | type Msg 15 | = UpdateText String 16 | | AddTodo 17 | | RemoveTodo Int 18 | 19 | 20 | type alias Model = 21 | { text : String 22 | , todos : List String 23 | } 24 | 25 | 26 | view : Model -> Html Msg 27 | view model = 28 | div [ class "text-center" ] 29 | [ input [ onInput UpdateText, value model.text, autofocus True ] [] 30 | , button 31 | [ onClick AddTodo, class "btn btn-primary" ] 32 | [ text "Add Todo" ] 33 | , div [] 34 | (List.indexedMap 35 | (\index todo -> 36 | div [] 37 | [ text todo 38 | 39 | -- We add a little "X" that we can click to remove 40 | -- the todo at the specified index. 41 | , span [ onClick (RemoveTodo index) ] [ text " X" ] 42 | ] 43 | ) 44 | model.todos 45 | ) 46 | ] 47 | 48 | 49 | update : Msg -> Model -> Model 50 | update msg model = 51 | case msg of 52 | UpdateText newText -> 53 | { model | text = newText } 54 | 55 | AddTodo -> 56 | { model | text = "", todos = model.todos ++ [ model.text ] } 57 | 58 | -- We use a let expression to get the todos before and after, then 59 | -- we set the newTodos value to them concatenated together. 60 | RemoveTodo index -> 61 | let 62 | beforeTodos = 63 | List.take index model.todos 64 | 65 | afterTodos = 66 | List.drop (index + 1) model.todos 67 | 68 | newTodos = 69 | beforeTodos ++ afterTodos 70 | in 71 | { model | todos = newTodos } 72 | 73 | 74 | main : Program () Model Msg 75 | main = 76 | sandbox 77 | { init = { text = "", todos = [] } 78 | , view = view 79 | , update = update 80 | } 81 | 82 | 83 | -- Exercises to get to lesson 16 84 | {- 85 | 86 | The task at hand is to: (i) add some bootstrap classes to make things look a little more presentable. 87 | Secondly, we wan add a place holder for the input - when we type new to dos in the field, a placeholder 88 | would be helpful for users: https://package.elm-lang.org/packages/elm/html/latest/Html-Attributes#placeholder 89 | 90 | If you are using elm make then you may have to get access to some bootstrap styles - add them to your 91 | index.html page (or whatever you have named your page). Then run elm make src/Main.elm --debug 92 | If you don't have an src directory, then you will have to create one. You will also have to name 93 | your elm file Main.elm for the above to work. 94 | 95 | -} 96 | -------------------------------------------------------------------------------- /16-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, input, span, text) 5 | import Html.Attributes exposing (autofocus, class, placeholder, value) 6 | import Html.Events exposing (onClick, onInput) 7 | 8 | 9 | type Msg 10 | = UpdateText String 11 | | AddTodo 12 | | RemoveTodo Int 13 | 14 | 15 | type alias Model = 16 | { text : String 17 | , todos : List String 18 | } 19 | 20 | 21 | 22 | -- We added some nice little touches to the web app. We added a placeholder 23 | -- attribute that's similar to the placeholder attribute you're used to with 24 | -- native HTML. 25 | 26 | 27 | view : Model -> Html Msg 28 | view model = 29 | div [ class "col-12 col-sm-6 offset-sm-3" ] 30 | [ div [ class "row" ] 31 | [ div [ class "col-9" ] 32 | [ input 33 | [ onInput UpdateText 34 | , value model.text 35 | , autofocus True 36 | , class "form-control" 37 | , placeholder "Enter a todo" 38 | ] 39 | [] 40 | ] 41 | , div [ class "col-3" ] 42 | [ button 43 | [ onClick AddTodo, class "btn btn-primary form-control" ] 44 | [ text "+" ] 45 | ] 46 | ] 47 | , div [] (List.indexedMap viewTodo model.todos) 48 | ] 49 | 50 | 51 | 52 | -- We made the styling nicer by taking advantage of Bootstrap classes. 53 | 54 | 55 | viewTodo : Int -> String -> Html Msg 56 | viewTodo index todo = 57 | div [ class "card" ] 58 | [ div [ class "card-block" ] 59 | [ text todo 60 | , span 61 | [ onClick (RemoveTodo index) 62 | , class "float-right" 63 | ] 64 | [ text "✖" ] 65 | ] 66 | ] 67 | 68 | 69 | update : Msg -> Model -> Model 70 | update msg model = 71 | case msg of 72 | UpdateText newText -> 73 | { model | text = newText } 74 | 75 | AddTodo -> 76 | { model | text = "", todos = model.todos ++ [ model.text ] } 77 | 78 | RemoveTodo index -> 79 | let 80 | beforeTodos = 81 | List.take index model.todos 82 | 83 | afterTodos = 84 | List.drop (index + 1) model.todos 85 | 86 | newTodos = 87 | beforeTodos ++ afterTodos 88 | in 89 | { model | todos = newTodos } 90 | 91 | 92 | main : Program () Model Msg 93 | main = 94 | sandbox 95 | { init = { text = "", todos = [] } 96 | , view = view 97 | , update = update 98 | } 99 | 100 | 101 | 102 | {- 103 | Exercises to get to Lesson 17: 104 | 105 | We want to be able to hit the enter button to add a todo. 106 | 107 | How do we do this? 108 | To accomplish this, we wrap the input and button 109 | in a form and made the AddTodo message get passed whenever an onSubmit 110 | event gets triggered. The onSubmit event will get triggered whenever 111 | the user hits enter in the input box or clicks on the button. Good luck and see you in lesson 17! 112 | 113 | -} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 25 Elm Examples 2 | 3 | These 25 Elm examples will take you from building a static view all the way to building an interactive web app with routing. 4 | 5 | ## Links to the examples using Ellie 6 | 7 | - [01-hello-world.elm](https://ellie-app.com/3Z93zn5Qy3Da1) 8 | - [02-hello-world.elm](https://ellie-app.com/3Z93XBfHxcxa1) 9 | - [03-hello-world.elm](https://ellie-app.com/3Z94mwpfHN7a1) 10 | - [04-hello-world.elm](https://ellie-app.com/3Z95c4y6L9ha1) 11 | - [05-counter.elm](https://ellie-app.com/3Z95CNBn5Jha1) 12 | - [06-counter.elm](https://ellie-app.com/3Z97wqKsdn5a1) 13 | - [07-counter.elm](https://ellie-app.com/3Z97Y4VvSfRa1) 14 | - [08-counter.elm](https://ellie-app.com/3Z989cTkqPja1) 15 | - [09-counters.elm](https://ellie-app.com/3Z98PYxYvC4a1) 16 | - [10-counters.elm](https://ellie-app.com/3Z998Dq7PD5a1) 17 | - [11-counters.elm](https://ellie-app.com/3Z99qnMwbQra1) 18 | - [12-counters.elm](https://ellie-app.com/3Z99K3tpYbKa1) 19 | - [13-input-box.elm](https://ellie-app.com/3Z99YBq3jM8a1) 20 | - [14-todos.elm](https://ellie-app.com/3Z9bnpxFvffa1) 21 | - [15-todos.elm](https://ellie-app.com/3Z9bCnDPRv8a1) 22 | - [16-todos.elm](https://ellie-app.com/3Z9bXz3RsyQa1) 23 | - [17-todos.elm](https://ellie-app.com/3Z9cf8ZrCpta1) 24 | - [18-editable-todos.elm](https://ellie-app.com/3Z9cDJ3N9g4a1) 25 | - [19-editable-todos.elm](https://ellie-app.com/3Z9d3dXRdPxa1) 26 | - [20-editable-todos.elm](https://ellie-app.com/3Z9dhJHkXg6a1) 27 | - [21-localstorage-editable-todos.elm](https://ellie-app.com/3Z9dKFRMyNDa1) 28 | - [22-localstorage-editable-todos.elm](https://ellie-app.com/3Z9fg9YtbV4a1) 29 | - [23-filter-todos.elm](https://ellie-app.com/3Z9fFVqF9Pra1) 30 | - [24-filter-todos.elm](https://ellie-app.com/3Z9gnSZKYPTa1) 31 | - [25-navigation-todos.elm](https://ellie-app.com/3wR7QgwzngSa1/25) 32 | 33 | ## How to run and view examples 21-25 on your computer 34 | You can use the Ellie links for examples 21-25, but if you want to run it locally, 35 | you can copy/paste the following commands which will clone the Github repo, 36 | then compile example 21 to a JavaScript file called elm.js, 37 | then you will open the HTML for example 21 in your default browser. 38 | 39 | ```bash 40 | git clone https://github.com/bryanjenningz/25-elm-examples.git 41 | cd 25-elm-examples 42 | elm make 21-localstorage-editable-todos.elm --output elm.js 43 | elm reactor 44 | # The elm reactor command will server your files to http://localhost:8000, 45 | # so go to http://localhost:8000 in your browser and select the HTML file. 46 | ``` 47 | 48 | The above code will compile and open example 21 in your default browser. 49 | To compile and view examples 22 through 25 locally, you do the same process of 50 | compiling the Elm file you want to the elm.js JavaScript file, 51 | then running `elm reactor` and selecting the HTML file in your browser at 52 | http://localhost:8000. 53 | 54 | ## Other Free Resources 55 | 56 | - [Official Elm Guide](https://guide.elm-lang.org/) 57 | - [Ellie App](https://ellie-app.com/) 58 | - [Elm subreddit](https://www.reddit.com/r/elm/) 59 | - [Elm Slack channel](https://elmlang.slack.com/) 60 | - [RealWorld Elm example app](https://github.com/rtfeldman/elm-spa-example) 61 | 62 | 63 | These 25 Elm examples were inspired by the [examples](http://elm-lang.org/examples) 64 | on the Elm website and the [official Elm guide](https://guide.elm-lang.org/). 65 | Each example tries to build off of the previous example by adding a small amount 66 | of code so that it's easy to understand and see how you can build stuff in Elm. 67 | If you have any suggestions for more examples that should be added or if anything 68 | is unclear, add an issue or make a pull request. 69 | -------------------------------------------------------------------------------- /09-counters.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- Before, we had just one counter, but now we are working with multiple 11 | -- counters. 12 | -- Before, we had the Msg type as Increment, which was enough information for 13 | -- us to know that we can increment our counter. Now that we have multiple 14 | -- counters, we need to pass information about which counter we want to 15 | -- increment. So now we have (Increment Int) as our Msg type, where the Int 16 | -- value will indicate the index of the counter we want to increment. So if we 17 | -- want to increment the counter at index 0, we can have the onClick event 18 | -- trigger the message (Increment 0). If we want to increment the counter at 19 | -- index 1, we can trigger the message (Increment 1). 20 | 21 | 22 | type Msg 23 | = Increment Int 24 | 25 | 26 | 27 | -- We've changed the Model alias so that it's a list of integers. Each integer 28 | -- represents a counter's count in a list of counters. 29 | 30 | 31 | type alias Model = 32 | List Int 33 | 34 | 35 | 36 | -- This is a function that takes the index value, the count value, and returns 37 | -- the HTML that represents the counter. When the button is clicked, it will 38 | -- trigger an onClick event which will pass (Increment index) as the message to 39 | -- the update function. The index value is the index that the button is at in 40 | -- the list of buttons. 41 | 42 | 43 | viewCount : Int -> Int -> Html Msg 44 | viewCount index count = 45 | div [ class "mb-2" ] 46 | [ text (String.fromInt count) 47 | , button 48 | [ class "btn btn-primary", onClick (Increment index) ] 49 | [ text "+" ] 50 | ] 51 | 52 | 53 | 54 | -- The view function returns a div element that has a list of counters 55 | -- as its child. The List.indexedMap function takes a function and a list 56 | -- and returns a mapped version of the list that it took as an argument. 57 | -- List.indexedMap is similar to Array.prototype.map in JavaScript. 58 | -- The list index will get passed as the first argument to the function and 59 | -- the list value will get passed as the second argument to the function. 60 | 61 | 62 | view : Model -> Html Msg 63 | view model = 64 | div [ class "text-center" ] 65 | (List.indexedMap viewCount model) 66 | 67 | 68 | 69 | -- We only need to deal with one type of message which is the (Increment Int) 70 | -- value message. Whenever a counter is clicked, it will trigger an event which 71 | -- passes the (Increment Int) value into the update function. 72 | -- We map the model, which is a list and we update the value at the index that 73 | -- got clicked by using the List.indexedMap function. 74 | 75 | 76 | update : Msg -> Model -> Model 77 | update msg model = 78 | case msg of 79 | Increment index -> 80 | List.indexedMap 81 | (\i count -> 82 | if i == index then 83 | count + 1 84 | 85 | else 86 | count 87 | ) 88 | model 89 | 90 | 91 | 92 | -- We set the model value to initially be [ 0, 0 ], so our view will display 93 | -- 2 counters that each have the value 0 in the beginning. 94 | 95 | 96 | main : Program () Model Msg 97 | main = 98 | sandbox 99 | { init = [ 0, 0 ] 100 | , view = view 101 | , update = update 102 | } 103 | -------------------------------------------------------------------------------- /17-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, form, input, span, text) 5 | import Html.Attributes exposing (autofocus, class, placeholder, value) 6 | import Html.Events exposing (onClick, onInput, onSubmit) 7 | 8 | 9 | type Msg 10 | = UpdateText String 11 | | AddTodo 12 | | RemoveTodo Int 13 | 14 | 15 | type alias Model = 16 | { text : String 17 | , todos : List String 18 | } 19 | 20 | 21 | 22 | -- We made it so you can now hit enter to add a todo instead of having to 23 | -- click on the button. To accomplish this, we wrapped the input and button 24 | -- in a form and made the AddTodo message get passed whenever an onSubmit 25 | -- event gets triggered. The onSubmit event will get triggered whenever 26 | -- the user hits enter in the input box or clicks on the button. 27 | 28 | 29 | view : Model -> Html Msg 30 | view model = 31 | div [ class "col-12 col-sm-6 offset-sm-3" ] 32 | [ form [ class "row", onSubmit AddTodo ] 33 | [ div [ class "col-9" ] 34 | [ input 35 | [ onInput UpdateText 36 | , value model.text 37 | , autofocus True 38 | , class "form-control" 39 | , placeholder "Enter a todo" 40 | ] 41 | [] 42 | ] 43 | , div [ class "col-3" ] 44 | [ button 45 | [ class "btn btn-primary form-control" ] 46 | [ text "+" ] 47 | ] 48 | ] 49 | , div [] (List.indexedMap viewTodo model.todos) 50 | ] 51 | 52 | 53 | viewTodo : Int -> String -> Html Msg 54 | viewTodo index todo = 55 | div [ class "card" ] 56 | [ div [ class "card-block" ] 57 | [ text todo 58 | , span 59 | [ onClick (RemoveTodo index) 60 | , class "float-right" 61 | ] 62 | [ text "✖" ] 63 | ] 64 | ] 65 | 66 | 67 | update : Msg -> Model -> Model 68 | update msg model = 69 | case msg of 70 | UpdateText newText -> 71 | { model | text = newText } 72 | 73 | AddTodo -> 74 | { model | text = "", todos = model.todos ++ [ model.text ] } 75 | 76 | RemoveTodo index -> 77 | let 78 | beforeTodos = 79 | List.take index model.todos 80 | 81 | afterTodos = 82 | List.drop (index + 1) model.todos 83 | 84 | newTodos = 85 | beforeTodos ++ afterTodos 86 | in 87 | { model | todos = newTodos } 88 | 89 | 90 | main : Program () Model Msg 91 | main = 92 | sandbox 93 | { init = { text = "", todos = [] } 94 | , view = view 95 | , update = update 96 | } 97 | 98 | {- 99 | Exercises to proceed to # 18 100 | 101 | What do we need to do? 102 | What if we wanted to edit an existing to do item? How would we go about doing that? 103 | We want to be able to double click and existing to do item. When we double click 104 | that item, we want it to turn into an input form field, so that we can edit it. 105 | When we press enter, we want the todo item to be updated, and we want the form 106 | to disappear and things to appear as normal. 107 | 108 | HINT: We need to keep track of which to do item is being edited. If an item is being edited 109 | then we need to: (i) present a input text field. And when we are finished editing, we need to 110 | save the the new to do element. 111 | 112 | Play around with this to see what we want to create: https://ellie-app.com/3Z9cDJ3N9g4a1 113 | Then try to create it using your own solution. Do not look at the code that is there, 114 | otherwise you won't learn as much. 115 | 116 | 117 | -} -------------------------------------------------------------------------------- /05-counter.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- We've exposed 3 new values: sandbox, button, and onClick. 4 | -- The first one is sandbox, which will allow us to write an interactive 5 | -- application instead of just static HTML like before. We've also exposed 6 | -- the button function, which will be displayed as a button element. 7 | -- We are exposing the onClick function from the Html.Events module. We use 8 | -- this similarly to how we use the onclick attribute in native HTML. 9 | 10 | import Browser exposing (sandbox) 11 | import Html exposing (Html, button, div, text) 12 | import Html.Attributes exposing (class) 13 | import Html.Events exposing (onClick) 14 | 15 | 16 | 17 | -- We created a new type that we're calling Msg. The Msg type can only be 18 | -- the value Increment. 19 | 20 | 21 | type Msg 22 | = Increment 23 | 24 | 25 | 26 | -- We are creating a type alias called Model. Type aliases don't create a new 27 | -- type, they just make it so the program is easier to understand. 28 | 29 | 30 | type alias Model = 31 | Int 32 | 33 | 34 | 35 | -- This is the view function. The view function takes the model, which is an 36 | -- integer, then returns an HTML element, which gets displayed on the screen. 37 | -- Every time the model gets updated, the new value for the model will get passed 38 | -- into the view function, which will output the HTML display. 39 | -- So the view function is simply just a pure function that takes in the model 40 | -- state as an argument and returns the HTML view that gets displayed on the page. 41 | 42 | 43 | view model = 44 | div [ class "text-center" ] 45 | [ div [] [ text (String.fromInt model) ] 46 | , button 47 | -- The onClick function takes Increment value and will trigger an event 48 | -- whenever the user clicks on the button. 49 | -- When an event is triggered, the message value gets passed to the update 50 | -- function, then the update function returns the new model state. 51 | -- So whenever a user clicks the button, the onClick event will get triggered 52 | -- which will pass the Increment value to the update function. 53 | [ class "btn btn-primary", onClick Increment ] 54 | [ text "+" ] 55 | ] 56 | 57 | 58 | 59 | -- The update function will get called whenever an event is triggered. The message value 60 | -- will be passed in as the first value and the current model state will be passed in as 61 | -- the second value. The update function returns the new model state, which will be passed 62 | -- into the view function. 63 | 64 | 65 | update msg model = 66 | -- Here we're using a case expression, which is similar to a switch statement in 67 | -- JavaScript. We are checking to see what value the msg argument is. If the msg 68 | -- argument is Increment, then we're going to return the model value plus one. So 69 | -- we are effectively incrementing the model's value by one. This new model value will 70 | -- be passed into the view function and the view function will return the new HTML 71 | -- that gets rendered to the page. 72 | case msg of 73 | Increment -> 74 | model + 1 75 | 76 | 77 | 78 | -- We've changed the main value so now instead of being static HTML, it's a sandbox 79 | -- program. We use the sandbox function and pass in a record. The record has 80 | -- to have 3 properties: init, view, and update. The init property is the initial 81 | -- value that the model is set to. The view property is the view function which takes 82 | -- the model and returns the displayed HTML, and the update property is a function 83 | -- that takes a message and the model as arguments and returns the new model. 84 | -- Initially, the model will be passed to the view function as an argument and the 85 | -- view function will return the HTML. The sandbox will handle displaying 86 | -- that HTML to the page so the user can see and interact with that HTML. If the user 87 | -- triggers any events like the onClick event we have, the message and the model will 88 | -- get passed to the update function and then the update function will return the new 89 | -- model. Now that the model is different, the view function will get passed the new 90 | -- model value and return the new HTML. That new HTML will get displayed on the screen. 91 | 92 | 93 | main = 94 | sandbox 95 | { init = 0 96 | , view = view 97 | , update = update 98 | } 99 | -------------------------------------------------------------------------------- /12-counters.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (sandbox) 4 | import Html exposing (Html, button, div, text) 5 | import Html.Attributes exposing (class) 6 | import Html.Events exposing (onClick) 7 | 8 | 9 | 10 | -- We've added a (Remove Int) value to our Msg union type. 11 | -- The (Remove Int) value will be represent removing a counter at the 12 | -- specified index. 13 | 14 | 15 | type Msg 16 | = Increment Int 17 | | Decrement Int 18 | | Remove Int 19 | | AddCount 20 | 21 | 22 | type alias Model = 23 | List Int 24 | 25 | 26 | viewCount : Int -> Int -> Html Msg 27 | viewCount index count = 28 | div [ class "mb-2" ] 29 | [ text (String.fromInt count) 30 | , button 31 | [ class "btn btn-primary ml-2", onClick (Increment index) ] 32 | [ text "+" ] 33 | , button 34 | [ class "btn btn-primary ml-2", onClick (Decrement index) ] 35 | [ text "-" ] 36 | 37 | -- We've added a button which will trigger the (Remove Int) message when 38 | -- it's clicked. 39 | , button 40 | [ class "btn btn-primary ml-2", onClick (Remove index) ] 41 | [ text "X" ] 42 | ] 43 | 44 | 45 | view : Model -> Html Msg 46 | view model = 47 | div [ class "text-center" ] 48 | [ div [ class "mb-2" ] 49 | [ button 50 | [ class "btn btn-primary", onClick AddCount ] 51 | [ text "Add Count" ] 52 | ] 53 | , div [] (List.indexedMap viewCount model) 54 | ] 55 | 56 | 57 | update : Msg -> Model -> Model 58 | update msg model = 59 | case msg of 60 | Increment index -> 61 | List.indexedMap 62 | (\i count -> 63 | if i == index then 64 | count + 1 65 | 66 | else 67 | count 68 | ) 69 | model 70 | 71 | Decrement index -> 72 | List.indexedMap 73 | (\i count -> 74 | if i == index then 75 | count - 1 76 | 77 | else 78 | count 79 | ) 80 | model 81 | 82 | -- We've added this clause to our case expression that handles the 83 | -- (Remove Int) message. 84 | Remove index -> 85 | -- This is a let expression. Let expressions allow us to create 86 | -- temporary values that we can use to make our code more readable. 87 | let 88 | -- We're using List.take to get the values in the list that are 89 | -- before the index in the list. 90 | -- The List.take function will take the first n elements from 91 | -- the list that it gets passed. 92 | -- For example: 93 | -- List.take 2 [1, 3, 5, 4] == [1, 3] 94 | -- List.take 1 [3, 2, 1] == [3] 95 | -- List.take 0 [5, 6, 7] == [] 96 | -- Since we want to get all the values in the list that are 97 | -- before the index, we can write List.take index model 98 | before = 99 | List.take index model 100 | 101 | -- We're using List.drop to get the values in the list that are 102 | -- after the index in the list. 103 | -- The List.drop function will drop the first n elements from 104 | -- the list that it gets passed. 105 | -- For example: 106 | -- List.drop 2 [1, 3, 5, 4] == [5, 4] 107 | -- List.drop 1 [3, 2, 1] == [2, 1] 108 | -- List.drop 0 [5, 6, 7] == [5, 6, 7] 109 | -- Since we want to get all the values after the index, we can 110 | -- drop the first (index + 1) by writing List.drop (index + 1) model. 111 | after = 112 | List.drop (index + 1) model 113 | in 114 | -- This is the expression that gets returned in the let expression. 115 | -- Since we have the list of values before the removed index and 116 | -- the list of values after the removed index, we can concatenate 117 | -- them together and that will be the new value that we use as 118 | -- our model. 119 | before ++ after 120 | 121 | AddCount -> 122 | model ++ [ 0 ] 123 | 124 | 125 | main : Program () Model Msg 126 | main = 127 | sandbox 128 | { init = [ 0, 0 ] 129 | , view = view 130 | , update = update 131 | } 132 | -------------------------------------------------------------------------------- /20-editable-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser exposing (element) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (autofocus, class, placeholder, value) 6 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 7 | 8 | 9 | type Msg 10 | = UpdateText String 11 | | AddTodo 12 | | RemoveTodo Int 13 | | Edit Int String 14 | | EditSave Int String 15 | 16 | 17 | type alias TodoEdit = 18 | { index : Int 19 | , text : String 20 | } 21 | 22 | 23 | type alias Model = 24 | { text : String 25 | , todos : List String 26 | , editing : Maybe TodoEdit 27 | } 28 | 29 | 30 | view : Model -> Html Msg 31 | view model = 32 | div [ class "col-12 col-sm-6 offset-sm-3" ] 33 | [ form [ class "row", onSubmit AddTodo ] 34 | [ div [ class "col-9" ] 35 | [ input 36 | [ onInput UpdateText 37 | , value model.text 38 | , autofocus True 39 | , class "form-control" 40 | , placeholder "Enter a todo" 41 | ] 42 | [] 43 | ] 44 | , div [ class "col-3" ] 45 | [ button 46 | [ class "btn btn-primary form-control" ] 47 | [ text "+" ] 48 | ] 49 | ] 50 | , div [] (List.indexedMap (viewTodo model.editing) model.todos) 51 | ] 52 | 53 | 54 | viewTodo : Maybe TodoEdit -> Int -> String -> Html Msg 55 | viewTodo editing index todo = 56 | case editing of 57 | Just todoEdit -> 58 | if todoEdit.index == index then 59 | viewEditTodo index todoEdit 60 | 61 | else 62 | viewNormalTodo index todo 63 | 64 | Nothing -> 65 | viewNormalTodo index todo 66 | 67 | 68 | viewEditTodo : Int -> TodoEdit -> Html Msg 69 | viewEditTodo index todoEdit = 70 | div [ class "card" ] 71 | [ div [ class "card-block" ] 72 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 73 | [ input 74 | [ onInput (Edit index) 75 | , class "form-control" 76 | , value todoEdit.text 77 | ] 78 | [] 79 | ] 80 | ] 81 | ] 82 | 83 | 84 | viewNormalTodo : Int -> String -> Html Msg 85 | viewNormalTodo index todo = 86 | div [ class "card" ] 87 | [ div [ class "card-block" ] 88 | [ span 89 | [ onDoubleClick (Edit index todo) ] 90 | [ text todo ] 91 | , span 92 | [ onClick (RemoveTodo index) 93 | , class "float-right" 94 | ] 95 | [ text "✖" ] 96 | ] 97 | ] 98 | 99 | 100 | update : Msg -> Model -> ( Model, Cmd Msg ) 101 | update msg model = 102 | case msg of 103 | UpdateText newText -> 104 | ( { model | text = newText }, Cmd.none ) 105 | 106 | AddTodo -> 107 | ( { model | text = "", todos = model.todos ++ [ model.text ] } 108 | , Cmd.none 109 | ) 110 | 111 | RemoveTodo index -> 112 | let 113 | beforeTodos = 114 | List.take index model.todos 115 | 116 | afterTodos = 117 | List.drop (index + 1) model.todos 118 | 119 | newTodos = 120 | beforeTodos ++ afterTodos 121 | in 122 | ( { model | todos = newTodos }, Cmd.none ) 123 | 124 | Edit index todoText -> 125 | ( { model | editing = Just { index = index, text = todoText } } 126 | , Cmd.none 127 | ) 128 | 129 | EditSave index todoText -> 130 | let 131 | newTodos = 132 | List.indexedMap 133 | (\i todo -> 134 | if i == index then 135 | todoText 136 | 137 | else 138 | todo 139 | ) 140 | model.todos 141 | in 142 | ( { model | editing = Nothing, todos = newTodos } 143 | , Cmd.none 144 | ) 145 | 146 | 147 | subscriptions : Model -> Sub Msg 148 | subscriptions model = 149 | Sub.none 150 | 151 | 152 | init : () -> ( Model, Cmd Msg ) 153 | init flags = 154 | -- This is the short-hand version of what we had before. 155 | -- Before we had the model initially as the value: 156 | -- { text = "", todos = [ "Laundry", "Dishes" ], editing = Nothing } 157 | -- We can also represent this value as this: 158 | -- Model "" [ "Laundry", "Dishes" ] Nothing 159 | -- Whenever we make a type alias that's a record, like Model, we 160 | -- can use Model as a constructor function that returns a Model record. 161 | -- Since we defined the Model type alias like this: 162 | -- type alias Model = 163 | -- { text : String 164 | -- , todos : List String 165 | -- , editing : Maybe TodoEdit 166 | -- } 167 | -- (Model "" [ "Laundry", "Dishes" ] Nothing) will make the first 168 | -- argument the text property since that is first in the type alias 169 | -- declaration. The second argument will be the todos property, and 170 | -- the third argument will be the editing property. 171 | ( Model "" [ "Laundry", "Dishes" ] Nothing 172 | , Cmd.none 173 | ) 174 | 175 | 176 | main : Program () Model Msg 177 | main = 178 | element 179 | { init = init 180 | , view = view 181 | , update = update 182 | , subscriptions = subscriptions 183 | } 184 | -------------------------------------------------------------------------------- /21-localstorage-editable-todos.elm: -------------------------------------------------------------------------------- 1 | -- We added "port" in from of the module declaration, which indicates that we 2 | -- are using ports in this module. Ports are a way of communicating to JavaScript. 3 | -- We're using ports to save our model state in localStorage. 4 | 5 | 6 | port module Main exposing (main) 7 | 8 | import Browser exposing (element) 9 | import Html exposing (..) 10 | import Html.Attributes exposing (autofocus, class, placeholder, value) 11 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 12 | 13 | 14 | type Msg 15 | = UpdateText String 16 | | AddTodo 17 | | RemoveTodo Int 18 | | Edit Int String 19 | | EditSave Int String 20 | 21 | 22 | type alias TodoEdit = 23 | { index : Int 24 | , text : String 25 | } 26 | 27 | 28 | type alias Model = 29 | { text : String 30 | , todos : List String 31 | , editing : Maybe TodoEdit 32 | } 33 | 34 | 35 | view : Model -> Html Msg 36 | view model = 37 | div [ class "col-12 col-sm-6 offset-sm-3" ] 38 | [ form [ class "row", onSubmit AddTodo ] 39 | [ div [ class "col-9" ] 40 | [ input 41 | [ onInput UpdateText 42 | , value model.text 43 | , autofocus True 44 | , class "form-control" 45 | , placeholder "Enter a todo" 46 | ] 47 | [] 48 | ] 49 | , div [ class "col-3" ] 50 | [ button 51 | [ class "btn btn-primary form-control" ] 52 | [ text "+" ] 53 | ] 54 | ] 55 | , div [] (List.indexedMap (viewTodo model.editing) model.todos) 56 | ] 57 | 58 | 59 | viewTodo : Maybe TodoEdit -> Int -> String -> Html Msg 60 | viewTodo editing index todo = 61 | case editing of 62 | Just todoEdit -> 63 | if todoEdit.index == index then 64 | viewEditTodo index todoEdit 65 | 66 | else 67 | viewNormalTodo index todo 68 | 69 | Nothing -> 70 | viewNormalTodo index todo 71 | 72 | 73 | viewEditTodo : Int -> TodoEdit -> Html Msg 74 | viewEditTodo index todoEdit = 75 | div [ class "card" ] 76 | [ div [ class "card-block" ] 77 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 78 | [ input 79 | [ onInput (Edit index) 80 | , class "form-control" 81 | , value todoEdit.text 82 | ] 83 | [] 84 | ] 85 | ] 86 | ] 87 | 88 | 89 | viewNormalTodo : Int -> String -> Html Msg 90 | viewNormalTodo index todo = 91 | div [ class "card" ] 92 | [ div [ class "card-block" ] 93 | [ span 94 | [ onDoubleClick (Edit index todo) ] 95 | [ text todo ] 96 | , span 97 | [ onClick (RemoveTodo index) 98 | , class "float-right" 99 | ] 100 | [ text "✖" ] 101 | ] 102 | ] 103 | 104 | 105 | 106 | -- There are some clauses of the case statement that return a command now. 107 | -- The command carries the current todos state, which gets sent to JavaScript 108 | -- via ports. The JavaScript code is subscribed to the saveTodos port, which 109 | -- passes the todos into a callback that saves the todos to localStorage in 110 | -- JavaScript. 111 | 112 | 113 | update : Msg -> Model -> ( Model, Cmd Msg ) 114 | update msg model = 115 | case msg of 116 | UpdateText newText -> 117 | ( { model | text = newText }, Cmd.none ) 118 | 119 | AddTodo -> 120 | let 121 | newTodos = 122 | model.todos ++ [ model.text ] 123 | in 124 | ( { model | text = "", todos = newTodos } 125 | , saveTodos newTodos 126 | ) 127 | 128 | RemoveTodo index -> 129 | let 130 | beforeTodos = 131 | List.take index model.todos 132 | 133 | afterTodos = 134 | List.drop (index + 1) model.todos 135 | 136 | newTodos = 137 | beforeTodos ++ afterTodos 138 | in 139 | ( { model | todos = newTodos }, saveTodos newTodos ) 140 | 141 | Edit index todoText -> 142 | ( { model | editing = Just { index = index, text = todoText } } 143 | , Cmd.none 144 | ) 145 | 146 | EditSave index todoText -> 147 | let 148 | newTodos = 149 | List.indexedMap 150 | (\i todo -> 151 | if i == index then 152 | todoText 153 | 154 | else 155 | todo 156 | ) 157 | model.todos 158 | in 159 | ( { model | editing = Nothing, todos = newTodos } 160 | , saveTodos newTodos 161 | ) 162 | 163 | 164 | 165 | -- This is the port declaration that we're going to use to save our todos 166 | -- to localStorage. 167 | 168 | 169 | port saveTodos : List String -> Cmd msg 170 | 171 | 172 | subscriptions : Model -> Sub Msg 173 | subscriptions model = 174 | Sub.none 175 | 176 | 177 | 178 | -- The init value is now a function that takes in the flags as an argument 179 | -- and returns a tuple containing the initial model and a command. We initially 180 | -- set the todos value to the todos that come from the flag. The todos that 181 | -- came with the flag are the todos that were loaded from localStorage and passed 182 | -- into Elm. 183 | 184 | 185 | init : Flags -> ( Model, Cmd Msg ) 186 | init flags = 187 | ( Model "" flags.todos Nothing 188 | , Cmd.none 189 | ) 190 | 191 | 192 | 193 | -- We're going to use flags to load the todos from localStorage at the start of 194 | -- the web app. Flags are the value passed in from JavaScript in the very 195 | -- beginning. Since we want the todos from localStorage, we're going to make the 196 | -- Flags type be a record that has a todos property, which is a list of strings 197 | -- that represents the todos that were loaded from localStorage and passed into 198 | -- Elm as flags. 199 | 200 | 201 | type alias Flags = 202 | { todos : List String } 203 | 204 | 205 | 206 | -- The type declaration change from (Program () Model Msg) because now we're 207 | -- using flags to get the todos from JavaScript in the beginning of the program. 208 | 209 | 210 | main : Program Flags Model Msg 211 | main = 212 | element 213 | { init = init 214 | , view = view 215 | , update = update 216 | , subscriptions = subscriptions 217 | } 218 | -------------------------------------------------------------------------------- /22-localstorage-editable-todos.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (main) 2 | 3 | import Browser exposing (element) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (autofocus, checked, class, placeholder, style, type_, value) 6 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 7 | 8 | 9 | type Msg 10 | = UpdateText String 11 | | AddTodo 12 | | RemoveTodo Int 13 | | Edit Int String 14 | | EditSave Int String 15 | | ToggleTodo Int 16 | 17 | 18 | type alias TodoEdit = 19 | { index : Int 20 | , text : String 21 | } 22 | 23 | 24 | 25 | -- We changed the todos so that they aren't just strings, they now are records 26 | -- with a completed property which represents whether the user has completed 27 | -- that todo. 28 | 29 | 30 | type alias Todo = 31 | { text : String 32 | , completed : Bool 33 | } 34 | 35 | 36 | 37 | -- The todos property is now List Todo instead of List String. 38 | 39 | 40 | type alias Model = 41 | { text : String 42 | , todos : List Todo 43 | , editing : Maybe TodoEdit 44 | } 45 | 46 | 47 | view : Model -> Html Msg 48 | view model = 49 | div [ class "col-12 col-sm-6 offset-sm-3" ] 50 | [ form [ class "row", onSubmit AddTodo ] 51 | [ div [ class "col-9" ] 52 | [ input 53 | [ onInput UpdateText 54 | , value model.text 55 | , autofocus True 56 | , class "form-control" 57 | , placeholder "Enter a todo" 58 | ] 59 | [] 60 | ] 61 | , div [ class "col-3" ] 62 | [ button 63 | [ class "btn btn-primary form-control" ] 64 | [ text "+" ] 65 | ] 66 | ] 67 | , div [] (List.indexedMap (viewTodo model.editing) model.todos) 68 | ] 69 | 70 | 71 | viewTodo : Maybe TodoEdit -> Int -> Todo -> Html Msg 72 | viewTodo editing index todo = 73 | case editing of 74 | Just todoEdit -> 75 | if todoEdit.index == index then 76 | viewEditTodo index todoEdit 77 | 78 | else 79 | viewNormalTodo index todo 80 | 81 | Nothing -> 82 | viewNormalTodo index todo 83 | 84 | 85 | viewEditTodo : Int -> TodoEdit -> Html Msg 86 | viewEditTodo index todoEdit = 87 | div [ class "card" ] 88 | [ div [ class "card-block" ] 89 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 90 | [ input 91 | [ onInput (Edit index) 92 | , class "form-control" 93 | , value todoEdit.text 94 | ] 95 | [] 96 | ] 97 | ] 98 | ] 99 | 100 | 101 | viewNormalTodo : Int -> Todo -> Html Msg 102 | viewNormalTodo index todo = 103 | div [ class "card" ] 104 | [ div [ class "card-block" ] 105 | -- Added a checkbox that can be clicked to toggle the todo between 106 | -- being completed or incomplete. 107 | [ input 108 | [ onClick (ToggleTodo index) 109 | , type_ "checkbox" 110 | , checked todo.completed 111 | , class "mr-3" 112 | ] 113 | [] 114 | , span 115 | [ onDoubleClick (Edit index todo.text) 116 | 117 | -- Added styling to indicate a todo is completed. 118 | , style 119 | "text-decoration" 120 | (if todo.completed then 121 | "line-through" 122 | 123 | else 124 | "none" 125 | ) 126 | ] 127 | [ text todo.text ] 128 | , span 129 | [ onClick (RemoveTodo index) 130 | , class "float-right" 131 | ] 132 | [ text "✖" ] 133 | ] 134 | ] 135 | 136 | 137 | update : Msg -> Model -> ( Model, Cmd Msg ) 138 | update msg model = 139 | case msg of 140 | UpdateText newText -> 141 | ( { model | text = newText }, Cmd.none ) 142 | 143 | AddTodo -> 144 | let 145 | newTodos = 146 | model.todos ++ [ Todo model.text False ] 147 | in 148 | ( { model | text = "", todos = newTodos } 149 | , saveTodos newTodos 150 | ) 151 | 152 | RemoveTodo index -> 153 | let 154 | beforeTodos = 155 | List.take index model.todos 156 | 157 | afterTodos = 158 | List.drop (index + 1) model.todos 159 | 160 | newTodos = 161 | beforeTodos ++ afterTodos 162 | in 163 | ( { model | todos = newTodos }, saveTodos newTodos ) 164 | 165 | Edit index todoText -> 166 | ( { model | editing = Just { index = index, text = todoText } } 167 | , Cmd.none 168 | ) 169 | 170 | EditSave index todoText -> 171 | let 172 | newTodos = 173 | List.indexedMap 174 | (\i todo -> 175 | if i == index then 176 | { todo | text = todoText } 177 | 178 | else 179 | todo 180 | ) 181 | model.todos 182 | in 183 | ( { model | editing = Nothing, todos = newTodos } 184 | , saveTodos newTodos 185 | ) 186 | 187 | -- Added an extra clause to handle toggling the todo. 188 | ToggleTodo index -> 189 | let 190 | newTodos = 191 | List.indexedMap 192 | (\i todo -> 193 | if i == index then 194 | { todo | completed = not todo.completed } 195 | 196 | else 197 | todo 198 | ) 199 | model.todos 200 | in 201 | ( { model | todos = newTodos }, saveTodos newTodos ) 202 | 203 | 204 | 205 | -- We changed the port type declaration to take a list of Todo records. 206 | 207 | 208 | port saveTodos : List Todo -> Cmd msg 209 | 210 | 211 | subscriptions : Model -> Sub Msg 212 | subscriptions model = 213 | Sub.none 214 | 215 | 216 | init : Flags -> ( Model, Cmd Msg ) 217 | init flags = 218 | ( Model "" flags.todos Nothing 219 | , Cmd.none 220 | ) 221 | 222 | 223 | type alias Flags = 224 | { todos : List Todo } 225 | 226 | 227 | main : Program Flags Model Msg 228 | main = 229 | element 230 | { init = init 231 | , view = view 232 | , update = update 233 | , subscriptions = subscriptions 234 | } 235 | -------------------------------------------------------------------------------- /19-editable-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- We're exposing element now instead of sandbox. 4 | -- This will allow us to do side effects with commands and subscriptions. 5 | -- I'll explain more about commands and subscriptions later, but for now 6 | -- just think of them as the 2 only ways to do side effects in Elm. 7 | -- Having side effects be really controlled like this is really nice for 8 | -- large projects because side effects can only happen in 2 locations, so 9 | -- it makes programs easier to understand. 10 | 11 | import Browser exposing (element) 12 | import Html exposing (..) 13 | import Html.Attributes exposing (autofocus, class, placeholder, value) 14 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 15 | 16 | 17 | type Msg 18 | = UpdateText String 19 | | AddTodo 20 | | RemoveTodo Int 21 | | Edit Int String 22 | | EditSave Int String 23 | 24 | 25 | type alias TodoEdit = 26 | { index : Int 27 | , text : String 28 | } 29 | 30 | 31 | type alias Model = 32 | { text : String 33 | , todos : List String 34 | , editing : Maybe TodoEdit 35 | } 36 | 37 | 38 | view : Model -> Html Msg 39 | view model = 40 | div [ class "col-12 col-sm-6 offset-sm-3" ] 41 | [ form [ class "row", onSubmit AddTodo ] 42 | [ div [ class "col-9" ] 43 | [ input 44 | [ onInput UpdateText 45 | , value model.text 46 | , autofocus True 47 | , class "form-control" 48 | , placeholder "Enter a todo" 49 | ] 50 | [] 51 | ] 52 | , div [ class "col-3" ] 53 | [ button 54 | [ class "btn btn-primary form-control" ] 55 | [ text "+" ] 56 | ] 57 | ] 58 | , div [] (List.indexedMap (viewTodo model.editing) model.todos) 59 | ] 60 | 61 | 62 | viewTodo : Maybe TodoEdit -> Int -> String -> Html Msg 63 | viewTodo editing index todo = 64 | case editing of 65 | Just todoEdit -> 66 | if todoEdit.index == index then 67 | viewEditTodo index todoEdit 68 | 69 | else 70 | viewNormalTodo index todo 71 | 72 | Nothing -> 73 | viewNormalTodo index todo 74 | 75 | 76 | viewEditTodo : Int -> TodoEdit -> Html Msg 77 | viewEditTodo index todoEdit = 78 | div [ class "card" ] 79 | [ div [ class "card-block" ] 80 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 81 | [ input 82 | [ onInput (Edit index) 83 | , class "form-control" 84 | , value todoEdit.text 85 | ] 86 | [] 87 | ] 88 | ] 89 | ] 90 | 91 | 92 | viewNormalTodo : Int -> String -> Html Msg 93 | viewNormalTodo index todo = 94 | div [ class "card" ] 95 | [ div [ class "card-block" ] 96 | [ span 97 | [ onDoubleClick (Edit index todo) ] 98 | [ text todo ] 99 | , span 100 | [ onClick (RemoveTodo index) 101 | , class "float-right" 102 | ] 103 | [ text "✖" ] 104 | ] 105 | ] 106 | 107 | 108 | 109 | -- We're using element now instead of sandbox, so our update function is 110 | -- slightly different. 111 | -- The update function is mostly the same as before but now instead of just 112 | -- returning the model, we now return a tuple containing the new model value 113 | -- and a command which can perform side effects. 114 | -- We don't need to do any side effects, so we've just added Cmd.none as the 115 | -- command for each returning value of the case expression. 116 | -- Since Elm is a pure functional programming language, the only way you can 117 | -- perform side effects is by using commands and subscriptions. You'll see 118 | -- how they work later. Just think of commands as a way of asking for some 119 | -- side effect to happen and think of subscriptions as a way of listening or 120 | -- subscribing to the result of some side effect. 121 | -- Commands get returned from the update function and the resulting values 122 | -- produced from subscriptions get passed as a message to the update function. 123 | 124 | 125 | update : Msg -> Model -> ( Model, Cmd Msg ) 126 | update msg model = 127 | case msg of 128 | UpdateText newText -> 129 | ( { model | text = newText }, Cmd.none ) 130 | 131 | AddTodo -> 132 | ( { model | text = "", todos = model.todos ++ [ model.text ] } 133 | , Cmd.none 134 | ) 135 | 136 | RemoveTodo index -> 137 | let 138 | beforeTodos = 139 | List.take index model.todos 140 | 141 | afterTodos = 142 | List.drop (index + 1) model.todos 143 | 144 | newTodos = 145 | beforeTodos ++ afterTodos 146 | in 147 | ( { model | todos = newTodos }, Cmd.none ) 148 | 149 | Edit index todoText -> 150 | ( { model | editing = Just { index = index, text = todoText } } 151 | , Cmd.none 152 | ) 153 | 154 | EditSave index todoText -> 155 | let 156 | newTodos = 157 | List.indexedMap 158 | (\i todo -> 159 | if i == index then 160 | todoText 161 | 162 | else 163 | todo 164 | ) 165 | model.todos 166 | in 167 | ( { model | editing = Nothing, todos = newTodos } 168 | , Cmd.none 169 | ) 170 | 171 | 172 | 173 | -- We don't need subscriptions, so we're just going to have the subscription 174 | -- function return Sub.none, which indicates we have no subscriptions. 175 | -- I'll explain subscriptions more in the future when we use them, so don't 176 | -- worry about them right now. 177 | 178 | 179 | subscriptions : Model -> Sub Msg 180 | subscriptions model = 181 | Sub.none 182 | 183 | 184 | init : () -> ( Model, Cmd Msg ) 185 | init flags = 186 | ( { text = "" 187 | , todos = [ "Laundry", "Dishes" ] 188 | , editing = Nothing 189 | } 190 | , Cmd.none 191 | ) 192 | 193 | 194 | main : Program () Model Msg 195 | main = 196 | -- We are now using element instead of sandbox, which takes 197 | -- a record with the properties: init, view, update, and subscriptions. 198 | -- The init property is similar to the init property in sandbox 199 | -- except that it takes a function that takes in flags and return a tuple of 200 | -- type ( Model, Cmd Msg ). The Cmd Msg is useful for if you want to 201 | -- perform any side effects in the beginning of the program. You usually 202 | -- don't need to perform any side effects, so you just put the value Cmd.none 203 | -- as the command value whenever you don't need to do any commands. 204 | element 205 | { init = init 206 | , view = view 207 | , update = update 208 | , subscriptions = subscriptions 209 | } 210 | 211 | 212 | 213 | -- How to get to Exercise 20: 214 | -- change the init function to make use of the model constructor. 215 | -- HINT: follow these instructions: 216 | -- The model right now has this as the value: 217 | -- { text = "", todos = [ "Laundry", "Dishes" ], editing = Nothing } 218 | -- We can also represent this value as this: 219 | -- Model "" [ "Laundry", "Dishes" ] Nothing 220 | -- Whenever we make a type alias that's a record, like Model, we 221 | -- can use Model as a constructor function that returns a Model record. 222 | -- Since we defined the Model type alias like this: 223 | -- type alias Model = 224 | -- { text : String 225 | -- , todos : List String 226 | -- , editing : Maybe TodoEdit 227 | -- } 228 | -- (Model "" [ "Laundry", "Dishes" ] Nothing) will make the first 229 | -- argument the text property since that is first in the type alias 230 | -- declaration. The second argument will be the todos property, and 231 | -- the third argument will be the editing property. 232 | -- Go ahead and make that change! 233 | -------------------------------------------------------------------------------- /18-editable-todos.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- I got lazy exposing all of those functions from the Html module, so 4 | -- I decided to use (..), which means that I'm exposing all the functions 5 | -- from the Html module. 6 | 7 | import Browser exposing (sandbox) 8 | import Html exposing (..) 9 | import Html.Attributes exposing (autofocus, class, placeholder, value) 10 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 11 | 12 | 13 | 14 | -- We added 2 new values to the Msg union type, (Edit Int String) and 15 | -- (EditSave Int String). These values will be used to represent editing a todo. 16 | -- The (Edit Int String) value is used for keeping track of the current todo 17 | -- being edited. The Int represents the index of the todo and the String 18 | -- represents the text for that todo. 19 | -- The (EditSave Int String) value will be used to save the new value of the 20 | -- todo. The Int is todo index, the string is the string we want to save that 21 | -- todo as. 22 | 23 | 24 | type Msg 25 | = UpdateText String 26 | | AddTodo 27 | | RemoveTodo Int 28 | | Edit Int String 29 | | EditSave Int String 30 | 31 | 32 | 33 | -- We are creating a new type alias which is just a record that will represent 34 | -- the editing state. The index property is the index of the todo we're editing 35 | -- and the text is that todo's string value. 36 | 37 | 38 | type alias TodoEdit = 39 | { index : Int 40 | , text : String 41 | } 42 | 43 | 44 | 45 | -- We added a new property called editing, which will keep track of the editing 46 | -- state. It's type is Maybe TodoEdit instead of just TodoEdit. We are using a 47 | -- Maybe type which is similar to nullable values in JavaScript. The Maybe 48 | -- equivalent of null is the Nothing value. When a Maybe value is not Nothing, 49 | -- then it is Just something. For example, if I have a Maybe Int type, that means 50 | -- it can be Nothing or it can be Just 1 or Just 2 or Just 3, etc. This will 51 | -- make more sense when you get used to using Maybe types. Don't worry if it 52 | -- doesn't make complete sense yet. Just think of Maybe types as nullable types 53 | -- in JavaScript. 54 | 55 | 56 | type alias Model = 57 | { text : String 58 | , todos : List String 59 | , editing : Maybe TodoEdit 60 | } 61 | 62 | 63 | view : Model -> Html Msg 64 | view model = 65 | div [ class "col-12 col-sm-6 offset-sm-3" ] 66 | [ form [ class "row", onSubmit AddTodo ] 67 | [ div [ class "col-9" ] 68 | [ input 69 | [ onInput UpdateText 70 | , value model.text 71 | , autofocus True 72 | , class "form-control" 73 | , placeholder "Enter a todo" 74 | ] 75 | [] 76 | ] 77 | , div [ class "col-3" ] 78 | [ button 79 | [ class "btn btn-primary form-control" ] 80 | [ text "+" ] 81 | ] 82 | ] 83 | 84 | -- We are using (viewTodo model.editing) as the function, that gets 85 | -- passed into List.indexedMap. All this means is that the first argument 86 | -- to viewTodo is model.editing, the remaining 2 arguments will be 87 | -- passed in by List.indexedMap. 88 | , div [] (List.indexedMap (viewTodo model.editing) model.todos) 89 | ] 90 | 91 | 92 | 93 | -- We use a case expression to check the editing value, which is model.editing. 94 | -- The editing value is a Maybe type, so it can either be Nothing or Just value. 95 | -- If its value is Nothing, that means we aren't editing any todos. 96 | -- If the value is (Just todoEdit), then we need to check to see if the index 97 | -- that we're editing is the current index we're at, if it is, then we need 98 | -- to return the viewEditTodo function with the appropriate arguments passed in. 99 | 100 | 101 | viewTodo : Maybe TodoEdit -> Int -> String -> Html Msg 102 | viewTodo editing index todo = 103 | case editing of 104 | Just todoEdit -> 105 | if todoEdit.index == index then 106 | viewEditTodo index todoEdit 107 | 108 | else 109 | viewNormalTodo index todo 110 | 111 | Nothing -> 112 | viewNormalTodo index todo 113 | 114 | 115 | 116 | -- This is what a todo looks like when it's being edited. You can save edits 117 | -- by hitting enter, which will trigger an onSubmit event, which will pass the 118 | -- (EditSave todoEdit.index todoEdit.text) message to the update function. 119 | -- Every time you change the text in the input box, the onInput event will take 120 | -- the (Edit index) message add the current input box string to the end so 121 | -- that it's like (Edit index editText), then pass that message to the update 122 | -- function. 123 | 124 | 125 | viewEditTodo : Int -> TodoEdit -> Html Msg 126 | viewEditTodo index todoEdit = 127 | div [ class "card" ] 128 | [ div [ class "card-block" ] 129 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 130 | [ input 131 | [ onInput (Edit index) 132 | , class "form-control" 133 | , value todoEdit.text 134 | ] 135 | [] 136 | ] 137 | ] 138 | ] 139 | 140 | 141 | 142 | -- This is what a todo looks like when it's not being edited. If you want to 143 | -- edit a todo, simply double click on the todo text, which will change the state 144 | -- so that you are editing that todo. 145 | 146 | 147 | viewNormalTodo : Int -> String -> Html Msg 148 | viewNormalTodo index todo = 149 | div [ class "card" ] 150 | [ div [ class "card-block" ] 151 | [ span 152 | [ onDoubleClick (Edit index todo) ] 153 | [ text todo ] 154 | , span 155 | [ onClick (RemoveTodo index) 156 | , class "float-right" 157 | ] 158 | [ text "✖" ] 159 | ] 160 | ] 161 | 162 | 163 | update : Msg -> Model -> Model 164 | update msg model = 165 | case msg of 166 | UpdateText newText -> 167 | { model | text = newText } 168 | 169 | AddTodo -> 170 | { model | text = "", todos = model.todos ++ [ model.text ] } 171 | 172 | RemoveTodo index -> 173 | let 174 | beforeTodos = 175 | List.take index model.todos 176 | 177 | afterTodos = 178 | List.drop (index + 1) model.todos 179 | 180 | newTodos = 181 | beforeTodos ++ afterTodos 182 | in 183 | { model | todos = newTodos } 184 | 185 | -- We are just setting the editing property in the model the editing 186 | -- value that we can use to represent the edit. 187 | Edit index todoText -> 188 | { model | editing = Just { index = index, text = todoText } } 189 | 190 | -- We use a let expression to create the new todos. We use List.indexedMap 191 | -- and change the string of the todo at the editing index to the new string. 192 | -- We also set the editing property to Nothing because we aren't editing 193 | -- anymore. 194 | EditSave index todoText -> 195 | let 196 | newTodos = 197 | List.indexedMap 198 | (\i todo -> 199 | if i == index then 200 | todoText 201 | 202 | else 203 | todo 204 | ) 205 | model.todos 206 | in 207 | { model | editing = Nothing, todos = newTodos } 208 | 209 | 210 | 211 | -- I set the todos property of the model to the list 212 | -- [ "Laundry", "Dishes" ] in the beginning. The editing property is set to 213 | -- Nothing in the beginning since we aren't editing anything in the beginning. 214 | 215 | 216 | main : Program () Model Msg 217 | main = 218 | sandbox 219 | { init = 220 | { text = "" 221 | , todos = [ "Laundry", "Dishes" ] 222 | , editing = Nothing 223 | } 224 | , view = view 225 | , update = update 226 | } 227 | 228 | 229 | {- 230 | Exercises to proceed to # 19 231 | 232 | What do we need to do? 233 | Right now we are using the Browser.sandbox method. We now need to change this to use 234 | the Browser.element method: https://package.elm-lang.org/packages/elm/browser/latest/Browser#element 235 | 236 | 237 | -} 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /23-filter-todos.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (main) 2 | 3 | import Browser exposing (element) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (autofocus, checked, class, placeholder, style, type_, value) 6 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 7 | 8 | 9 | 10 | -- Added a SetFilter value to the Msg union type. 11 | 12 | 13 | type Msg 14 | = UpdateText String 15 | | AddTodo 16 | | RemoveTodo Int 17 | | Edit Int String 18 | | EditSave Int String 19 | | ToggleTodo Int 20 | | SetFilter Filter 21 | 22 | 23 | 24 | -- Made a new Filter type to represent all the possible filter values. 25 | 26 | 27 | type Filter 28 | = All 29 | | Incomplete 30 | | Completed 31 | 32 | 33 | type alias TodoEdit = 34 | { index : Int 35 | , text : String 36 | } 37 | 38 | 39 | type alias Todo = 40 | { text : String 41 | , completed : Bool 42 | } 43 | 44 | 45 | 46 | -- Added a filter property to the model state to track the filter value. 47 | 48 | 49 | type alias Model = 50 | { text : String 51 | , todos : List Todo 52 | , editing : Maybe TodoEdit 53 | , filter : Filter 54 | } 55 | 56 | 57 | view : Model -> Html Msg 58 | view model = 59 | div [ class "col-12 col-sm-6 offset-sm-3" ] 60 | [ form [ class "row", onSubmit AddTodo ] 61 | [ div [ class "col-9" ] 62 | [ input 63 | [ onInput UpdateText 64 | , value model.text 65 | , autofocus True 66 | , class "form-control" 67 | , placeholder "Enter a todo" 68 | ] 69 | [] 70 | ] 71 | , div [ class "col-3" ] 72 | [ button 73 | [ class "btn btn-primary form-control" ] 74 | [ text "+" ] 75 | ] 76 | ] 77 | , viewFilters model.filter 78 | 79 | -- This <| operator means to evaluate everything to the right of the <| 80 | -- before passing it to the expression to the left. So the List.indexedMap 81 | -- expression will be evaluated first, then the resulting list will get 82 | -- passed to (div []). 83 | , div [] <| 84 | List.indexedMap 85 | (viewTodo model.editing) 86 | -- We now filter the todos based on the current filter 87 | (filterTodos model.filter model.todos) 88 | ] 89 | 90 | 91 | 92 | -- Returns the todos that should be displayed based on what the filter is. 93 | 94 | 95 | filterTodos : Filter -> List Todo -> List Todo 96 | filterTodos filter todos = 97 | case filter of 98 | All -> 99 | todos 100 | 101 | Incomplete -> 102 | List.filter (\t -> not t.completed) todos 103 | 104 | Completed -> 105 | List.filter (\t -> t.completed) todos 106 | 107 | 108 | 109 | -- Added the HTML representation of the filters. 110 | 111 | 112 | viewFilters : Filter -> Html Msg 113 | viewFilters filter = 114 | div [] 115 | [ viewFilter All (filter == All) "All" 116 | , viewFilter Incomplete (filter == Incomplete) "Incomplete" 117 | , viewFilter Completed (filter == Completed) "Completed" 118 | ] 119 | 120 | 121 | 122 | -- Here's how each filter is displayed. 123 | 124 | 125 | viewFilter : Filter -> Bool -> String -> Html Msg 126 | viewFilter filter isFilter filterText = 127 | if isFilter then 128 | span [ class "mr-3" ] [ text filterText ] 129 | 130 | else 131 | span 132 | [ class "text-primary mr-3" 133 | 134 | -- When you click on a filter, it will get set as the current filter. 135 | , onClick (SetFilter filter) 136 | , style "cursor" "pointer" 137 | ] 138 | [ text filterText ] 139 | 140 | 141 | viewTodo : Maybe TodoEdit -> Int -> Todo -> Html Msg 142 | viewTodo editing index todo = 143 | case editing of 144 | Just todoEdit -> 145 | if todoEdit.index == index then 146 | viewEditTodo index todoEdit 147 | 148 | else 149 | viewNormalTodo index todo 150 | 151 | Nothing -> 152 | viewNormalTodo index todo 153 | 154 | 155 | viewEditTodo : Int -> TodoEdit -> Html Msg 156 | viewEditTodo index todoEdit = 157 | div [ class "card" ] 158 | [ div [ class "card-block" ] 159 | [ form [ onSubmit (EditSave todoEdit.index todoEdit.text) ] 160 | [ input 161 | [ onInput (Edit index) 162 | , class "form-control" 163 | , value todoEdit.text 164 | ] 165 | [] 166 | ] 167 | ] 168 | ] 169 | 170 | 171 | viewNormalTodo : Int -> Todo -> Html Msg 172 | viewNormalTodo index todo = 173 | div [ class "card" ] 174 | [ div [ class "card-block" ] 175 | [ input 176 | [ onClick (ToggleTodo index) 177 | , type_ "checkbox" 178 | , checked todo.completed 179 | , class "mr-3" 180 | ] 181 | [] 182 | , span 183 | [ onDoubleClick (Edit index todo.text) 184 | , style 185 | "text-decoration" 186 | (if todo.completed then 187 | "line-through" 188 | 189 | else 190 | "none" 191 | ) 192 | ] 193 | [ text todo.text ] 194 | , span 195 | [ onClick (RemoveTodo index) 196 | , class "float-right" 197 | ] 198 | [ text "✖" ] 199 | ] 200 | ] 201 | 202 | 203 | update : Msg -> Model -> ( Model, Cmd Msg ) 204 | update msg model = 205 | case msg of 206 | UpdateText newText -> 207 | ( { model | text = newText }, Cmd.none ) 208 | 209 | AddTodo -> 210 | let 211 | newTodos = 212 | model.todos ++ [ Todo model.text False ] 213 | in 214 | ( { model | text = "", todos = newTodos } 215 | , saveTodos newTodos 216 | ) 217 | 218 | RemoveTodo index -> 219 | let 220 | beforeTodos = 221 | List.take index model.todos 222 | 223 | afterTodos = 224 | List.drop (index + 1) model.todos 225 | 226 | newTodos = 227 | beforeTodos ++ afterTodos 228 | in 229 | ( { model | todos = newTodos }, saveTodos newTodos ) 230 | 231 | Edit index todoText -> 232 | ( { model | editing = Just { index = index, text = todoText } } 233 | , Cmd.none 234 | ) 235 | 236 | EditSave index todoText -> 237 | let 238 | newTodos = 239 | List.indexedMap 240 | (\i todo -> 241 | if i == index then 242 | { todo | text = todoText } 243 | 244 | else 245 | todo 246 | ) 247 | model.todos 248 | in 249 | ( { model | editing = Nothing, todos = newTodos } 250 | , saveTodos newTodos 251 | ) 252 | 253 | ToggleTodo index -> 254 | let 255 | newTodos = 256 | List.indexedMap 257 | (\i todo -> 258 | if i == index then 259 | { todo | completed = not todo.completed } 260 | 261 | else 262 | todo 263 | ) 264 | model.todos 265 | in 266 | ( { model | todos = newTodos }, saveTodos newTodos ) 267 | 268 | -- We added this clause to set the filter to its new value when 269 | -- the message value is (SetFilter Filter). 270 | SetFilter filter -> 271 | ( { model | filter = filter }, Cmd.none ) 272 | 273 | 274 | port saveTodos : List Todo -> Cmd msg 275 | 276 | 277 | subscriptions : Model -> Sub Msg 278 | subscriptions model = 279 | Sub.none 280 | 281 | 282 | init : Flags -> ( Model, Cmd Msg ) 283 | init flags = 284 | ( Model "" flags.todos Nothing All 285 | , Cmd.none 286 | ) 287 | 288 | 289 | type alias Flags = 290 | { todos : List Todo } 291 | 292 | 293 | main : Program Flags Model Msg 294 | main = 295 | element 296 | { init = init 297 | , view = view 298 | , update = update 299 | , subscriptions = subscriptions 300 | } 301 | -------------------------------------------------------------------------------- /24-filter-todos.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (main) 2 | 3 | -- Importing the Random module because we're going to use it to generate 4 | -- a random id so that we can uniquely identify each todo. 5 | 6 | import Browser exposing (element) 7 | import Html exposing (..) 8 | import Html.Attributes exposing (autofocus, checked, class, placeholder, style, type_, value) 9 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 10 | import Random 11 | 12 | 13 | 14 | -- Added GenerateTodoId message, which we're going to use to generate a 15 | -- random id for a newly created todo. 16 | -- AddTodo was changed to (AddTodo Int) because it now takes the integer 17 | -- id that was randomly generated and uses that to add the new todo. 18 | 19 | 20 | type Msg 21 | = UpdateText String 22 | | GenerateTodoId 23 | | AddTodo Int 24 | | RemoveTodo Int 25 | | Edit Int String 26 | | EditSave Int String 27 | | ToggleTodo Int 28 | | SetFilter Filter 29 | 30 | 31 | type Filter 32 | = All 33 | | Incomplete 34 | | Completed 35 | 36 | 37 | 38 | -- Before TodoEdit had an index and a text field, now we're going to be 39 | -- using the todo's id to identify which todo we're editing, so I 40 | -- switched the field name from index to id. 41 | 42 | 43 | type alias TodoEdit = 44 | { id : Int 45 | , text : String 46 | } 47 | 48 | 49 | 50 | -- Added an id field, which will hold each todo's randomly generated id. 51 | 52 | 53 | type alias Todo = 54 | { id : Int 55 | , text : String 56 | , completed : Bool 57 | } 58 | 59 | 60 | type alias Model = 61 | { text : String 62 | , todos : List Todo 63 | , editing : Maybe TodoEdit 64 | , filter : Filter 65 | } 66 | 67 | 68 | view : Model -> Html Msg 69 | view model = 70 | div [ class "col-12 col-sm-6 offset-sm-3" ] 71 | -- GenerateTodoId is now the message that gets passed into 72 | -- the update function as the message when the form submits. 73 | [ form [ class "row", onSubmit GenerateTodoId ] 74 | [ div [ class "col-9" ] 75 | [ input 76 | [ onInput UpdateText 77 | , value model.text 78 | , autofocus True 79 | , class "form-control" 80 | , placeholder "Enter a todo" 81 | ] 82 | [] 83 | ] 84 | , div [ class "col-3" ] 85 | [ button 86 | [ class "btn btn-primary form-control" ] 87 | [ text "+" ] 88 | ] 89 | ] 90 | , viewFilters model.filter 91 | , div [] <| 92 | -- We use List.map now instead of List.indexedMap, since we 93 | -- are using the id and not the index to edit and remove todos. 94 | List.map 95 | (viewTodo model.editing) 96 | (filterTodos model.filter model.todos) 97 | ] 98 | 99 | 100 | filterTodos : Filter -> List Todo -> List Todo 101 | filterTodos filter todos = 102 | case filter of 103 | All -> 104 | todos 105 | 106 | Incomplete -> 107 | List.filter (\t -> not t.completed) todos 108 | 109 | Completed -> 110 | List.filter (\t -> t.completed) todos 111 | 112 | 113 | viewFilters : Filter -> Html Msg 114 | viewFilters filter = 115 | div [] 116 | [ viewFilter All (filter == All) "All" 117 | , viewFilter Incomplete (filter == Incomplete) "Incomplete" 118 | , viewFilter Completed (filter == Completed) "Completed" 119 | ] 120 | 121 | 122 | viewFilter : Filter -> Bool -> String -> Html Msg 123 | viewFilter filter isFilter filterText = 124 | if isFilter then 125 | span [ class "mr-3" ] [ text filterText ] 126 | 127 | else 128 | span 129 | [ class "text-primary mr-3" 130 | , onClick (SetFilter filter) 131 | , style "cursor" "pointer" 132 | ] 133 | [ text filterText ] 134 | 135 | 136 | 137 | -- viewTodo now has one less argument because it doesn't need 138 | -- to have the index passed in anymore since it uses the id 139 | -- for editing and removing each todo. It also doesn't pass 140 | -- the index into viewEditTodo and viewNormalTodo anymore. 141 | 142 | 143 | viewTodo : Maybe TodoEdit -> Todo -> Html Msg 144 | viewTodo editing todo = 145 | case editing of 146 | Just todoEdit -> 147 | if todoEdit.id == todo.id then 148 | viewEditTodo todoEdit 149 | 150 | else 151 | viewNormalTodo todo 152 | 153 | Nothing -> 154 | viewNormalTodo todo 155 | 156 | 157 | 158 | -- viewEditTodo has one less argument because it doesn't need the index 159 | -- for editing, it uses the todo's id to identify the todo that is being edited. 160 | -- It passes in todoEdit.id into Edit and EditSave now instead of the index. 161 | 162 | 163 | viewEditTodo : TodoEdit -> Html Msg 164 | viewEditTodo todoEdit = 165 | div [ class "card" ] 166 | [ div [ class "card-block" ] 167 | [ form [ onSubmit (EditSave todoEdit.id todoEdit.text) ] 168 | [ input 169 | [ onInput (Edit todoEdit.id) 170 | , class "form-control" 171 | , value todoEdit.text 172 | ] 173 | [] 174 | ] 175 | ] 176 | ] 177 | 178 | 179 | 180 | -- viewNormalTodo has one less argument because it doesn't need the index. 181 | -- It now passes todo.id into ToggleTodo, Edit, and RemoveTodo instead of the index. 182 | 183 | 184 | viewNormalTodo : Todo -> Html Msg 185 | viewNormalTodo todo = 186 | div [ class "card" ] 187 | [ div [ class "card-block" ] 188 | [ input 189 | [ onClick (ToggleTodo todo.id) 190 | , type_ "checkbox" 191 | , checked todo.completed 192 | , class "mr-3" 193 | ] 194 | [] 195 | , span 196 | [ onDoubleClick (Edit todo.id todo.text) 197 | , style 198 | "text-decoration" 199 | (if todo.completed then 200 | "line-through" 201 | 202 | else 203 | "none" 204 | ) 205 | ] 206 | [ text todo.text ] 207 | , span 208 | [ onClick (RemoveTodo todo.id) 209 | , class "float-right" 210 | ] 211 | [ text "✖" ] 212 | ] 213 | ] 214 | 215 | 216 | update : Msg -> Model -> ( Model, Cmd Msg ) 217 | update msg model = 218 | case msg of 219 | UpdateText newText -> 220 | ( { model | text = newText }, Cmd.none ) 221 | 222 | -- When the form to add a new todo submits, it now passes in 223 | -- GenerateTodoId, which will generate a random integer id, 224 | -- then the result will be passed into AddTodo which now accepts 225 | -- an integer. 226 | -- I'm going to explain each part of this: 227 | -- Random.generate AddTodo (Random.int Random.minInt Random.maxInt) 228 | -- Random.int is a function that takes 2 Int arguments and returns 229 | -- a (Generate Int) type. Random.minInt is the minimum integer value 230 | -- that a 32-bit integer can be. Random.maxInt is the maximum integer 231 | -- value that a 32-bit integer can be. 232 | -- So (Random.int Random.minInt Random.maxInt) is a Generator Int type. 233 | -- Random.generate is a function that takes a function that accepts an 234 | -- Int and returns a Msg as the first argument and a Generator Int as 235 | -- the second argument, then it will randomly generate an integer 236 | -- between Random.minInt and Random.maxInt, then it will pass that 237 | -- integer to AddTodo, which will get passed into the update function. 238 | GenerateTodoId -> 239 | ( model 240 | , Random.generate AddTodo (Random.int Random.minInt Random.maxInt) 241 | ) 242 | 243 | -- So now after the random id is generated, it gets passed into AddTodo 244 | -- which gets passed into the update function. We take the id and current 245 | -- model.text value to create the new todo and then we append the new 246 | -- todo to the end of model.todos. 247 | AddTodo todoId -> 248 | let 249 | newTodos = 250 | model.todos ++ [ Todo todoId model.text False ] 251 | in 252 | ( { model | text = "", todos = newTodos } 253 | , saveTodos newTodos 254 | ) 255 | 256 | -- Since we get passed the id, we can just use List.filter to 257 | -- keep all the todos that don't have the same id as the one 258 | -- we are removing. 259 | RemoveTodo todoId -> 260 | let 261 | newTodos = 262 | List.filter (\todo -> todo.id /= todoId) model.todos 263 | in 264 | ( { model | todos = newTodos }, saveTodos newTodos ) 265 | 266 | -- We use id to track the edited todo instead of the index. 267 | Edit todoId todoText -> 268 | ( { model | editing = Just { id = todoId, text = todoText } } 269 | , Cmd.none 270 | ) 271 | 272 | -- We are now saving the todo, so if the todo's id is the id that we 273 | -- were editing, then we change the text of that todo to the edit text. 274 | -- If its id isn't the same as the edit todo's id, we keep it the same 275 | -- because it wasn't the todo that we were editing. 276 | EditSave todoId todoText -> 277 | let 278 | newTodos = 279 | List.map 280 | (\todo -> 281 | if todo.id == todoId then 282 | { todo | text = todoText } 283 | 284 | else 285 | todo 286 | ) 287 | model.todos 288 | in 289 | ( { model | editing = Nothing, todos = newTodos } 290 | , saveTodos newTodos 291 | ) 292 | 293 | -- We map over the todos and change the completed field of 294 | -- the todo that has the id that we chose to toggle. 295 | ToggleTodo todoId -> 296 | let 297 | newTodos = 298 | List.map 299 | (\todo -> 300 | if todo.id == todoId then 301 | { todo | completed = not todo.completed } 302 | 303 | else 304 | todo 305 | ) 306 | model.todos 307 | in 308 | ( { model | todos = newTodos }, saveTodos newTodos ) 309 | 310 | SetFilter filter -> 311 | ( { model | filter = filter }, Cmd.none ) 312 | 313 | 314 | port saveTodos : List Todo -> Cmd msg 315 | 316 | 317 | subscriptions : Model -> Sub Msg 318 | subscriptions model = 319 | Sub.none 320 | 321 | 322 | init : Flags -> ( Model, Cmd Msg ) 323 | init flags = 324 | ( Model "" flags.todos Nothing All 325 | , Cmd.none 326 | ) 327 | 328 | 329 | type alias Flags = 330 | { todos : List Todo } 331 | 332 | 333 | main : Program Flags Model Msg 334 | main = 335 | element 336 | { init = init 337 | , view = view 338 | , update = update 339 | , subscriptions = subscriptions 340 | } 341 | -------------------------------------------------------------------------------- /25-navigation-todos.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (main) 2 | 3 | -- We need some additional imports from Browser and Browser.Navigation packages. 4 | -- We're going to use functionality from them to keep track of the URL and display 5 | -- the appropriate todos for that URL. 6 | -- For example, "/#incomplete" will show the incomplete todos and 7 | -- "/#completed" will show the completed todos. All other URLs will 8 | -- show all of the todos. 9 | 10 | import Browser exposing (Document, UrlRequest(..), application) 11 | import Browser.Navigation exposing (Key) 12 | import Html exposing (..) 13 | import Html.Attributes exposing (autofocus, checked, class, href, placeholder, style, type_, value) 14 | import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit) 15 | import Random 16 | import Url exposing (Url) 17 | 18 | 19 | 20 | -- We add two new messages: 21 | -- LinkClicked and ChangeUrl which are sent to our application by 22 | -- Browser.application every time when user clicks a link on a page or changes 23 | -- the URL in browser's navigation bar. 24 | 25 | 26 | type Msg 27 | = UpdateText String 28 | | GenerateTodoId 29 | | AddTodo Int 30 | | RemoveTodo Int 31 | | Edit Int String 32 | | EditSave Int String 33 | | ToggleTodo Int 34 | | SetFilter Filter 35 | | LinkClicked UrlRequest 36 | | ChangeUrl Url 37 | 38 | 39 | type Filter 40 | = All 41 | | Incomplete 42 | | Completed 43 | 44 | 45 | type alias TodoEdit = 46 | { id : Int 47 | , text : String 48 | } 49 | 50 | 51 | type alias Todo = 52 | { id : Int 53 | , text : String 54 | , completed : Bool 55 | } 56 | 57 | 58 | type alias Model = 59 | { text : String 60 | , todos : List Todo 61 | , editing : Maybe TodoEdit 62 | , filter : Filter 63 | 64 | -- We need to store the Browser.Navigation.Key in our model to be able to 65 | -- call browser navigation functions from Browser.Navigation module. 66 | -- The key gets passed to us on application initialization, where we store 67 | -- it in our model. 68 | -- Each function from Browser.Navigation module which manipulates browser 69 | -- URL requires us to pass in this Key as a parameter. 70 | -- This was a design decision made by the core developers 71 | -- or Elm to prevent people from calling browser navigation functions outside 72 | -- Browser.application (e.g. from less featureful applications like those 73 | -- created by Browser.element), which could lead to a lot of insidious bugs. 74 | , navigationKey : Key 75 | } 76 | 77 | 78 | 79 | -- view function for the Browser.application requires us to return a value of 80 | -- type `Document Msg`. This is just a record containing 81 | -- 1. title String, which is used as a page title in the browser toolbar 82 | -- 2. body, which is a list of Html elements that will be children of the page's 83 | -- body element. 84 | 85 | 86 | view : Model -> Document Msg 87 | view model = 88 | { title = "Navigation TODOs" 89 | , body = [ viewBody model ] 90 | } 91 | 92 | 93 | viewBody : Model -> Html Msg 94 | viewBody model = 95 | div [ class "col-12 col-sm-6 offset-sm-3" ] 96 | [ form [ class "row", onSubmit GenerateTodoId ] 97 | [ div [ class "col-9" ] 98 | [ input 99 | [ onInput UpdateText 100 | , value model.text 101 | , autofocus True 102 | , class "form-control" 103 | , placeholder "Enter a todo" 104 | ] 105 | [] 106 | ] 107 | , div [ class "col-3" ] 108 | [ button 109 | [ class "btn btn-primary form-control" ] 110 | [ text "+" ] 111 | ] 112 | ] 113 | , viewFilters model.filter 114 | , div [] <| 115 | List.map 116 | (viewTodo model.editing) 117 | (filterTodos model.filter model.todos) 118 | ] 119 | 120 | 121 | filterTodos : Filter -> List Todo -> List Todo 122 | filterTodos filter todos = 123 | case filter of 124 | All -> 125 | todos 126 | 127 | Incomplete -> 128 | List.filter (\t -> not t.completed) todos 129 | 130 | Completed -> 131 | List.filter (\t -> t.completed) todos 132 | 133 | 134 | viewFilters : Filter -> Html Msg 135 | viewFilters filter = 136 | div [] 137 | [ viewFilter All (filter == All) "All" 138 | , viewFilter Incomplete (filter == Incomplete) "Incomplete" 139 | , viewFilter Completed (filter == Completed) "Completed" 140 | ] 141 | 142 | 143 | viewFilter : Filter -> Bool -> String -> Html Msg 144 | viewFilter filter isFilter filterText = 145 | if isFilter then 146 | span [ class "mr-3" ] [ text filterText ] 147 | 148 | else 149 | a 150 | [ class "text-primary mr-3" 151 | 152 | -- Whenever the user clicks on a filter link, the 153 | -- hash in the URL changes to the filterText. 154 | -- So if you refresh the page and your URL is 155 | -- "/#completed", the completed todos will be visible. 156 | , href ("#" ++ String.toLower filterText) 157 | , onClick (SetFilter filter) 158 | , style "cursor" "pointer" 159 | ] 160 | [ text filterText ] 161 | 162 | 163 | viewTodo : Maybe TodoEdit -> Todo -> Html Msg 164 | viewTodo editing todo = 165 | case editing of 166 | Just todoEdit -> 167 | if todoEdit.id == todo.id then 168 | viewEditTodo todoEdit 169 | 170 | else 171 | viewNormalTodo todo 172 | 173 | Nothing -> 174 | viewNormalTodo todo 175 | 176 | 177 | viewEditTodo : TodoEdit -> Html Msg 178 | viewEditTodo todoEdit = 179 | div [ class "card" ] 180 | [ div [ class "card-block" ] 181 | [ form [ onSubmit (EditSave todoEdit.id todoEdit.text) ] 182 | [ input 183 | [ onInput (Edit todoEdit.id) 184 | , class "form-control" 185 | , value todoEdit.text 186 | ] 187 | [] 188 | ] 189 | ] 190 | ] 191 | 192 | 193 | viewNormalTodo : Todo -> Html Msg 194 | viewNormalTodo todo = 195 | div [ class "card" ] 196 | [ div [ class "card-block" ] 197 | [ input 198 | [ onClick (ToggleTodo todo.id) 199 | , type_ "checkbox" 200 | , checked todo.completed 201 | , class "mr-3" 202 | ] 203 | [] 204 | , span 205 | [ onDoubleClick (Edit todo.id todo.text) 206 | , style "text-decoration" 207 | (if todo.completed then 208 | "line-through" 209 | 210 | else 211 | "none" 212 | ) 213 | ] 214 | [ text todo.text ] 215 | , span 216 | [ onClick (RemoveTodo todo.id) 217 | , class "float-right" 218 | ] 219 | [ text "✖" ] 220 | ] 221 | ] 222 | 223 | 224 | update : Msg -> Model -> ( Model, Cmd Msg ) 225 | update msg model = 226 | case msg of 227 | UpdateText newText -> 228 | ( { model | text = newText }, Cmd.none ) 229 | 230 | GenerateTodoId -> 231 | ( model 232 | , Random.generate AddTodo (Random.int Random.minInt Random.maxInt) 233 | ) 234 | 235 | AddTodo todoId -> 236 | let 237 | newTodos = 238 | model.todos ++ [ Todo todoId model.text False ] 239 | in 240 | ( { model | text = "", todos = newTodos } 241 | , saveTodos newTodos 242 | ) 243 | 244 | RemoveTodo todoId -> 245 | let 246 | newTodos = 247 | List.filter (\todo -> todo.id /= todoId) model.todos 248 | in 249 | ( { model | todos = newTodos }, saveTodos newTodos ) 250 | 251 | Edit todoId todoText -> 252 | ( { model | editing = Just { id = todoId, text = todoText } } 253 | , Cmd.none 254 | ) 255 | 256 | EditSave todoId todoText -> 257 | let 258 | newTodos = 259 | List.map 260 | (\todo -> 261 | if todo.id == todoId then 262 | { todo | text = todoText } 263 | 264 | else 265 | todo 266 | ) 267 | model.todos 268 | in 269 | ( { model | editing = Nothing, todos = newTodos } 270 | , saveTodos newTodos 271 | ) 272 | 273 | ToggleTodo todoId -> 274 | let 275 | newTodos = 276 | List.map 277 | (\todo -> 278 | if todo.id == todoId then 279 | { todo | completed = not todo.completed } 280 | 281 | else 282 | todo 283 | ) 284 | model.todos 285 | in 286 | ( { model | todos = newTodos }, saveTodos newTodos ) 287 | 288 | SetFilter filter -> 289 | ( { model | filter = filter }, Cmd.none ) 290 | 291 | -- Whenever user clicks a link on the page, the Elm runtime generates 292 | -- this message for us and gives us the possibility to react. 293 | -- See the docs in the elm/browser package to understand the difference 294 | -- between Internal and External URL: 295 | -- https://package.elm-lang.org/packages/elm/browser/latest/Browser#UrlRequest 296 | LinkClicked urlRequest -> 297 | case urlRequest of 298 | Browser.Internal url -> 299 | ( model 300 | -- When user clicks a link in our app (like "Complete") 301 | -- we ask the browser to make the new url part of browsing history 302 | -- WITHOUT reloading the page (i.e. without triggering http 303 | -- request to load a new page from the server). 304 | , Browser.Navigation.pushUrl model.navigationKey (Url.toString url) 305 | ) 306 | 307 | Browser.External url -> 308 | ( model, Browser.Navigation.load url ) 309 | 310 | -- Whenever the URL changes, the current URL gets passed to 311 | -- ChangeUrl, which gets passed into the update function. 312 | -- We pass the url into urlToFilter, which takes the 313 | -- current location and returns the current filter. 314 | ChangeUrl url -> 315 | ( { model | filter = urlToFilter url }, Cmd.none ) 316 | 317 | 318 | 319 | -- We only care about url.fragment for determining which filter is set. 320 | -- If the fragment is "incomplete", we want our filter to be Incomplete, so 321 | -- that the todos that are incomplete are shown. 322 | -- We want "complete" to show the completed todos. 323 | -- The clause _ -> catches all other strings, which means that all other 324 | -- URL hashes will show all of the todos. 325 | 326 | 327 | urlToFilter : Url -> Filter 328 | urlToFilter url = 329 | case url.fragment of 330 | Nothing -> 331 | All 332 | 333 | Just hash -> 334 | case String.toLower hash of 335 | "incomplete" -> 336 | Incomplete 337 | 338 | "completed" -> 339 | Completed 340 | 341 | _ -> 342 | All 343 | 344 | 345 | port saveTodos : List Todo -> Cmd msg 346 | 347 | 348 | subscriptions : Model -> Sub Msg 349 | subscriptions model = 350 | Sub.none 351 | 352 | 353 | 354 | -- We're now using Browser.application, whose init function requires two additional 355 | -- arguments: 356 | -- 1. Url - This will be passed to us by Elm runtime when the application is 357 | -- initialized. 358 | -- In our app we're using the URL to parse the currently active filter from 359 | -- the URL's fragment (the part of URL following the '#' character). 360 | -- We use (urlToFilter url) which returns the filter that we will use for that URL. 361 | -- So if the page's URL is initially "/#completed", then (urlToFilter url) will 362 | -- return Completed as the filter, so the filter value will be Completed, 363 | -- which will lead to the completed todos to be shown. 364 | -- 2. Key is the navigation key that we need to save into our model. We need it 365 | -- to be able to control the browser's page loading functionality 366 | -- (see LinkClicked branch of the update function for how the Key is used). 367 | 368 | 369 | init : Flags -> Url -> Key -> ( Model, Cmd Msg ) 370 | init flags url navigationKey = 371 | ( { text = "" 372 | , todos = flags.todos 373 | , editing = Nothing 374 | , filter = urlToFilter url 375 | , navigationKey = navigationKey 376 | } 377 | , Cmd.none 378 | ) 379 | 380 | 381 | type alias Flags = 382 | { todos : List Todo } 383 | 384 | 385 | 386 | {- 387 | We're using Browser.application instead of Browser.element, which extends the 388 | latter in 3 important ways: 389 | 390 | 1. init gets two additional parameters: 391 | - the current Url from the browsers navigation bar. 392 | This allows you to show different things depending on the Url. 393 | - the navigation Key, which you can save in your model and use it later to be able 394 | add items to manipulate browser's navigation history 395 | 396 | 2. When someone clicks a link, like Home, it is intercepted 397 | as a UrlRequest. So instead of loading new HTML, onUrlRequest creates a message 398 | for your update where you can decide exactly what to do next. 399 | 400 | 3. When the URL changes, the new Url is sent to onUrlChange. 401 | The resulting message goes to update where you can decide how to show the new page. 402 | -} 403 | 404 | 405 | main : Program Flags Model Msg 406 | main = 407 | application 408 | { init = init 409 | , view = view 410 | , update = update 411 | , subscriptions = subscriptions 412 | , onUrlRequest = LinkClicked 413 | , onUrlChange = ChangeUrl 414 | } 415 | --------------------------------------------------------------------------------