├── .gitignore ├── examples ├── 1 │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ └── Counter.elm ├── 2 │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ ├── Counter.elm │ └── CounterPair.elm ├── 3 │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ ├── Counter.elm │ └── CounterList.elm ├── 4 │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ ├── Counter.elm │ └── CounterList.elm ├── 5 │ ├── assets │ │ └── waiting.gif │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ └── RandomGif.elm ├── 6 │ ├── assets │ │ └── waiting.gif │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ ├── RandomGifPair.elm │ └── RandomGif.elm ├── 7 │ ├── assets │ │ └── waiting.gif │ ├── Main.elm │ ├── elm-package.json │ ├── README.md │ ├── RandomGif.elm │ └── RandomGifList.elm └── 8 │ ├── Main.elm │ ├── README.md │ ├── elm-package.json │ ├── SpinSquarePair.elm │ └── SpinSquare.elm ├── diagrams └── signal-graph-summary.png ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js -------------------------------------------------------------------------------- /examples/5/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sandbox/elm-architecture-tutorial/master/examples/5/assets/waiting.gif -------------------------------------------------------------------------------- /examples/6/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sandbox/elm-architecture-tutorial/master/examples/6/assets/waiting.gif -------------------------------------------------------------------------------- /examples/7/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sandbox/elm-architecture-tutorial/master/examples/7/assets/waiting.gif -------------------------------------------------------------------------------- /diagrams/signal-graph-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sandbox/elm-architecture-tutorial/master/diagrams/signal-graph-summary.png -------------------------------------------------------------------------------- /examples/1/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import Counter exposing (update, view) 3 | import StartApp.Simple exposing (start) 4 | 5 | 6 | main = 7 | start 8 | { model = 0 9 | , update = update 10 | , view = view 11 | } 12 | -------------------------------------------------------------------------------- /examples/3/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import CounterList exposing (init, update, view) 3 | import StartApp.Simple exposing (start) 4 | 5 | 6 | main = 7 | start 8 | { model = init 9 | , update = update 10 | , view = view 11 | } 12 | -------------------------------------------------------------------------------- /examples/4/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import CounterList exposing (init, update, view) 3 | import StartApp.Simple exposing (start) 4 | 5 | 6 | main = 7 | start 8 | { model = init 9 | , update = update 10 | , view = view 11 | } 12 | -------------------------------------------------------------------------------- /examples/2/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import CounterPair exposing (init, update, view) 3 | import StartApp.Simple exposing (start) 4 | 5 | 6 | main = 7 | start 8 | { model = init 0 0 50 9 | , update = update 10 | , view = view 11 | } 12 | -------------------------------------------------------------------------------- /examples/7/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import Effects exposing (Never) 3 | import RandomGifList exposing (init, update, view) 4 | import StartApp 5 | import Task 6 | 7 | 8 | app = 9 | StartApp.start 10 | { init = init 11 | , update = update 12 | , view = view 13 | , inputs = [] 14 | } 15 | 16 | 17 | main = 18 | app.html 19 | 20 | 21 | port tasks : Signal (Task.Task Never ()) 22 | port tasks = 23 | app.tasks 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/8/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import Effects exposing (Never) 3 | import SpinSquarePair exposing (init, update, view) 4 | import StartApp 5 | import Task 6 | 7 | 8 | app = 9 | StartApp.start 10 | { init = init 11 | , update = update 12 | , view = view 13 | , inputs = [] 14 | } 15 | 16 | 17 | main = 18 | app.html 19 | 20 | 21 | port tasks : Signal (Task.Task Never ()) 22 | port tasks = 23 | app.tasks 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/5/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import Effects exposing (Never) 3 | import RandomGif exposing (init, update, view) 4 | import StartApp 5 | import Task 6 | 7 | 8 | app = 9 | StartApp.start 10 | { init = init "funny cats" 11 | , update = update 12 | , view = view 13 | , inputs = [] 14 | } 15 | 16 | 17 | main = 18 | app.html 19 | 20 | 21 | port tasks : Signal (Task.Task Never ()) 22 | port tasks = 23 | app.tasks 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/6/Main.elm: -------------------------------------------------------------------------------- 1 | 2 | import Effects exposing (Never) 3 | import RandomGifPair exposing (init, update, view) 4 | import StartApp 5 | import Task 6 | 7 | 8 | app = 9 | StartApp.start 10 | { init = init "funny cats" "funny dogs" 11 | , update = update 12 | , view = view 13 | , inputs = [] 14 | } 15 | 16 | 17 | main = 18 | app.html 19 | 20 | 21 | port tasks : Signal (Task.Task Never ()) 22 | port tasks = 23 | app.tasks 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/1/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.0.0 <= v < 3.0.0", 12 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 13 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 14 | }, 15 | "elm-version": "0.15.0 <= v < 0.16.0" 16 | } -------------------------------------------------------------------------------- /examples/2/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.0.0 <= v < 3.0.0", 12 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 13 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 14 | }, 15 | "elm-version": "0.15.0 <= v < 0.16.0" 16 | } -------------------------------------------------------------------------------- /examples/3/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.0.0 <= v < 3.0.0", 12 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 13 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 14 | }, 15 | "elm-version": "0.15.0 <= v < 0.16.0" 16 | } -------------------------------------------------------------------------------- /examples/4/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.0.0 <= v < 3.0.0", 12 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 13 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 14 | }, 15 | "elm-version": "0.15.0 <= v < 0.16.0" 16 | } -------------------------------------------------------------------------------- /examples/5/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.1.0 <= v < 3.0.0", 12 | "evancz/elm-effects": "2.0.0 <= v < 3.0.0", 13 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 14 | "evancz/elm-http": "1.0.0 <= v < 2.0.0", 15 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.15.1 <= v < 0.16.0" 18 | } -------------------------------------------------------------------------------- /examples/6/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.1.0 <= v < 3.0.0", 12 | "evancz/elm-effects": "2.0.0 <= v < 3.0.0", 13 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 14 | "evancz/elm-http": "1.0.0 <= v < 2.0.0", 15 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.15.1 <= v < 0.16.0" 18 | } -------------------------------------------------------------------------------- /examples/7/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "2.1.0 <= v < 3.0.0", 12 | "evancz/elm-effects": "2.0.0 <= v < 3.0.0", 13 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 14 | "evancz/elm-http": "1.0.0 <= v < 2.0.0", 15 | "evancz/start-app": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.15.1 <= v < 0.16.0" 18 | } -------------------------------------------------------------------------------- /examples/1/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/1) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/1/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/2/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/2) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/2/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/3/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/3) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/3/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/4/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/4) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/4/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/5/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/5) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/5/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/6/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/6) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/6/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/7/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/7) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/7/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/8/README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | 3 | Click [here](https://evancz.github.io/elm-architecture-tutorial/examples/8) to see it running. 4 | 5 | 6 | ## Run Locally 7 | 8 | (First [install Elm](http://elm-lang.org/install)) 9 | 10 | If you do not have this repo on your computer yet, run these commands. 11 | 12 | ```bash 13 | git clone https://github.com/evancz/elm-architecture-tutorial.git 14 | cd elm-architecture-tutorial 15 | ``` 16 | 17 | Once you are in the `elm-architecture-tutorial/` directory, run these commands: 18 | 19 | ```bash 20 | cd examples/8/ 21 | elm-reactor 22 | ``` 23 | 24 | And then open [http://localhost:8000/Main.elm?debug](http://localhost:8000/Main.elm?debug) to see it in action! -------------------------------------------------------------------------------- /examples/8/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/USER/PROJECT.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "Dandandan/Easing": "2.0.0 <= v < 3.0.0", 12 | "elm-lang/core": "2.1.0 <= v < 3.0.0", 13 | "evancz/elm-effects": "2.0.0 <= v < 3.0.0", 14 | "evancz/elm-html": "4.0.1 <= v < 5.0.0", 15 | "evancz/start-app": "2.0.0 <= v < 3.0.0", 16 | "evancz/elm-svg": "2.0.0 <= v < 3.0.0" 17 | }, 18 | "elm-version": "0.15.1 <= v < 0.16.0" 19 | } -------------------------------------------------------------------------------- /examples/1/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (style) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | -- MODEL 9 | 10 | type alias Model = Int 11 | 12 | 13 | -- UPDATE 14 | 15 | type Action = Increment | Decrement 16 | 17 | update : Action -> Model -> Model 18 | update action model = 19 | case action of 20 | Increment -> model + 1 21 | Decrement -> model - 1 22 | 23 | 24 | -- VIEW 25 | 26 | view : Signal.Address Action -> Model -> Html 27 | view address model = 28 | div [] 29 | [ button [ onClick address Decrement ] [ text "-" ] 30 | , div [ countStyle ] [ text (toString model) ] 31 | , div [ countStyle ] [ text (toString (model - 3)) ] 32 | , div [ countStyle ] [ text (toString model) ] 33 | , div [ countStyle ] [ text (toString (model * 2)) ] 34 | , button [ onClick address Increment ] [ text "+" ] 35 | ] 36 | 37 | 38 | countStyle : Attribute 39 | countStyle = 40 | style 41 | [ ("font-size", "20px") 42 | , ("font-family", "monospace") 43 | , ("display", "inline-block") 44 | , ("width", "50px") 45 | , ("text-align", "center") 46 | ] 47 | -------------------------------------------------------------------------------- /examples/3/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter (Model, init, Action, update, view) where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (style) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | -- MODEL 9 | 10 | type alias Model = Int 11 | 12 | 13 | init : Int -> Model 14 | init count = count 15 | 16 | 17 | -- UPDATE 18 | 19 | type Action = Increment | Decrement 20 | 21 | 22 | update : Action -> Model -> Model 23 | update action model = 24 | case action of 25 | Increment -> model + 1 26 | Decrement -> model - 1 27 | 28 | 29 | -- VIEW 30 | 31 | view : Signal.Address Action -> Model -> Html 32 | view address model = 33 | div [] 34 | [ button [ onClick address Decrement ] [ text "-" ] 35 | , div [ countStyle ] [ text (toString model) ] 36 | , div [ countStyle ] [ text (toString (model * 2)) ] 37 | , div [ countStyle ] [ text (toString (model * 3)) ] 38 | , button [ onClick address Increment ] [ text "+" ] 39 | ] 40 | 41 | 42 | countStyle : Attribute 43 | countStyle = 44 | style 45 | [ ("font-size", "20px") 46 | , ("font-family", "monospace") 47 | , ("display", "inline-block") 48 | , ("width", "50px") 49 | , ("text-align", "center") 50 | ] 51 | -------------------------------------------------------------------------------- /examples/2/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter (Model, init, Action, update, view) where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (style) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | -- MODEL 9 | 10 | type alias Model = Int 11 | 12 | 13 | init : Int -> Model 14 | init count = count 15 | 16 | 17 | -- UPDATE 18 | 19 | type Action = Increment | Decrement 20 | 21 | 22 | update : Action -> Model -> Model 23 | update action model = 24 | case action of 25 | Increment -> model + 1 26 | Decrement -> model - 1 27 | 28 | 29 | -- VIEW 30 | 31 | view : Signal.Address Action -> Model -> Html 32 | view address model = 33 | div [] 34 | [ button [ onClick address Decrement ] [ text "-" ] 35 | , div [ countStyle ] [ text (toString model) ] 36 | , div [ countStyle ] [ text (toString (model * 2)) ] 37 | , div [ countStyle ] [ text (toString (model * 3)) ] 38 | , div [ countStyle ] [ text (toString (model * 4)) ] 39 | , button [ onClick address Increment ] [ text "+" ] 40 | ] 41 | 42 | 43 | countStyle : Attribute 44 | countStyle = 45 | style 46 | [ ("font-size", "20px") 47 | , ("font-family", "monospace") 48 | , ("display", "inline-block") 49 | , ("width", "50px") 50 | , ("text-align", "center") 51 | ] 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Evan Czaplicki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /examples/8/SpinSquarePair.elm: -------------------------------------------------------------------------------- 1 | module SpinSquarePair where 2 | 3 | import Effects exposing (Effects) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import SpinSquare 7 | 8 | 9 | -- MODEL 10 | 11 | type alias Model = 12 | { left : SpinSquare.Model 13 | , right : SpinSquare.Model 14 | } 15 | 16 | 17 | init : (Model, Effects Action) 18 | init = 19 | let 20 | (left, leftFx) = SpinSquare.init 21 | (right, rightFx) = SpinSquare.init 22 | in 23 | ( Model left right 24 | , Effects.batch 25 | [ Effects.map Left leftFx 26 | , Effects.map Right rightFx 27 | ] 28 | ) 29 | 30 | 31 | -- UPDATE 32 | 33 | type Action 34 | = Left SpinSquare.Action 35 | | Right SpinSquare.Action 36 | 37 | 38 | update : Action -> Model -> (Model, Effects Action) 39 | update action model = 40 | case action of 41 | Left act -> 42 | let 43 | (left, fx) = SpinSquare.update act model.left 44 | in 45 | ( Model left model.right 46 | , Effects.map Left fx 47 | ) 48 | 49 | Right act -> 50 | let 51 | (right, fx) = SpinSquare.update act model.right 52 | in 53 | ( Model model.left right 54 | , Effects.map Right fx 55 | ) 56 | 57 | 58 | 59 | -- VIEW 60 | 61 | (=>) = (,) 62 | 63 | 64 | view : Signal.Address Action -> Model -> Html 65 | view address model = 66 | div [ style [ "display" => "flex" ] ] 67 | [ SpinSquare.view (Signal.forwardTo address Left) model.left 68 | , SpinSquare.view (Signal.forwardTo address Right) model.right 69 | ] -------------------------------------------------------------------------------- /examples/6/RandomGifPair.elm: -------------------------------------------------------------------------------- 1 | module RandomGifPair where 2 | 3 | import Effects exposing (Effects) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | 7 | import RandomGif 8 | 9 | 10 | -- MODEL 11 | 12 | type alias Model = 13 | { left : RandomGif.Model 14 | , right : RandomGif.Model 15 | } 16 | 17 | 18 | init : String -> String -> (Model, Effects Action) 19 | init leftTopic rightTopic = 20 | let 21 | (left, leftFx) = RandomGif.init leftTopic 22 | (right, rightFx) = RandomGif.init rightTopic 23 | in 24 | ( Model left right 25 | , Effects.batch 26 | [ Effects.map Left leftFx 27 | , Effects.map Right rightFx 28 | ] 29 | ) 30 | 31 | 32 | -- UPDATE 33 | 34 | type Action 35 | = Left RandomGif.Action 36 | | Right RandomGif.Action 37 | 38 | 39 | update : Action -> Model -> (Model, Effects Action) 40 | update action model = 41 | case action of 42 | Left act -> 43 | let 44 | (left, fx) = RandomGif.update act model.left 45 | in 46 | ( Model left model.right 47 | , Effects.map Left fx 48 | ) 49 | 50 | Right act -> 51 | let 52 | (right, fx) = RandomGif.update act model.right 53 | in 54 | ( Model model.left right 55 | , Effects.map Right fx 56 | ) 57 | 58 | 59 | -- VIEW 60 | 61 | view : Signal.Address Action -> Model -> Html 62 | view address model = 63 | div [ style [ ("display", "flex") ] ] 64 | [ RandomGif.view (Signal.forwardTo address Left) model.left 65 | , RandomGif.view (Signal.forwardTo address Right) model.right 66 | ] 67 | -------------------------------------------------------------------------------- /examples/2/CounterPair.elm: -------------------------------------------------------------------------------- 1 | module CounterPair where 2 | 3 | import Counter 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | 8 | 9 | -- MODEL 10 | 11 | type alias Model = 12 | { topCounter : Counter.Model 13 | , bottomCounter : Counter.Model 14 | , middleCounter : Counter.Model 15 | } 16 | 17 | 18 | init : Int -> Int -> Int -> Model 19 | init top bottom middle = 20 | { topCounter = Counter.init top 21 | , bottomCounter = Counter.init bottom 22 | , middleCounter = Counter.init middle 23 | } 24 | 25 | 26 | -- UPDATE 27 | 28 | type Action 29 | = Reset 30 | | Top Counter.Action 31 | | Bottom Counter.Action 32 | | Middle Counter.Action 33 | 34 | update : Action -> Model -> Model 35 | update action model = 36 | case action of 37 | Reset -> init 0 0 0 38 | 39 | Top act -> 40 | { model | 41 | topCounter <- Counter.update act model.topCounter 42 | } 43 | 44 | Bottom act -> 45 | { model | 46 | bottomCounter <- Counter.update act model.bottomCounter 47 | } 48 | 49 | Middle act -> 50 | { model | middleCounter <- Counter.update act model.middleCounter } 51 | 52 | 53 | 54 | -- VIEW 55 | 56 | view : Signal.Address Action -> Model -> Html 57 | view address model = 58 | div [] 59 | [ Counter.view (Signal.forwardTo address Top) model.topCounter 60 | , Counter.view (Signal.forwardTo address Bottom) model.bottomCounter 61 | , Counter.view (Signal.forwardTo address Middle) model.middleCounter 62 | , button [ onClick address Reset ] [ text "RESET" ] 63 | ] 64 | -------------------------------------------------------------------------------- /examples/4/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter (Model, init, Action, update, view, viewWithRemoveButton, Context) where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | 7 | 8 | -- MODEL 9 | 10 | type alias Model = Int 11 | 12 | 13 | init : Int -> Model 14 | init count = count 15 | 16 | 17 | -- UPDATE 18 | 19 | type Action = Increment | Decrement 20 | 21 | 22 | update : Action -> Model -> Model 23 | update action model = 24 | case action of 25 | Increment -> model + 1 26 | Decrement -> model - 1 27 | 28 | 29 | -- VIEW 30 | 31 | view : Signal.Address Action -> Model -> Html 32 | view address model = 33 | div [] 34 | [ button [ onClick address Decrement ] [ text "-" ] 35 | , div [ countStyle ] [ text (toString model) ] 36 | , button [ onClick address Increment ] [ text "+" ] 37 | ] 38 | 39 | 40 | type alias Context = 41 | { actions : Signal.Address Action 42 | , remove : Signal.Address () 43 | } 44 | 45 | 46 | viewWithRemoveButton : Context -> Model -> Html 47 | viewWithRemoveButton context model = 48 | div [] 49 | [ button [ onClick context.actions Decrement ] [ text "-" ] 50 | , div [ countStyle ] [ text (toString model) ] 51 | , button [ onClick context.actions Increment ] [ text "+" ] 52 | , div [ countStyle ] [] 53 | , button [ onClick context.remove () ] [ text "X" ] 54 | ] 55 | 56 | 57 | countStyle : Attribute 58 | countStyle = 59 | style 60 | [ ("font-size", "20px") 61 | , ("font-family", "monospace") 62 | , ("display", "inline-block") 63 | , ("width", "50px") 64 | , ("text-align", "center") 65 | ] 66 | -------------------------------------------------------------------------------- /examples/4/CounterList.elm: -------------------------------------------------------------------------------- 1 | module CounterList where 2 | 3 | import Counter 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | 8 | 9 | -- MODEL 10 | 11 | type alias Model = 12 | { counters : List ( ID, Counter.Model ) 13 | , nextID : ID 14 | } 15 | 16 | type alias ID = Int 17 | 18 | 19 | init : Model 20 | init = 21 | { counters = [] 22 | , nextID = 0 23 | } 24 | 25 | 26 | -- UPDATE 27 | 28 | type Action 29 | = Insert 30 | | Remove ID 31 | | Modify ID Counter.Action 32 | 33 | 34 | update : Action -> Model -> Model 35 | update action model = 36 | case action of 37 | Insert -> 38 | { model | 39 | counters <- ( model.nextID, Counter.init 0 ) :: model.counters, 40 | nextID <- model.nextID + 1 41 | } 42 | 43 | Remove id -> 44 | { model | 45 | counters <- List.filter (\(counterID, _) -> counterID /= id) model.counters 46 | } 47 | 48 | Modify id counterAction -> 49 | let updateCounter (counterID, counterModel) = 50 | if counterID == id 51 | then (counterID, Counter.update counterAction counterModel) 52 | else (counterID, counterModel) 53 | in 54 | { model | counters <- List.map updateCounter model.counters } 55 | 56 | 57 | -- VIEW 58 | 59 | view : Signal.Address Action -> Model -> Html 60 | view address model = 61 | let insert = button [ onClick address Insert ] [ text "Add" ] 62 | in 63 | div [] (insert :: List.map (viewCounter address) model.counters) 64 | 65 | 66 | viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html 67 | viewCounter address (id, model) = 68 | let context = 69 | Counter.Context 70 | (Signal.forwardTo address (Modify id)) 71 | (Signal.forwardTo address (always (Remove id))) 72 | in 73 | Counter.viewWithRemoveButton context model 74 | -------------------------------------------------------------------------------- /examples/5/RandomGif.elm: -------------------------------------------------------------------------------- 1 | module RandomGif where 2 | 3 | import Effects exposing (Effects, Never) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (style) 6 | import Html.Events exposing (onClick) 7 | import Http 8 | import Json.Decode as Json 9 | import Task 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = 15 | { topic : String 16 | , gifUrl : String 17 | } 18 | 19 | 20 | init : String -> (Model, Effects Action) 21 | init topic = 22 | ( Model topic "assets/waiting.gif" 23 | , getRandomGif topic 24 | ) 25 | 26 | 27 | -- UPDATE 28 | 29 | type Action 30 | = RequestMore 31 | | NewGif (Maybe String) 32 | 33 | 34 | update : Action -> Model -> (Model, Effects Action) 35 | update action model = 36 | case action of 37 | RequestMore -> 38 | (model, getRandomGif model.topic) 39 | 40 | NewGif maybeUrl -> 41 | ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) 42 | , Effects.none 43 | ) 44 | 45 | 46 | -- VIEW 47 | 48 | (=>) = (,) 49 | 50 | 51 | view : Signal.Address Action -> Model -> Html 52 | view address model = 53 | div [ style [ "width" => "200px" ] ] 54 | [ h2 [headerStyle] [text model.topic] 55 | , div [imgStyle model.gifUrl] [] 56 | , button [ onClick address RequestMore ] [ text "More Please!" ] 57 | ] 58 | 59 | 60 | headerStyle : Attribute 61 | headerStyle = 62 | style 63 | [ "width" => "200px" 64 | , "text-align" => "center" 65 | ] 66 | 67 | 68 | imgStyle : String -> Attribute 69 | imgStyle url = 70 | style 71 | [ "display" => "inline-block" 72 | , "width" => "200px" 73 | , "height" => "200px" 74 | , "background-position" => "center center" 75 | , "background-size" => "cover" 76 | , "background-image" => ("url('" ++ url ++ "')") 77 | ] 78 | 79 | 80 | -- EFFECTS 81 | 82 | getRandomGif : String -> Effects Action 83 | getRandomGif topic = 84 | Http.get decodeUrl (randomUrl topic) 85 | |> Task.toMaybe 86 | |> Task.map NewGif 87 | |> Effects.task 88 | 89 | 90 | randomUrl : String -> String 91 | randomUrl topic = 92 | Http.url "http://api.giphy.com/v1/gifs/random" 93 | [ "api_key" => "dc6zaTOxFJmzC" 94 | , "tag" => topic 95 | ] 96 | 97 | 98 | decodeUrl : Json.Decoder String 99 | decodeUrl = 100 | Json.at ["data", "image_url"] Json.string 101 | -------------------------------------------------------------------------------- /examples/6/RandomGif.elm: -------------------------------------------------------------------------------- 1 | module RandomGif where 2 | 3 | import Effects exposing (Effects, Never) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (style) 6 | import Html.Events exposing (onClick) 7 | import Http 8 | import Json.Decode as Json 9 | import Task 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = 15 | { topic : String 16 | , gifUrl : String 17 | } 18 | 19 | 20 | init : String -> (Model, Effects Action) 21 | init topic = 22 | ( Model topic "assets/waiting.gif" 23 | , getRandomGif topic 24 | ) 25 | 26 | 27 | -- UPDATE 28 | 29 | type Action 30 | = RequestMore 31 | | NewGif (Maybe String) 32 | 33 | 34 | update : Action -> Model -> (Model, Effects Action) 35 | update action model = 36 | case action of 37 | RequestMore -> 38 | (model, getRandomGif model.topic) 39 | 40 | NewGif maybeUrl -> 41 | ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) 42 | , Effects.none 43 | ) 44 | 45 | 46 | -- VIEW 47 | 48 | (=>) = (,) 49 | 50 | 51 | view : Signal.Address Action -> Model -> Html 52 | view address model = 53 | div [ style [ "width" => "200px" ] ] 54 | [ h2 [headerStyle] [text model.topic] 55 | , div [imgStyle model.gifUrl] [] 56 | , button [ onClick address RequestMore ] [ text "More Please!" ] 57 | ] 58 | 59 | 60 | headerStyle : Attribute 61 | headerStyle = 62 | style 63 | [ "width" => "200px" 64 | , "text-align" => "center" 65 | ] 66 | 67 | 68 | imgStyle : String -> Attribute 69 | imgStyle url = 70 | style 71 | [ "display" => "inline-block" 72 | , "width" => "200px" 73 | , "height" => "200px" 74 | , "background-position" => "center center" 75 | , "background-size" => "cover" 76 | , "background-image" => ("url('" ++ url ++ "')") 77 | ] 78 | 79 | 80 | -- EFFECTS 81 | 82 | getRandomGif : String -> Effects Action 83 | getRandomGif topic = 84 | Http.get decodeUrl (randomUrl topic) 85 | |> Task.toMaybe 86 | |> Task.map NewGif 87 | |> Effects.task 88 | 89 | 90 | randomUrl : String -> String 91 | randomUrl topic = 92 | Http.url "http://api.giphy.com/v1/gifs/random" 93 | [ "api_key" => "dc6zaTOxFJmzC" 94 | , "tag" => topic 95 | ] 96 | 97 | 98 | decodeUrl : Json.Decoder String 99 | decodeUrl = 100 | Json.at ["data", "image_url"] Json.string 101 | -------------------------------------------------------------------------------- /examples/7/RandomGif.elm: -------------------------------------------------------------------------------- 1 | module RandomGif where 2 | 3 | import Effects exposing (Effects, Never) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (style) 6 | import Html.Events exposing (onClick) 7 | import Http 8 | import Json.Decode as Json 9 | import Task 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = 15 | { topic : String 16 | , gifUrl : String 17 | } 18 | 19 | 20 | init : String -> (Model, Effects Action) 21 | init topic = 22 | ( Model topic "assets/waiting.gif" 23 | , getRandomGif topic 24 | ) 25 | 26 | 27 | -- UPDATE 28 | 29 | type Action 30 | = RequestMore 31 | | NewGif (Maybe String) 32 | 33 | 34 | update : Action -> Model -> (Model, Effects Action) 35 | update action model = 36 | case action of 37 | RequestMore -> 38 | (model, getRandomGif model.topic) 39 | 40 | NewGif maybeUrl -> 41 | ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) 42 | , Effects.none 43 | ) 44 | 45 | 46 | -- VIEW 47 | 48 | (=>) = (,) 49 | 50 | 51 | view : Signal.Address Action -> Model -> Html 52 | view address model = 53 | div [ style [ "width" => "200px" ] ] 54 | [ h2 [headerStyle] [text model.topic] 55 | , div [imgStyle model.gifUrl] [] 56 | , button [ onClick address RequestMore ] [ text "More Please!" ] 57 | ] 58 | 59 | 60 | headerStyle : Attribute 61 | headerStyle = 62 | style 63 | [ "width" => "200px" 64 | , "text-align" => "center" 65 | ] 66 | 67 | 68 | imgStyle : String -> Attribute 69 | imgStyle url = 70 | style 71 | [ "display" => "inline-block" 72 | , "width" => "200px" 73 | , "height" => "200px" 74 | , "background-position" => "center center" 75 | , "background-size" => "cover" 76 | , "background-image" => ("url('" ++ url ++ "')") 77 | ] 78 | 79 | 80 | -- EFFECTS 81 | 82 | getRandomGif : String -> Effects Action 83 | getRandomGif topic = 84 | Http.get decodeUrl (randomUrl topic) 85 | |> Task.toMaybe 86 | |> Task.map NewGif 87 | |> Effects.task 88 | 89 | 90 | randomUrl : String -> String 91 | randomUrl topic = 92 | Http.url "http://api.giphy.com/v1/gifs/random" 93 | [ "api_key" => "dc6zaTOxFJmzC" 94 | , "tag" => topic 95 | ] 96 | 97 | 98 | decodeUrl : Json.Decoder String 99 | decodeUrl = 100 | Json.at ["data", "image_url"] Json.string 101 | -------------------------------------------------------------------------------- /examples/8/SpinSquare.elm: -------------------------------------------------------------------------------- 1 | module SpinSquare (Model, Action, init, update, view) where 2 | 3 | import Easing exposing (ease, easeOutBounce, float) 4 | import Effects exposing (Effects) 5 | import Html exposing (Html) 6 | import Svg exposing (svg, rect, g, text, text') 7 | import Svg.Attributes exposing (..) 8 | import Svg.Events exposing (onClick) 9 | import Time exposing (Time, second) 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = 15 | { angle : Float 16 | , animationState : AnimationState 17 | } 18 | 19 | 20 | type alias AnimationState = 21 | Maybe { prevClockTime : Time, elapsedTime: Time } 22 | 23 | 24 | init : (Model, Effects Action) 25 | init = 26 | ( { angle = 0, animationState = Nothing } 27 | , Effects.none 28 | ) 29 | 30 | 31 | rotateStep = 90 32 | duration = second 33 | 34 | 35 | -- UPDATE 36 | 37 | type Action 38 | = Spin 39 | | Tick Time 40 | 41 | 42 | update : Action -> Model -> (Model, Effects Action) 43 | update msg model = 44 | case msg of 45 | Spin -> 46 | case model.animationState of 47 | Nothing -> 48 | ( model, Effects.tick Tick ) 49 | 50 | Just _ -> 51 | ( model, Effects.none ) 52 | 53 | Tick clockTime -> 54 | let 55 | newElapsedTime = 56 | case model.animationState of 57 | Nothing -> 58 | 0 59 | 60 | Just {elapsedTime, prevClockTime} -> 61 | elapsedTime + (clockTime - prevClockTime) 62 | in 63 | if newElapsedTime > duration then 64 | ( { angle = model.angle + rotateStep 65 | , animationState = Nothing 66 | } 67 | , Effects.none 68 | ) 69 | else 70 | ( { angle = model.angle 71 | , animationState = Just { elapsedTime = newElapsedTime, prevClockTime = clockTime } 72 | } 73 | , Effects.tick Tick 74 | ) 75 | 76 | 77 | -- VIEW 78 | 79 | toOffset : AnimationState -> Float 80 | toOffset animationState = 81 | case animationState of 82 | Nothing -> 83 | 0 84 | 85 | Just {elapsedTime} -> 86 | ease easeOutBounce float 0 rotateStep duration elapsedTime 87 | 88 | 89 | view : Signal.Address Action -> Model -> Html 90 | view address model = 91 | let 92 | angle = 93 | model.angle + toOffset model.animationState 94 | in 95 | svg 96 | [ width "200", height "200", viewBox "0 0 200 200" ] 97 | [ g [ transform ("translate(100, 100) rotate(" ++ toString angle ++ ")") 98 | , onClick (Signal.message address Spin) 99 | ] 100 | [ rect 101 | [ x "-50" 102 | , y "-50" 103 | , width "100" 104 | , height "100" 105 | , rx "15" 106 | , ry "15" 107 | , style "fill: #60B5CC;" 108 | ] 109 | [] 110 | , text' [ fill "white", textAnchor "middle" ] [ text "Click me!" ] 111 | ] 112 | ] -------------------------------------------------------------------------------- /examples/3/CounterList.elm: -------------------------------------------------------------------------------- 1 | module CounterList where 2 | 3 | import Counter 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import String exposing (toInt) 8 | 9 | 10 | -- MODEL 11 | 12 | type alias Model = 13 | { counters : List ( ID, Counter.Model ) 14 | , nextID : ID 15 | , removeID : ID 16 | , removeIndex : Int 17 | } 18 | 19 | type alias ID = Int 20 | 21 | 22 | init : Model 23 | init = 24 | { counters = [] 25 | , nextID = 0 26 | , removeID = 0 27 | , removeIndex = 0 28 | } 29 | 30 | 31 | -- UPDATE 32 | 33 | type Action 34 | = Insert 35 | | Remove 36 | | RemoveById 37 | | RemoveByIndex 38 | | ModifyRemoveID ID 39 | | ModifyRemoveIndex Int 40 | | Modify ID Counter.Action 41 | 42 | 43 | update : Action -> Model -> Model 44 | update action model = 45 | case action of 46 | Insert -> 47 | let newCounter = ( model.nextID, Counter.init model.nextID ) 48 | newCounters = model.counters ++ [ newCounter ] 49 | in 50 | { model | 51 | counters <- newCounters, 52 | nextID <- model.nextID + 1 53 | } 54 | 55 | Remove -> 56 | { model | counters <- List.drop 1 model.counters } 57 | 58 | RemoveById -> 59 | { model | counters <- List.filter ( \(counterid, _) -> counterid /= model.removeID) model.counters } 60 | 61 | RemoveByIndex -> 62 | { model | counters <- (List.take (model.removeIndex) model.counters) ++ (List.drop (model.removeIndex + 1) model.counters) } 63 | -- { model | counters <- List.map snd (List.filter ( \(counterIndex, _) -> counterIndex /= model.removeIndex) (List.indexedMap (,) model.counters)) } 64 | 65 | ModifyRemoveID id -> 66 | { model | removeID <- id } 67 | 68 | ModifyRemoveIndex id -> 69 | { model | removeIndex <- id } 70 | 71 | Modify id counterAction -> 72 | let updateCounter (counterID, counterModel) = 73 | if counterID == id 74 | then (counterID, Counter.update counterAction counterModel) 75 | else (counterID, counterModel) 76 | in 77 | { model | counters <- List.map updateCounter model.counters } 78 | 79 | 80 | -- VIEW 81 | view : Signal.Address Action -> Model -> Html 82 | view address model = 83 | let counters = List.map (viewCounter address) model.counters 84 | remove = button [ onClick address Remove ] [ text "Remove" ] 85 | insert = button [ onClick address Insert ] [ text "Add" ] 86 | removeId = input [ type' "number" 87 | , on "input" targetValue (\str -> Signal.message address (ModifyRemoveID (Maybe.withDefault 0 (Result.toMaybe (toInt str))))) ] [] 88 | removeWithId = button [ onClick address RemoveById ] [ text "Remove Specific Id" ] 89 | removeIndex = input [ type' "number" 90 | , on "input" targetValue (\str -> Signal.message address (ModifyRemoveIndex (Maybe.withDefault 0 (Result.toMaybe (toInt str))))) ] [] 91 | removeWithIndex = button [ onClick address RemoveByIndex ] [ text "Remove Specific Index" ] 92 | in 93 | div [] ([remove, insert, removeId, removeWithId, removeIndex, removeWithIndex ] ++ counters) 94 | 95 | 96 | viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html 97 | viewCounter address (id, model) = 98 | Counter.view (Signal.forwardTo address (Modify id)) model 99 | -------------------------------------------------------------------------------- /examples/7/RandomGifList.elm: -------------------------------------------------------------------------------- 1 | module RandomGifList where 2 | 3 | import Effects exposing (Effects, map, batch, Never) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as Json 8 | 9 | import RandomGif 10 | 11 | 12 | -- MODEL 13 | 14 | type alias Model = 15 | { topic : String 16 | , gifList : List (Int, RandomGif.Model) 17 | , uid : Int 18 | } 19 | 20 | 21 | init : (Model, Effects Action) 22 | init = 23 | ( Model "" [] 0 24 | , Effects.none 25 | ) 26 | 27 | 28 | -- UPDATE 29 | 30 | type Action 31 | = Topic String 32 | | Create 33 | | SubMsg Int RandomGif.Action 34 | 35 | 36 | update : Action -> Model -> (Model, Effects Action) 37 | update message model = 38 | case message of 39 | Topic topic -> 40 | ( { model | topic <- topic } 41 | , Effects.none 42 | ) 43 | 44 | Create -> 45 | let 46 | (newRandomGif, fx) = 47 | RandomGif.init model.topic 48 | 49 | newModel = 50 | Model "" (model.gifList ++ [(model.uid, newRandomGif)]) (model.uid + 1) 51 | in 52 | ( newModel 53 | , map (SubMsg model.uid) fx 54 | ) 55 | 56 | SubMsg msgId msg -> 57 | let 58 | subUpdate ((id, randomGif) as entry) = 59 | if id == msgId then 60 | let 61 | (newRandomGif, fx) = RandomGif.update msg randomGif 62 | in 63 | ( (id, newRandomGif) 64 | , map (SubMsg id) fx 65 | ) 66 | else 67 | (entry, Effects.none) 68 | 69 | (newGifList, fxList) = 70 | model.gifList 71 | |> List.map subUpdate 72 | |> List.unzip 73 | in 74 | ( { model | gifList <- newGifList } 75 | , batch fxList 76 | ) 77 | 78 | 79 | -- VIEW 80 | 81 | (=>) = (,) 82 | 83 | 84 | view : Signal.Address Action -> Model -> Html 85 | view address model = 86 | div [] 87 | [ input 88 | [ placeholder "What kind of gifs do you want?" 89 | , value model.topic 90 | , onEnter address Create 91 | , on "input" targetValue (Signal.message address << Topic) 92 | , inputStyle 93 | ] 94 | [] 95 | , div [ style [ "display" => "flex", "flex-wrap" => "wrap" ] ] 96 | (List.map (elementView address) model.gifList) 97 | ] 98 | 99 | 100 | elementView : Signal.Address Action -> (Int, RandomGif.Model) -> Html 101 | elementView address (id, model) = 102 | RandomGif.view (Signal.forwardTo address (SubMsg id)) model 103 | 104 | 105 | inputStyle : Attribute 106 | inputStyle = 107 | style 108 | [ ("width", "100%") 109 | , ("height", "40px") 110 | , ("padding", "10px 0") 111 | , ("font-size", "2em") 112 | , ("text-align", "center") 113 | ] 114 | 115 | 116 | onEnter : Signal.Address a -> a -> Attribute 117 | onEnter address value = 118 | on "keydown" 119 | (Json.customDecoder keyCode is13) 120 | (\_ -> Signal.message address value) 121 | 122 | 123 | is13 : Int -> Result String () 124 | is13 code = 125 | if code == 13 then Ok () else Err "not the right key code" 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Elm Architecture 2 | 3 | This tutorial outlines “The Elm Architecture” which you will see in all [Elm][] programs, from [TodoMVC][] and [dreamwriter][] to the code running in production at [NoRedInk][] and [CircuitHub][]. The basic pattern is useful whether you are writing your front-end in Elm or JS or whatever else. 4 | 5 | [Elm]: http://elm-lang.org/ 6 | [TodoMVC]: https://github.com/evancz/elm-todomvc 7 | [dreamwriter]: https://github.com/rtfeldman/dreamwriter#dreamwriter 8 | [NoRedInk]: https://www.noredink.com/ 9 | [CircuitHub]: https://www.circuithub.com/ 10 | 11 | The Elm Architecture is a simple pattern for infinitely nestable components. It is great for modularity, code reuse, and testing. Ultimately, this pattern makes it easy to create complex web apps in a way that stays modular. We will run through 8 examples, slowly building on core principles and patterns: 12 | 13 | 1. [Counter](http://evancz.github.io/elm-architecture-tutorial/examples/1.html) 14 | 2. [Pair of counters](http://evancz.github.io/elm-architecture-tutorial/examples/2.html) 15 | 3. [List of counters](http://evancz.github.io/elm-architecture-tutorial/examples/3.html) 16 | 4. [List of counters (variation)](http://evancz.github.io/elm-architecture-tutorial/examples/4.html) 17 | 5. [GIF fetcher](http://evancz.github.io/elm-architecture-tutorial/examples/5.html) 18 | 6. [Pair of GIF fetchers](http://evancz.github.io/elm-architecture-tutorial/examples/6.html) 19 | 7. [List of GIF fetchers](http://evancz.github.io/elm-architecture-tutorial/examples/7.html) 20 | 8. [Pair of animating squares](http://evancz.github.io/elm-architecture-tutorial/examples/8.html) 21 | 22 | This tutorial will really help! It will bring out the concepts and ideas necessary to get to make examples 7 and 8 super easy. Investing in the foundation will be worth it! 23 | 24 | One very interesting aspect of the architecture in all these programs is that it *emerges* from Elm naturally. The language design itself leads you towards this architecture whether you have read this document and know the benefits or not. I actually discovered this pattern just using Elm and have been shocked by its simplicity and power. 25 | 26 | **Note**: To follow along with this tutorial with code, [install Elm](http://elm-lang.org/install) and fork this repo. Each example in the tutorial gives instructions of how to run the code. 27 | 28 | 29 | ## The Basic Pattern 30 | 31 | The logic of every Elm program will break up into three cleanly separated parts: 32 | 33 | * model 34 | * update 35 | * view 36 | 37 | You can pretty reliably start with the following skeleton and then iteratively fill in details for your particular case. 38 | 39 | > If you are new to reading Elm code, check out the [language docs](http://elm-lang.org/docs) which covers everything from syntax to getting into a “functional mindset”. The first two sections of [the complete guide](http://elm-lang.org/docs#complete-guide) will get you up to speed! 40 | 41 | ```elm 42 | -- MODEL 43 | 44 | type alias Model = { ... } 45 | 46 | 47 | -- UPDATE 48 | 49 | type Action = Reset | ... 50 | 51 | update : Action -> Model -> Model 52 | update action model = 53 | case action of 54 | Reset -> ... 55 | ... 56 | 57 | 58 | -- VIEW 59 | 60 | view : Model -> Html 61 | view = 62 | ... 63 | ``` 64 | 65 | This tutorial is all about this pattern and small variations and extensions. 66 | 67 | 68 | ## Example 1: A Counter 69 | 70 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/1.html) / [run locally](examples/1/)** 71 | 72 | Our first example is a simple counter that can be incremented or decremented. 73 | 74 | [The code](examples/1/Counter.elm) starts with a very simple model. We just need to keep track of a single number: 75 | 76 | ```elm 77 | type alias Model = Int 78 | ``` 79 | 80 | When it comes to updating our model, things are relatively simple again. We define a set of actions that can be performed, and an `update` function to actually perform those actions: 81 | 82 | ```elm 83 | type Action = Increment | Decrement 84 | 85 | update : Action -> Model -> Model 86 | update action model = 87 | case action of 88 | Increment -> model + 1 89 | Decrement -> model - 1 90 | ``` 91 | 92 | Notice that our `Action` [union type][] does not *do* anything. It simply describes the actions that are possible. If someone decides our counter should be doubled when a certain button is pressed, that will be a new case in `Action`. This means our code ends up very clear about how our model can be transformed. Anyone reading this code will immediately know what is allowed and what is not. Furthermore, they will know exactly how to add new features in a consistent way. 93 | 94 | [union type]: http://elm-lang.org/learn/Union-Types.elm 95 | 96 | Finally, we create a way to `view` our `Model`. We are using [elm-html][] to create some HTML to show in a browser. We will create a div that contains: a decrement button, a div showing the current count, and an increment button. 97 | 98 | [elm-html]: http://elm-lang.org/blog/Blazing-Fast-Html.elm 99 | 100 | ```elm 101 | view : Signal.Address Action -> Model -> Html 102 | view address model = 103 | div [] 104 | [ button [ onClick address Decrement ] [ text "-" ] 105 | , div [ countStyle ] [ text (toString model) ] 106 | , button [ onClick address Increment ] [ text "+" ] 107 | ] 108 | 109 | countStyle : Attribute 110 | countStyle = 111 | ... 112 | ``` 113 | 114 | The tricky thing about our `view` function is the `Address`. We will dive into that in the next section! For now, I just want you to notice that **this code is entirely declarative**. We take in a `Model` and produce some `Html`. That is it. At no point do we mutate the DOM manually, which gives the library [much more freedom to make clever optimizations][elm-html] and actually makes rendering *faster* overall. It is crazy. Furthermore, `view` is a plain old function so we can get the full power of Elm’s module system, test frameworks, and libraries when creating views. 115 | 116 | This pattern is the essence of architecting Elm programs. Every example we see from now on will be a slight variation on this basic pattern: `Model`, `update`, `view`. 117 | 118 | 119 | ## Starting the Program 120 | 121 | Pretty much all Elm programs will have a small bit of code that drives the whole application. For each example in this tutorial, that code is broken out into `Main.elm`. For our counter example, the interesting code looks like this: 122 | 123 | ```elm 124 | import Counter exposing (update, view) 125 | import StartApp.Simple exposing (start) 126 | 127 | main = 128 | start { model = 0, update = update, view = view } 129 | ``` 130 | 131 | We are using the [`StartApp`](https://github.com/evancz/start-app) package to wire together our initial model with the update and view functions. It is a small wrapper around Elm's [signals](http://elm-lang.org/learn/Using-Signals.elm) so that you do not need to dive into that concept yet. 132 | 133 | The key to wiring up your application is the concept of an `Address`. Every event handler in our `view` function reports to a particular address. It just sends chunks of data along. The `StartApp` package monitors all the messages coming in to this address and feeds them into the `update` function. The model gets updated and [elm-html][] takes care of rendering the changes efficiently. 134 | 135 | This means values flow through an Elm program in only one direction, something like this: 136 | 137 | ![Signal Graph Summary](diagrams/signal-graph-summary.png) 138 | 139 | The blue part is our core Elm program which is exactly the model/update/view pattern we have been discussing so far. When programming in Elm, you can mostly think inside this box and make great progress. 140 | 141 | Notice we are not *performing* actions as they get sent back to our app. We are simply sending some data over. This separation is a key detail, keeping our logic totally separate from our view code. 142 | 143 | 144 | ## Example 2: A Pair of Counters 145 | 146 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/2.html) / [run locally](examples/2/)** 147 | 148 | In example 1 we created a basic counter, but how does that pattern scale when we want *two* counters? Can we keep things modular? 149 | 150 | Wouldn't it be great if we could reuse all the code from example 1? The crazy thing about the Elm Architecture is that **we can reuse code with absolutely no changes**. When we created the `Counter` module in example one, it encapsulated all the implementation details so we can use them elsewhere: 151 | 152 | ```elm 153 | module Counter (Model, init, Action, update, view) where 154 | 155 | type Model 156 | 157 | init : Int -> Model 158 | 159 | type Action 160 | 161 | update : Action -> Model -> Model 162 | 163 | view : Signal.Address Action -> Model -> Html 164 | ``` 165 | 166 | Creating modular code is all about creating strong abstractions. We want boundaries which appropriately expose functionality and hide implementation. From outside of the `Counter` module, we just see a basic set of values: `Model`, `init`, `Action`, `update`, and `view`. We do not care at all how these things are implemented. In fact, it is *impossible* to know how these things are implemented. This means no one can rely on implementation details that were not made public. 167 | 168 | So we can reuse our `Counter` module, but now we need to use it to create our `CounterPair`. As always, we start with a `Model`: 169 | 170 | ```elm 171 | type alias Model = 172 | { topCounter : Counter.Model 173 | , bottomCounter : Counter.Model 174 | } 175 | 176 | init : Int -> Int -> Model 177 | init top bottom = 178 | { topCounter = Counter.init top 179 | , bottomCounter = Counter.init bottom 180 | } 181 | ``` 182 | 183 | Our `Model` is a record with two fields, one for each of the counters we would like to show on screen. This fully describes all of the application state. We also have an `init` function to create a new `Model` whenever we want. 184 | 185 | Next we describe the set of `Actions` we would like to support. This time our features should be: reset all counters, update the top counter, or update the bottom counter. 186 | 187 | ```elm 188 | type Action 189 | = Reset 190 | | Top Counter.Action 191 | | Bottom Counter.Action 192 | ``` 193 | 194 | Notice that our [union type][] refers to the `Counter.Action` type, but we do not know the particulars of those actions. When we create our `update` function, we are mainly routing these `Counter.Actions` to the right place: 195 | 196 | ```elm 197 | update : Action -> Model -> Model 198 | update action model = 199 | case action of 200 | Reset -> init 0 0 201 | 202 | Top act -> 203 | { model | 204 | topCounter <- Counter.update act model.topCounter 205 | } 206 | 207 | Bottom act -> 208 | { model | 209 | bottomCounter <- Counter.update act model.bottomCounter 210 | } 211 | ``` 212 | 213 | So now the final thing to do is create a `view` function that shows both of our counters on screen along with a reset button. 214 | 215 | ```elm 216 | view : Signal.Address Action -> Model -> Html 217 | view address model = 218 | div [] 219 | [ Counter.view (Signal.forwardTo address Top) model.topCounter 220 | , Counter.view (Signal.forwardTo address Bottom) model.bottomCounter 221 | , button [ onClick address Reset ] [ text "RESET" ] 222 | ] 223 | ``` 224 | 225 | Notice that we are able to reuse the `Counter.view` function for both of our counters. For each counter we create a forwarding address. Essentially what we are doing here is saying, “these counters will tag all outgoing messages with `Top` or `Bottom` so we can tell the difference.” 226 | 227 | That is the whole thing. The cool thing is that we can keep nesting more and more. We can take the `CounterPair` module, expose the key values and functions, and create a `CounterPairPair` or whatever it is we need. 228 | 229 | 230 | ## Example 3: A Dynamic List of Counters 231 | 232 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/3.html) / [run locally](examples/3/)** 233 | 234 | A pair of counters is cool, but what about a list of counters where we can add and remove counters as we see fit? Can this pattern work for that too? 235 | 236 | Again we can reuse the `Counter` module exactly as it was in example 1 and 2! 237 | 238 | ```elm 239 | module Counter (Model, init, Action, update, view) 240 | ``` 241 | 242 | That means we can just get started on our `CounterList` module. As always, we begin with our `Model`: 243 | 244 | ```elm 245 | type alias Model = 246 | { counters : List ( ID, Counter.Model ) 247 | , nextID : ID 248 | } 249 | 250 | type alias ID = Int 251 | ``` 252 | 253 | Now our model has a list of counters, each annotated with a unique ID. These IDs allow us to distinguish between them, so if we need to update counter number 4 we have a nice way to refer to it. (This ID also gives us something convenient to [`key`][key] on when we are thinking about optimizing rendering, but that is not the focus of this tutorial!) Our model also contains a 254 | `nextID` which helps us assign unique IDs to each counter as we add new ones. 255 | 256 | [key]: http://package.elm-lang.org/packages/evancz/elm-html/latest/Html-Attributes#key 257 | 258 | Now we can define the set of `Actions` that can be performed on our model. We want to be able to add counters, remove counters, and update certain counters. 259 | 260 | ```elm 261 | type Action 262 | = Insert 263 | | Remove 264 | | Modify ID Counter.Action 265 | ``` 266 | 267 | Our `Action` [union type][] is shockingly close to the high-level description. Now we can define our `update` function. 268 | 269 | ```elm 270 | update : Action -> Model -> Model 271 | update action model = 272 | case action of 273 | Insert -> 274 | let newCounter = ( model.nextID, Counter.init 0 ) 275 | newCounters = model.counters ++ [ newCounter ] 276 | in 277 | { model | 278 | counters <- newCounters, 279 | nextID <- model.nextID + 1 280 | } 281 | 282 | Remove -> 283 | { model | counters <- List.drop 1 model.counters } 284 | 285 | Modify id counterAction -> 286 | let updateCounter (counterID, counterModel) = 287 | if counterID == id 288 | then (counterID, Counter.update counterAction counterModel) 289 | else (counterID, counterModel) 290 | in 291 | { model | counters <- List.map updateCounter model.counters } 292 | ``` 293 | 294 | Here is a high-level description of each case: 295 | 296 | * `Insert` — First we create a new counter and put it at the end of 297 | our counter list. Then we increment our `nextID` so that we have a fresh 298 | ID next time around. 299 | 300 | * `Remove` — Drop the first member of our counter list. 301 | 302 | * `Modify` — Run through all of our counters. If we find one with 303 | a matching ID, we perform the given `Action` on that counter. 304 | 305 | All that is left to do now is to define the `view`. 306 | 307 | ```elm 308 | view : Signal.Address Action -> Model -> Html 309 | view address model = 310 | let counters = List.map (viewCounter address) model.counters 311 | remove = button [ onClick address Remove ] [ text "Remove" ] 312 | insert = button [ onClick address Insert ] [ text "Add" ] 313 | in 314 | div [] ([remove, insert] ++ counters) 315 | 316 | viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html 317 | viewCounter address (id, model) = 318 | Counter.view (Signal.forwardTo address (Modify id)) model 319 | ``` 320 | 321 | The fun part here is the `viewCounter` function. It uses the same old 322 | `Counter.view` function, but in this case we provide a forwarding address that annotates all messages with the ID of the particular counter that is getting rendered. 323 | 324 | When we create the actual `view` function, we map `viewCounter` over all of our counters and create add and remove buttons that report to the `address` directly. 325 | 326 | This ID trick can be used any time you want a dynamic number of subcomponents. Counters are very simple, but the pattern would work exactly the same if you had a list of user profiles or tweets or newsfeed items or product details. 327 | 328 | 329 | ## Example 4: A Fancier List of Counters 330 | 331 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/4.html) / [run locally](examples/4/)** 332 | 333 | Okay, keeping things simple and modular on a dynamic list of counters is pretty cool, but instead of a general remove button, what if each counter had its own specific remove button? Surely *that* will mess things up! 334 | 335 | Nah, it works. 336 | 337 | In this case our goals mean that we need a new way to view a `Counter` that adds a remove button. Interestingly, we can keep the `view` function from before and add a new `viewWithRemoveButton` function that provides a slightly different view of our underlying `Model`. This is pretty cool. We do not need to duplicate any code or do any crazy subtyping or overloading. We just add a new function to the public API to expose new functionality! 338 | 339 | ```elm 340 | module Counter (Model, init, Action, update, view, viewWithRemoveButton, Context) where 341 | 342 | ... 343 | 344 | type alias Context = 345 | { actions : Signal.Address Action 346 | , remove : Signal.Address () 347 | } 348 | 349 | viewWithRemoveButton : Context -> Model -> Html 350 | viewWithRemoveButton context model = 351 | div [] 352 | [ button [ onClick context.actions Decrement ] [ text "-" ] 353 | , div [ countStyle ] [ text (toString model) ] 354 | , button [ onClick context.actions Increment ] [ text "+" ] 355 | , div [ countStyle ] [] 356 | , button [ onClick context.remove () ] [ text "X" ] 357 | ] 358 | ``` 359 | 360 | The `viewWithRemoveButton` function adds one extra button. Notice that the increment/decrement buttons send messages to the `actions` address but the delete button sends messages to the `remove` address. The messages we send along to `remove` are essentially saying, “hey, whoever owns me, remove me!” It is up to whoever owns this particular counter to do the removing. 361 | 362 | Now that we have our new `viewWithRemoveButton`, we can create a `CounterList` module which puts all the individual counters together. The `Model` is the same as in example 3: a list of counters and a unique ID. 363 | 364 | ```elm 365 | type alias Model = 366 | { counters : List ( ID, Counter.Model ) 367 | , nextID : ID 368 | } 369 | 370 | type alias ID = Int 371 | ``` 372 | 373 | Our set of actions is a bit different. Instead of removing any old counter, we want to remove a specific one, so the `Remove` case now holds an ID. 374 | 375 | ```elm 376 | type Action 377 | = Insert 378 | | Remove ID 379 | | Modify ID Counter.Action 380 | ``` 381 | 382 | The `update` function is pretty similar to example 3 as well. 383 | 384 | ```elm 385 | update : Action -> Model -> Model 386 | update action model = 387 | case action of 388 | Insert -> 389 | { model | 390 | counters <- ( model.nextID, Counter.init 0 ) :: model.counters, 391 | nextID <- model.nextID + 1 392 | } 393 | 394 | Remove id -> 395 | { model | 396 | counters <- List.filter (\(counterID, _) -> counterID /= id) model.counters 397 | } 398 | 399 | Modify id counterAction -> 400 | let updateCounter (counterID, counterModel) = 401 | if counterID == id 402 | then (counterID, Counter.update counterAction counterModel) 403 | else (counterID, counterModel) 404 | in 405 | { model | counters <- List.map updateCounter model.counters } 406 | ``` 407 | 408 | In the case of `Remove`, we take out the counter that has the ID we are supposed to remove. Otherwise, the cases are quite close to how they were before. 409 | 410 | Finally, we put it all together in the `view`: 411 | 412 | ```elm 413 | view : Signal.Address Action -> Model -> Html 414 | view address model = 415 | let insert = button [ onClick address Insert ] [ text "Add" ] 416 | in 417 | div [] (insert :: List.map (viewCounter address) model.counters) 418 | 419 | viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html 420 | viewCounter address (id, model) = 421 | let context = 422 | Counter.Context 423 | (Signal.forwardTo address (Modify id)) 424 | (Signal.forwardTo address (always (Remove id))) 425 | in 426 | Counter.viewWithRemoveButton context model 427 | ``` 428 | 429 | In our `viewCounter` function, we construct the `Counter.Context` to pass in all the necessary forwarding addresses. In both cases we annotate each `Counter.Action` so that we know which counter to modify or remove. 430 | 431 | 432 | ## Big Lessons So Far 433 | 434 | **Basic Pattern** — Everything is built around a `Model`, a way to `update` that model, and a way to `view` that model. Everything is a variation on this basic pattern. 435 | 436 | **Nesting Modules** — Forwarding addresses makes it easy to nest our basic pattern, hiding implementation details entirely. We can nest this pattern arbitrarily deep, and each level only needs to know about what is going on one level lower. 437 | 438 | **Adding Context** — Sometimes to `update` or `view` our model, extra information is needed. We can always add some `Context` to these functions and pass in all the additional information we need without complicating our `Model`. 439 | 440 | ```elm 441 | update : Context -> Action -> Model -> Model 442 | view : Context' -> Model -> Html 443 | ``` 444 | 445 | At every level of nesting we can derive the specific `Context` needed for each submodule. 446 | 447 | **Testing is Easy** — All of the functions we have created are [pure functions][pure]. That makes it extremely easy to test your `update` function. There is no special initialization or mocking or configuration step, you just call the function with the arguments you would like to test. 448 | 449 | [pure]: http://en.wikipedia.org/wiki/Pure_function 450 | 451 | 452 | ## Example 5: Random GIF Viewer 453 | 454 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/5.html) / [run locally](examples/5/)** 455 | 456 | So we have covered how to create infinitely nestable components, but what happens when we want to do an HTTP request from somewhere in there? Or talk to a database? This example starts using [the `elm-effects` package][fx] to create a simple component that fetches random gifs from giphy.com with the topic “funny cats”. 457 | 458 | [fx]: http://package.elm-lang.org/packages/evancz/elm-effects/latest 459 | 460 | As you look through [the implementation](examples/5/RandomGif.elm), notice that it is pretty much the same code as the counter in example 1. The `Model` is very typical: 461 | 462 | ```elm 463 | type alias Model = 464 | { topic : String 465 | , gifUrl : String 466 | } 467 | ``` 468 | 469 | We need to know what the `topic` of the finder is and what `gifUrl` we are showing right this second. The only new thing in this example is that `init` and `update` have slightly fancier types: 470 | 471 | ```elm 472 | init : String -> (Model, Effects Action) 473 | 474 | update : Action -> Model -> (Model, Effects Action) 475 | ``` 476 | 477 | Instead of returning just a new `Model` we also give back some effects that we would like to run. So we will be using [the `Effects` API][fx_api], which looks something like this: 478 | 479 | [fx_api]: http://package.elm-lang.org/packages/evancz/elm-effects/latest/Effects 480 | 481 | ```elm 482 | module Effects where 483 | 484 | type Effects a 485 | 486 | none : Effects a 487 | -- don't do anything 488 | 489 | task : Task Never a -> Effects a 490 | -- request a task, do HTTP and database stuff 491 | ``` 492 | 493 | The `Effects` type is essentially a data structure holding a bunch of independent tasks that will get run at some later point. Let’s get a better feeling of how this works by checking out how `update` works in this example: 494 | 495 | ```elm 496 | type Action 497 | = RequestMore 498 | | NewGif (Maybe String) 499 | 500 | 501 | update : Action -> Model -> (Model, Effects Action) 502 | update msg model = 503 | case msg of 504 | RequestMore -> 505 | ( model 506 | , getRandomGif model.topic 507 | ) 508 | 509 | NewGif maybeUrl -> 510 | ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) 511 | , Effects.none 512 | ) 513 | 514 | -- getRandomGif : String -> Effects Action 515 | ``` 516 | 517 | So the user can trigger a `RequestMore` action by clicking the “More Please!” button, and when the server responds it will give us a `NewGif` action. We handle both these scenarios in our `update` function. 518 | 519 | In the case of `RequestMore` first return the existing model. The user just clicked a button, there is nothing to change right now. We also create an `Effects Action` using the `getRandomGif` function. We will get to how `getRandomGif` is defined soon. For now we just need to know that when an `Effects Action` is run, it will produce a bunch of `Action` values that will be routed throughout the application. So `getRandomGif model.topic` will eventually result in an action like this: 520 | 521 | ```elm 522 | NewGif (Just "http://s3.amazonaws.com/giphygifs/media/ka1aeBvFCSLD2/giphy.gif") 523 | ``` 524 | 525 | It returns a `Maybe` because the request to the server may fail. That `Action` will get fed right back into our `update` function. So when we take the `NewGif` route we just update the current `gifUrl` if possible. If the request failed, we just stick with the current `model.gifUrl`. 526 | 527 | We see the same kind of thing happening in `init` which defines the initial model and asks for a GIF in the correct topic from giphy.com’s API. 528 | 529 | ```elm 530 | init : String -> (Model, Effects Action) 531 | init topic = 532 | ( Model topic "assets/waiting.gif" 533 | , getRandomGif topic 534 | ) 535 | 536 | -- getRandomGif : String -> Effects Action 537 | ``` 538 | 539 | Again, when the random GIF effect is complete, it will produce an `Action` that gets routed to our `update` function. 540 | 541 | > **Note:** So far we have been using the `StartApp.Simple` module from [the start-app package](http://package.elm-lang.org/packages/evancz/start-app/latest), but now upgrade to the `StartApp` module. It is able to handle the complexity of more realistic web apps. It has [a slightly fancier API](http://package.elm-lang.org/packages/evancz/start-app/latest/StartApp). The crucial change is that it can handle our new `init` and `update` types. 542 | 543 | One of the crucial aspects of this example is the `getRandomGif` function that actually describes how to get a random GIF. It uses [tasks][] and [the `Http` package][http], and I will try to give an overview of how these things are being used as we go. Let’s look at the definition: 544 | 545 | [tasks]: http://elm-lang.org/guide/reactivity#tasks 546 | [http]: http://package.elm-lang.org/packages/evancz/elm-http/latest 547 | 548 | ```elm 549 | getRandomGif : String -> Effects Action 550 | getRandomGif topic = 551 | Http.get decodeImageUrl (randomUrl topic) 552 | |> Task.toMaybe 553 | |> Task.map NewGif 554 | |> Effects.task 555 | 556 | -- The first line there created an HTTP GET request. It tries to 557 | -- get some JSON at `randomUrl topic` and decodes the result 558 | -- with `decodeImage`. Both are defined below! 559 | -- 560 | -- Next we use `Task.toMaybe` to capture any potential failures and 561 | -- apply the `NewGif` tag to turn the result into a `Action`. 562 | -- Finally we turn it into an `Effects` value that can be used in our 563 | -- `init` or `update` functions. 564 | 565 | 566 | -- Given a topic, construct a URL for the giphy API. 567 | randomUrl : String -> String 568 | randomUrl topic = 569 | Http.url "http://api.giphy.com/v1/gifs/random" 570 | [ "api_key" => "dc6zaTOxFJmzC" 571 | , "tag" => topic 572 | ] 573 | 574 | 575 | -- A JSON decoder that takes a big chunk of JSON spit out by 576 | -- giphy and extracts the string at `json.data.image_url` 577 | decodeImageUrl : Json.Decoder String 578 | decodeImageUrl = 579 | Json.at ["data", "image_url"] Json.string 580 | ``` 581 | 582 | Once we have written this up, we are able to reuse `getRandomGif` in our `init` and `update` functions. 583 | 584 | One of the interesting things about the task returned by `getRandomGif` is that it can `Never` fail. The idea is that any potential failure *must* be handled explicitly. We do not want any tasks failing silently. 585 | 586 | I am going to try to explain exactly how that works, but it is not crucial to get every piece of this to use things! Okay, so every `Task` has a failure type and a success type. For example, an HTTP task may have a type like this `Task Http.Error String` such that we can fail with an `Http.Error` or succeed with a `String`. This makes it nice to chain a bunch of tasks together without worrying too much about errors. Now lets say our component requests a task, but the task fails. What happens then? Who gets notified? How do we recover? By making the failure type `Never` we force any potential errors into the success type such that they can be handled explicitly by the component. In our case, we use `Task.toMaybe : Task x a -> Task y (Maybe a)` so our `update` function must explicitly handle HTTP failures. This means tasks cannot silently fail, you always handle potential errors explicitly. 587 | 588 | 589 | ## Example 6: Pair of random GIF viewers 590 | 591 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/6.html) / [run locally](examples/6/)** 592 | 593 | Alright, effects can be done, but what about *nested* effects? Did you think about that?! This example reuses the exact code from the GIF viewer in example 5 to create a pair of independent GIF viewers. 594 | 595 | As you look through [the implementation](examples/6/RandomGifPair.elm), notice that it is pretty much the same code as the pair of counters in example 2. The `Model` is defined as two `RandomGif.Model` values: 596 | 597 | ```elm 598 | type alias Model = 599 | { left : RandomGif.Model 600 | , right : RandomGif.Model 601 | } 602 | ``` 603 | 604 | This lets us keep track of each independently. Our actions are just routing messages to the appropriate subcomponent. 605 | 606 | ```elm 607 | type Action 608 | = Left RandomGif.Action 609 | | Right RandomGif.Action 610 | ``` 611 | 612 | The interesting thing is that we actually use the `Left` and `Right` tags a bit in our `update` and `init` functions. 613 | 614 | ```elm 615 | -- Effects.map : (a -> b) -> Effects a -> Effects b 616 | 617 | update : Action -> Model -> (Model, Effects Action) 618 | update action model = 619 | case action of 620 | Left msg -> 621 | let 622 | (left, fx) = RandomGif.update msg model.left 623 | in 624 | ( Model left model.right 625 | , Effects.map Left fx 626 | ) 627 | 628 | Right msg -> 629 | let 630 | (right, fx) = RandomGif.update msg model.right 631 | in 632 | ( Model model.left right 633 | , Effects.map Right fx 634 | ) 635 | ``` 636 | 637 | So in each branch we call the `RandomGif.update` function which is returning a new model and some effects we are calling `fx`. We return an updated model like normal, but we need to do some extra work on our effects. Instead of returning them directly, we use [`Effects.map`](http://package.elm-lang.org/packages/evancz/elm-effects/latest/Effects#map) function to turn them into the same kind of `Action`. This works very much like `Signal.forwardTo`, letting us tag the values to make it clear how they should be routed. 638 | 639 | The same thing happens in the `init` function. We provide a topic for each random GIF viewer and get back an initial model and some effects. 640 | 641 | ```elm 642 | init : String -> String -> (Model, Effects Action) 643 | init leftTopic rightTopic = 644 | let 645 | (left, leftFx) = RandomGif.init leftTopic 646 | (right, rightFx) = RandomGif.init rightTopic 647 | in 648 | ( Model left right 649 | , Effects.batch 650 | [ Effects.map Left leftFx 651 | , Effects.map Right rightFx 652 | ] 653 | ) 654 | 655 | -- Effects.batch : List (Effects a) -> Effects a 656 | ``` 657 | 658 | In this case we not only use `Effects.map` to tag results appropriately, we also use the [`Effects.batch`](http://package.elm-lang.org/packages/evancz/elm-effects/latest/Effects#batch) function to lump them all together. All of the requested tasks will get spawned off and run independently, so the left and right effects will be in progress at the same time. 659 | 660 | 661 | ## Example 7: List of random GIF viewers 662 | 663 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/7.html) / [run locally](examples/7/)** 664 | 665 | This example lets you have a list of random GIF viewers where you can create the topics yourself. Again, we reuse the core `RandomGif` module exactly as is. 666 | 667 | When you look through [the implementation](examples/7/RandomGifList.elm) you will see that it exactly corresponds to example 3. We put all of our submodels in a list associated with an ID and do our operations based on those IDs. The only thing new is that we are using `Effects` in the `init` and `update` function, putting them together with `Effects.map` and `Effects.batch`. 668 | 669 | Please open an issue if this section should go into more detail about how things work! 670 | 671 | 672 | ## Example 8: Animation 673 | 674 | **[demo](http://evancz.github.io/elm-architecture-tutorial/examples/8.html) / [run locally](examples/8/)** 675 | 676 | Now we have seen components with tasks that can be nested in arbitrary ways, but how does it work for animation? 677 | 678 | Interestingly, it is pretty much exactly the same! (Or perhaps it is no longer surprising that the same pattern as in all the other examples works here too... Seems like a pretty good pattern!) 679 | 680 | This example is a pair of clickable squares. When you click a square, it rotates 90 degrees. Overall the code is an adapted form of example 2 and example 6 where we keep all the logic for animation in `SpinSquare.elm` which we then reuse multiple times in `SpinSquarePair.elm`. 681 | 682 | So all the new and interesting stuff is happening [in `SpinSquare`](examples/8/SpinSquare.elm), so we are going to focus on that code. The first thing we need is a model: 683 | 684 | ```elm 685 | type alias Model = 686 | { angle : Float 687 | , animationState : AnimationState 688 | } 689 | 690 | 691 | type alias AnimationState = 692 | Maybe { prevClockTime : Time, elapsedTime: Time } 693 | 694 | 695 | rotateStep = 90 696 | duration = second 697 | ``` 698 | 699 | So our core model is the `angle` that the square is currently at and then some `animationState` to track what is going on with any ongoing animation. If there is no animation it is `Nothing`, but if something is happening it holds: 700 | 701 | * `prevClockTime` — The most recent clock time which we will use for calculating time diffs. It will help us know exactly how many milliseconds have passed since last frame. 702 | * `elapsedTime` — A number between 0 and `duration` that tells us how far we are in the animation. 703 | 704 | The `rotateStep` constant is just declaring how far it turns on each click. You can mess with that and everything should keep working. 705 | 706 | Now the interesting stuff all happens in `update`: 707 | 708 | ```elm 709 | type Action 710 | = Spin 711 | | Tick Time 712 | 713 | 714 | update : Action -> Model -> (Model, Effects Action) 715 | update msg model = 716 | case msg of 717 | Spin -> 718 | case model.animationState of 719 | Nothing -> 720 | ( model, Effects.tick Tick ) 721 | 722 | Just _ -> 723 | ( model, Effects.none ) 724 | 725 | Tick clockTime -> 726 | let 727 | newElapsedTime = 728 | case model.animationState of 729 | Nothing -> 730 | 0 731 | 732 | Just {elapsedTime, prevClockTime} -> 733 | elapsedTime + (clockTime - prevClockTime) 734 | in 735 | if newElapsedTime > duration then 736 | ( { angle = model.angle + rotateStep 737 | , animationState = Nothing 738 | } 739 | , Effects.none 740 | ) 741 | else 742 | ( { angle = model.angle 743 | , animationState = Just { elapsedTime = newElapsedTime, prevClockTime = clockTime } 744 | } 745 | , Effects.tick Tick 746 | ) 747 | ``` 748 | 749 | There are two kinds of `Action` we need to handle: 750 | 751 | - `Spin` indicates that a user clicked the shape, requesting a spin. So in the `update` function, we request a clock tick if there is no animation going and just let things stay as is if one is already going. 752 | - `Tick` indicates that we have gotten a clock tick so we need to take an animation step. In the `update` function this means we need to update our `animationState`. So first we check if there is an animation in progress. If so, we just figure out what the `newElapsedTime` is by taking the current `elapsedTime` and adding a time diff to it. If the now elapsed time is greater than `duration` we stop animating and stop requesting new clock ticks. Otherwise we update the animation state and request another clock tick. 753 | 754 | Again, I think we can cut this code down as we write more code like this and start seeing the general pattern. Should be exciting to find! 755 | 756 | Finally we have a somewhat interesting `view` function! This example gets a nice bouncy animation, but we are just incrementing our `elapsedTime` in linear chunks. How is that happening? 757 | 758 | The `view` code itself is totally standard [`elm-svg`](http://package.elm-lang.org/packages/evancz/elm-svg/latest/) to make some fancier clickable shapes. The cool part of the view code is `toOffset` which calculates the rotation offset for the current `AnimationState`. 759 | 760 | ```elm 761 | -- import Easing exposing (ease, easeOutBounce, float) 762 | 763 | toOffset : AnimationState -> Float 764 | toOffset animationState = 765 | case animationState of 766 | Nothing -> 767 | 0 768 | 769 | Just {elapsedTime} -> 770 | ease easeOutBounce float 0 rotateStep duration elapsedTime 771 | ``` 772 | 773 | We are using the [@Dandandan](https://github.com/Dandandan)’s [easing package](http://package.elm-lang.org/packages/Dandandan/Easing/latest) which makes it to do [all sorts of cool easings](http://easings.net/) on numbers, colors, points, and any other crazy thing you want. 774 | 775 | So the `ease` function is taking a number between 0 and `duration`. It then turns that into a number between 0 and `rotateStep` which we set to 90 degrees up at the top of our program. You also provide an easing. In our case we gave `easeOutBounce` which means as we slide from 0 to `duration`, we will get a number between 0 and 90 with that easing added. Pretty crazy! Try swapping `easeOutBounce` out for [other easings](http://package.elm-lang.org/packages/Dandandan/Easing/latest/Easing) and see how it looks! 776 | 777 | From here, we wire everything together in `SpinSquarePair`, but that code is pretty much exactly the same as in example 2 and example 6. 778 | 779 | Okay, so that is the basics of doing animation with this library! It is not clear if we nailed everything here, so let us know how things go as you get more experience. Hopefully we can make it even easier! 780 | 781 | > **Note:** I expect we can build some abstractions on top of the core ideas here. This example does some lower level stuff, but I bet we can find some nice patterns to make this easier as we work with it more. If you find it weird now, try to make something better and tell us about it! 782 | --------------------------------------------------------------------------------