├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── examples ├── 01-button.elm ├── 02-field.elm ├── 03-form.elm ├── 04-maybe.elm ├── 05-http.elm ├── 06-json.elm ├── 07-random.elm └── 08-time.elm └── src └── Main.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016, 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm 2 | 3 | [Elm](https://elm-lang.org/) is a programming language that compiles to JavaScript. It is known for its friendly error messages, helping you find issues quickly and refactor large projects with confidence. Elm is also [very fast](https://elm-lang.org/blog/blazing-fast-html-round-two) and [very small](https://elm-lang.org/blog/small-assets-without-the-headache) when compared with React, Angular, Ember, etc. 4 | 5 | This repo focuses on **The Elm Architecture**, an architecture pattern you see in all Elm programs. It has influenced projects like Redux that borrow core concepts but add many JS-focused ideas. 6 | 7 | 8 | ## The Elm Architecture 9 | 10 | The Elm Architecture is a simple pattern for architecting webapps. The core idea is that your code is built around a `Model` of your application state, a way to `update` your model, and a way to `view` your model. 11 | 12 | To learn more about this, read the [the official guide][guide] and check out [this section][arch] which is all about The Elm Architecture. This repo is a collection of all the examples in that section, so you can follow along and compile things on your computer as you read through. 13 | 14 | [guide]: https://guide.elm-lang.org/ 15 | [arch]: https://guide.elm-lang.org/architecture/ 16 | 17 | 18 | ## Run The Examples 19 | 20 | After you [install](https://guide.elm-lang.org/install.html), run the following commands in your terminal to download this repo and start a server that compiles Elm for you: 21 | 22 | ```bash 23 | git clone https://github.com/evancz/elm-architecture-tutorial.git 24 | cd elm-architecture-tutorial 25 | elm reactor 26 | ``` 27 | 28 | Now go to [http://localhost:8000/](http://localhost:8000/) and start looking at the `examples/` directory. When you edit an Elm file, just refresh the corresponding page in your browser and it will recompile! 29 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.4", 11 | "elm/html": "1.0.0", 12 | "elm/http": "2.0.0", 13 | "elm/json": "1.1.3", 14 | "elm/random": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0" 17 | }, 18 | "indirect": { 19 | "elm/bytes": "1.0.8", 20 | "elm/file": "1.0.5", 21 | "elm/virtual-dom": "1.0.2" 22 | } 23 | }, 24 | "test-dependencies": { 25 | "direct": {}, 26 | "indirect": {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/01-button.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (Html, button, div, text) 3 | import Html.Events exposing (onClick) 4 | 5 | 6 | 7 | -- MAIN 8 | 9 | 10 | main = 11 | Browser.sandbox { init = init, update = update, view = view } 12 | 13 | 14 | 15 | -- MODEL 16 | 17 | 18 | type alias Model = Int 19 | 20 | 21 | init : Model 22 | init = 23 | 0 24 | 25 | 26 | 27 | -- UPDATE 28 | 29 | 30 | type Msg 31 | = Increment 32 | | Decrement 33 | 34 | 35 | update : Msg -> Model -> Model 36 | update msg model = 37 | case msg of 38 | Increment -> 39 | model + 1 40 | 41 | Decrement -> 42 | model - 1 43 | 44 | 45 | 46 | -- VIEW 47 | 48 | 49 | view : Model -> Html Msg 50 | view model = 51 | div [] 52 | [ button [ onClick Decrement ] [ text "-" ] 53 | , div [] [ text (String.fromInt model) ] 54 | , button [ onClick Increment ] [ text "+" ] 55 | ] 56 | -------------------------------------------------------------------------------- /examples/02-field.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (Html, Attribute, div, input, text) 3 | import Html.Attributes exposing (..) 4 | import Html.Events exposing (onInput) 5 | 6 | 7 | 8 | -- MAIN 9 | 10 | 11 | main = 12 | Browser.sandbox { init = init, update = update, view = view } 13 | 14 | 15 | 16 | -- MODEL 17 | 18 | 19 | type alias Model = 20 | { content : String 21 | } 22 | 23 | 24 | init : Model 25 | init = 26 | { content = "" } 27 | 28 | 29 | 30 | -- UPDATE 31 | 32 | 33 | type Msg 34 | = Change String 35 | 36 | 37 | update : Msg -> Model -> Model 38 | update msg model = 39 | case msg of 40 | Change newContent -> 41 | { model | content = newContent } 42 | 43 | 44 | 45 | -- VIEW 46 | 47 | 48 | view : Model -> Html Msg 49 | view model = 50 | div [] 51 | [ input [ placeholder "Text to reverse", value model.content, onInput Change ] [] 52 | , div [] [ text (String.reverse model.content) ] 53 | ] 54 | -------------------------------------------------------------------------------- /examples/03-form.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (..) 3 | import Html.Attributes exposing (..) 4 | import Html.Events exposing (onInput) 5 | 6 | 7 | 8 | -- MAIN 9 | 10 | 11 | main = 12 | Browser.sandbox { init = init, update = update, view = view } 13 | 14 | 15 | 16 | -- MODEL 17 | 18 | 19 | type alias Model = 20 | { name : String 21 | , password : String 22 | , passwordAgain : String 23 | } 24 | 25 | 26 | init : Model 27 | init = 28 | Model "" "" "" 29 | 30 | 31 | 32 | -- UPDATE 33 | 34 | 35 | type Msg 36 | = Name String 37 | | Password String 38 | | PasswordAgain String 39 | 40 | 41 | update : Msg -> Model -> Model 42 | update msg model = 43 | case msg of 44 | Name name -> 45 | { model | name = name } 46 | 47 | Password password -> 48 | { model | password = password } 49 | 50 | PasswordAgain password -> 51 | { model | passwordAgain = password } 52 | 53 | 54 | 55 | -- VIEW 56 | 57 | 58 | view : Model -> Html Msg 59 | view model = 60 | div [] 61 | [ viewInput "text" "Name" model.name Name 62 | , viewInput "password" "Password" model.password Password 63 | , viewInput "password" "Re-enter Password" model.passwordAgain PasswordAgain 64 | , viewValidation model 65 | ] 66 | 67 | 68 | viewInput : String -> String -> String -> (String -> msg) -> Html msg 69 | viewInput t p v toMsg = 70 | input [ type_ t, placeholder p, value v, onInput toMsg ] [] 71 | 72 | 73 | viewValidation : Model -> Html msg 74 | viewValidation model = 75 | if model.password == model.passwordAgain then 76 | div [ style "color" "green" ] [ text "OK" ] 77 | else 78 | div [ style "color" "red" ] [ text "Passwords do not match!" ] 79 | -------------------------------------------------------------------------------- /examples/04-maybe.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (Html, Attribute, span, input, text) 3 | import Html.Attributes exposing (..) 4 | import Html.Events exposing (onInput) 5 | 6 | 7 | 8 | -- MAIN 9 | 10 | 11 | main = 12 | Browser.sandbox { init = init, update = update, view = view } 13 | 14 | 15 | 16 | -- MODEL 17 | 18 | 19 | type alias Model = 20 | { input : String 21 | } 22 | 23 | 24 | init : Model 25 | init = 26 | { input = "" } 27 | 28 | 29 | 30 | -- UPDATE 31 | 32 | 33 | type Msg 34 | = Change String 35 | 36 | 37 | update : Msg -> Model -> Model 38 | update msg model = 39 | case msg of 40 | Change newInput -> 41 | { model | input = newInput } 42 | 43 | 44 | 45 | -- VIEW 46 | 47 | 48 | view : Model -> Html Msg 49 | view model = 50 | case String.toFloat model.input of 51 | Just celsius -> 52 | viewConverter model.input "blue" (String.fromFloat (celsius * 1.8 + 32)) 53 | 54 | Nothing -> 55 | viewConverter model.input "red" "???" 56 | 57 | 58 | viewConverter : String -> String -> String -> Html Msg 59 | viewConverter userInput color equivalentTemp = 60 | span [] 61 | [ input [ value userInput, onInput Change, style "width" "40px" ] [] 62 | , text "°C = " 63 | , span [ style "color" color ] [ text equivalentTemp ] 64 | , text "°F" 65 | ] 66 | -------------------------------------------------------------------------------- /examples/05-http.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (Html, text, pre) 3 | import Http 4 | 5 | 6 | 7 | -- MAIN 8 | 9 | 10 | main = 11 | Browser.element 12 | { init = init 13 | , update = update 14 | , subscriptions = subscriptions 15 | , view = view 16 | } 17 | 18 | 19 | 20 | -- MODEL 21 | 22 | 23 | type Model 24 | = Failure 25 | | Loading 26 | | Success String 27 | 28 | 29 | init : () -> (Model, Cmd Msg) 30 | init _ = 31 | ( Loading 32 | , Http.get 33 | { url = "https://elm-lang.org/assets/public-opinion.txt" 34 | , expect = Http.expectString GotText 35 | } 36 | ) 37 | 38 | 39 | 40 | -- UPDATE 41 | 42 | 43 | type Msg 44 | = GotText (Result Http.Error String) 45 | 46 | 47 | update : Msg -> Model -> (Model, Cmd Msg) 48 | update msg model = 49 | case msg of 50 | GotText result -> 51 | case result of 52 | Ok fullText -> 53 | (Success fullText, Cmd.none) 54 | 55 | Err _ -> 56 | (Failure, Cmd.none) 57 | 58 | 59 | 60 | -- SUBSCRIPTIONS 61 | 62 | 63 | subscriptions : Model -> Sub Msg 64 | subscriptions model = 65 | Sub.none 66 | 67 | 68 | 69 | -- VIEW 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | case model of 75 | Failure -> 76 | text "I was unable to load your book." 77 | 78 | Loading -> 79 | text "Loading..." 80 | 81 | Success fullText -> 82 | pre [] [ text fullText ] 83 | -------------------------------------------------------------------------------- /examples/06-json.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (..) 3 | import Html.Attributes exposing (..) 4 | import Html.Events exposing (..) 5 | import Http 6 | import Json.Decode exposing (Decoder, field, string) 7 | 8 | 9 | 10 | -- MAIN 11 | 12 | 13 | main = 14 | Browser.element 15 | { init = init 16 | , update = update 17 | , subscriptions = subscriptions 18 | , view = view 19 | } 20 | 21 | 22 | 23 | -- MODEL 24 | 25 | 26 | type Model 27 | = Failure 28 | | Loading 29 | | Success String 30 | 31 | 32 | init : () -> (Model, Cmd Msg) 33 | init _ = 34 | (Loading, getRandomCatGif) 35 | 36 | 37 | 38 | -- UPDATE 39 | 40 | 41 | type Msg 42 | = MorePlease 43 | | GotGif (Result Http.Error String) 44 | 45 | 46 | update : Msg -> Model -> (Model, Cmd Msg) 47 | update msg model = 48 | case msg of 49 | MorePlease -> 50 | (Loading, getRandomCatGif) 51 | 52 | GotGif result -> 53 | case result of 54 | Ok url -> 55 | (Success url, Cmd.none) 56 | 57 | Err _ -> 58 | (Failure, Cmd.none) 59 | 60 | 61 | 62 | -- SUBSCRIPTIONS 63 | 64 | 65 | subscriptions : Model -> Sub Msg 66 | subscriptions model = 67 | Sub.none 68 | 69 | 70 | 71 | -- VIEW 72 | 73 | 74 | view : Model -> Html Msg 75 | view model = 76 | div [] 77 | [ h2 [] [ text "Random Cats" ] 78 | , viewGif model 79 | ] 80 | 81 | 82 | viewGif : Model -> Html Msg 83 | viewGif model = 84 | case model of 85 | Failure -> 86 | div [] 87 | [ text "I could not load a random cat for some reason. " 88 | , button [ onClick MorePlease ] [ text "Try Again!" ] 89 | ] 90 | 91 | Loading -> 92 | text "Loading..." 93 | 94 | Success url -> 95 | div [] 96 | [ button [ onClick MorePlease, style "display" "block" ] [ text "More Please!" ] 97 | , img [ src url ] [] 98 | ] 99 | 100 | 101 | 102 | -- HTTP 103 | 104 | 105 | getRandomCatGif : Cmd Msg 106 | getRandomCatGif = 107 | Http.get 108 | { url = "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cat" 109 | , expect = Http.expectJson GotGif gifDecoder 110 | } 111 | 112 | 113 | gifDecoder : Decoder String 114 | gifDecoder = 115 | field "data" (field "image_url" string) 116 | -------------------------------------------------------------------------------- /examples/07-random.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (..) 3 | import Html.Events exposing (..) 4 | import Random 5 | 6 | 7 | 8 | -- MAIN 9 | 10 | 11 | main = 12 | Browser.element 13 | { init = init 14 | , update = update 15 | , subscriptions = subscriptions 16 | , view = view 17 | } 18 | 19 | 20 | 21 | -- MODEL 22 | 23 | 24 | type alias Model = 25 | { dieFace : Int 26 | } 27 | 28 | 29 | init : () -> (Model, Cmd Msg) 30 | init _ = 31 | ( Model 1 32 | , Cmd.none 33 | ) 34 | 35 | 36 | 37 | -- UPDATE 38 | 39 | 40 | type Msg 41 | = Roll 42 | | NewFace Int 43 | 44 | 45 | update : Msg -> Model -> (Model, Cmd Msg) 46 | update msg model = 47 | case msg of 48 | Roll -> 49 | ( model 50 | , Random.generate NewFace (Random.int 1 6) 51 | ) 52 | 53 | NewFace newFace -> 54 | ( Model newFace 55 | , Cmd.none 56 | ) 57 | 58 | 59 | 60 | -- SUBSCRIPTIONS 61 | 62 | 63 | subscriptions : Model -> Sub Msg 64 | subscriptions model = 65 | Sub.none 66 | 67 | 68 | 69 | -- VIEW 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | div [] 75 | [ h1 [] [ text (String.fromInt model.dieFace) ] 76 | , button [ onClick Roll ] [ text "Roll" ] 77 | ] -------------------------------------------------------------------------------- /examples/08-time.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import Html exposing (..) 3 | import Task 4 | import Time 5 | 6 | 7 | 8 | -- MAIN 9 | 10 | 11 | main = 12 | Browser.element 13 | { init = init 14 | , view = view 15 | , update = update 16 | , subscriptions = subscriptions 17 | } 18 | 19 | 20 | 21 | -- MODEL 22 | 23 | 24 | type alias Model = 25 | { zone : Time.Zone 26 | , time : Time.Posix 27 | } 28 | 29 | 30 | init : () -> (Model, Cmd Msg) 31 | init _ = 32 | ( Model Time.utc (Time.millisToPosix 0) 33 | , Task.perform AdjustTimeZone Time.here 34 | ) 35 | 36 | 37 | 38 | -- UPDATE 39 | 40 | 41 | type Msg 42 | = Tick Time.Posix 43 | | AdjustTimeZone Time.Zone 44 | 45 | 46 | 47 | update : Msg -> Model -> (Model, Cmd Msg) 48 | update msg model = 49 | case msg of 50 | Tick newTime -> 51 | ( { model | time = newTime } 52 | , Cmd.none 53 | ) 54 | 55 | AdjustTimeZone newZone -> 56 | ( { model | zone = newZone } 57 | , Cmd.none 58 | ) 59 | 60 | 61 | 62 | -- SUBSCRIPTIONS 63 | 64 | 65 | subscriptions : Model -> Sub Msg 66 | subscriptions model = 67 | Time.every 1000 Tick 68 | 69 | 70 | 71 | -- VIEW 72 | 73 | 74 | view : Model -> Html Msg 75 | view model = 76 | let 77 | hour = String.fromInt (Time.toHour model.zone model.time) 78 | minute = String.fromInt (Time.toMinute model.zone model.time) 79 | second = String.fromInt (Time.toSecond model.zone model.time) 80 | in 81 | h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ] -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, button, div, text) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | 9 | -- MAIN 10 | 11 | 12 | main = 13 | Browser.sandbox { init = init, update = update, view = view } 14 | 15 | 16 | 17 | -- MODEL 18 | 19 | 20 | type alias Model = Int 21 | 22 | 23 | init : Model 24 | init = 25 | 0 26 | 27 | 28 | 29 | -- UPDATE 30 | 31 | 32 | type Msg 33 | = Increment 34 | | Decrement 35 | 36 | 37 | update : Msg -> Model -> Model 38 | update msg model = 39 | case msg of 40 | Increment -> 41 | model + 1 42 | 43 | Decrement -> 44 | model - 1 45 | 46 | 47 | 48 | -- VIEW 49 | 50 | 51 | view : Model -> Html Msg 52 | view model = 53 | div [] 54 | [ button [ onClick Decrement ] [ text "-" ] 55 | , div [] [ text (String.fromInt model) ] 56 | , button [ onClick Increment ] [ text "+" ] 57 | ] 58 | --------------------------------------------------------------------------------