├── .gitignore ├── elm-package.json ├── example ├── elm-package.json ├── README.md ├── index.html ├── LICENSE ├── style.css └── Todo.elm ├── LICENSE ├── README.md └── src └── Rocket.elm /.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | # elm-repl generated files 4 | repl-temp-* 5 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "A straightforward alternative to (!)", 4 | "repository": "https://github.com/NoRedInk/rocket-update.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": ["Rocket"], 10 | "dependencies": { 11 | "elm-lang/core": "5.0.0 <= v < 6.0.0" 12 | }, 13 | "elm-version": "0.18.0 <= v < 0.19.0" 14 | } 15 | -------------------------------------------------------------------------------- /example/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": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-lang/dom": "1.1.1 <= v < 2.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # [TodoMVC in Elm](https://github.com/evancz/elm-todomvc) - EXCEPT WITH ROCKETS 🚀🚀🚀 2 | 3 | All of the Elm code lives in `Todo.elm` and relies on the [elm-lang/html][html] library. 4 | 5 | [html]: http://package.elm-lang.org/packages/elm-lang/html/latest 6 | 7 | There also is a port handler set up in `index.html` to store the Elm application's state in `localStorage` on every update. 8 | 9 | 10 | ## Build Instructions 11 | 12 | Run the following command from the root of this project: 13 | 14 | ```bash 15 | elm-make Todo.elm --output elm.js 16 | ``` 17 | 18 | Then open `index.html` in your browser! 19 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017, 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, NoRedInk 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rocket-update 🚀 2 | 3 | This package provides a simpler alternative to the `(!)` operator. 4 | 5 | ## Tuple Rocketry 6 | 7 | The "rocket operator" `(=>)` does nothing more than return a tuple: 8 | 9 | ```elm 10 | ("display" => "none") == ( "display", "none" ) 11 | ``` 12 | 13 | ## Style Rocketry 14 | 15 | It looks kinda nice to use this for things like styles: 16 | 17 | ```elm 18 | button [ style [ "display" => "none" ] ] [ text "invisible!" ] 19 | ``` 20 | instead of 21 | 22 | ```elm 23 | button [ style [ ( "display", "none" ) ] ] [ text "invisible!" ] 24 | ``` 25 | 26 | ## Update Rocketry 27 | 28 | If you tweak `update` to return `( model, List (Cmd msg) )`, then you can use 29 | `(=>)` to serve the same purpose as `(!)`, like so: 30 | 31 | ```elm 32 | update : Msg -> Model -> ( Model, List (Cmd Msg) ) 33 | update msg model = 34 | case msg of 35 | Reset -> 36 | { model | stuff = newStuff } => [] 37 | 38 | SendRequest -> 39 | model => [ Http.send someRequest ] 40 | 41 | SendOtherRequest -> 42 | model 43 | |> doSomethingToModel 44 | |> doSomethingElseToModel 45 | => [ Http.send someOtherRequest ] 46 | ``` 47 | 48 | To get an `update` function of this type, just use the `Rocket.batchUpdate` function when calling `program` and you're all set. (There's a `batchInit` function for `init` too, so `update` and `init` can be consistent.) 49 | 50 | ```elm 51 | main : Program Never Model Msg 52 | main = 53 | Html.program 54 | { init = init >> Rocket.batchInit 55 | , update = update >> Rocket.batchUpdate 56 | , view = view 57 | , subscriptions = subscriptions 58 | } 59 | ``` 60 | 61 | ## That's it! 62 | 63 | In the `example/` folder you can find [`elm-todomvc`](https://github.com/evancz/elm-todomvc) revised to use this style. 64 | 65 | Enjoy! 🚀 66 | -------------------------------------------------------------------------------- /src/Rocket.elm: -------------------------------------------------------------------------------- 1 | module Rocket exposing ((=>), batchInit, batchUpdate) 2 | 3 | {-| This module contains the "rocket operator" `(=>)`, as well as functions 4 | to tweak `update` and `init` so that they work naturally with it. 5 | 6 | @docs (=>) 7 | 8 | @docs batchUpdate, batchInit 9 | -} 10 | 11 | 12 | {-| Returns a tuple. Lets you write: 13 | 14 | button [ style [ "display" => "none" ] ] [ text "Yo" ] 15 | 16 | instead of 17 | 18 | button [ style [ ( "display", "none" ) ] ] [ text "Yo" ] 19 | 20 | Also works great for `update` functions, especially those that return 21 | `( model, List (Cmd msg) )` like so: 22 | 23 | update : Msg -> Model -> ( Model, List (Cmd Msg) ) 24 | update msg model = 25 | case msg of 26 | Reset -> 27 | { model | stuff = newStuff } => [] 28 | 29 | SendRequest -> 30 | model => [ Http.send someRequest ] 31 | 32 | SendOtherRequest -> 33 | model 34 | |> doSomethingToModel 35 | |> doSomethingElseToModel 36 | => [ Http.send someOtherRequest ] 37 | 38 | See [`batchUpdate`](#batchUpdate) for how to obtain an `update` function like this. 39 | -} 40 | (=>) : a -> b -> ( a, b ) 41 | (=>) = 42 | (,) 43 | 44 | 45 | {-| infixl 0 means the (=>) operator has the same precedence as (<|) and (|>), 46 | meaning you can use it at the end of a pipeline and have the precedence work out. 47 | -} 48 | infixl 0 => 49 | 50 | 51 | {-| Use this with `program` to make `init` return `( model, List (Cmd msg) )` 52 | 53 | main : Program Never Model Msg 54 | main = 55 | Html.programWithFlags 56 | { init = init >> Rocket.batchInit 57 | , update = update >> Rocket.batchUpdate 58 | , view = view 59 | , subscriptions = subscriptions 60 | } 61 | -} 62 | batchInit : ( model, List (Cmd msg) ) -> ( model, Cmd msg ) 63 | batchInit = 64 | batchCommands 65 | 66 | 67 | {-| Use this with `program` to make `update` return `( model, List (Cmd msg) )` 68 | 69 | main : Program Never Model Msg 70 | main = 71 | Html.program 72 | { init = init >> Rocket.batchInit 73 | , update = update >> Rocket.batchUpdate 74 | , view = view 75 | , subscriptions = subscriptions 76 | } 77 | -} 78 | batchUpdate : (model -> ( model, List (Cmd msg) )) -> model -> ( model, Cmd msg ) 79 | batchUpdate fn = 80 | fn >> batchCommands 81 | 82 | 83 | {-| Used internally for batchInit and batchUpdate 84 | -} 85 | batchCommands : ( model, List (Cmd msg) ) -> ( model, Cmd msg ) 86 | batchCommands ( model, commands ) = 87 | model => Cmd.batch commands 88 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '❯'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml;utf8,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: '×'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li .edit { 246 | display: none; 247 | } 248 | 249 | .todo-list li.editing:last-child { 250 | margin-bottom: -1px; 251 | } 252 | 253 | .footer { 254 | color: #777; 255 | padding: 10px 15px; 256 | height: 20px; 257 | text-align: center; 258 | border-top: 1px solid #e6e6e6; 259 | } 260 | 261 | .footer:before { 262 | content: ''; 263 | position: absolute; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | height: 50px; 268 | overflow: hidden; 269 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 270 | 0 8px 0 -3px #f6f6f6, 271 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 272 | 0 16px 0 -6px #f6f6f6, 273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | .todo-count { 277 | float: left; 278 | text-align: left; 279 | } 280 | 281 | .todo-count strong { 282 | font-weight: 300; 283 | } 284 | 285 | .filters { 286 | margin: 0; 287 | padding: 0; 288 | list-style: none; 289 | position: absolute; 290 | right: 0; 291 | left: 0; 292 | } 293 | 294 | .filters li { 295 | display: inline; 296 | } 297 | 298 | .filters li a { 299 | color: inherit; 300 | margin: 3px; 301 | padding: 3px 7px; 302 | text-decoration: none; 303 | border: 1px solid transparent; 304 | border-radius: 3px; 305 | } 306 | 307 | .filters li a.selected, 308 | .filters li a:hover { 309 | border-color: rgba(175, 47, 47, 0.1); 310 | } 311 | 312 | .filters li a.selected { 313 | border-color: rgba(175, 47, 47, 0.2); 314 | } 315 | 316 | .clear-completed, 317 | html .clear-completed:active { 318 | float: right; 319 | position: relative; 320 | line-height: 20px; 321 | text-decoration: none; 322 | cursor: pointer; 323 | position: relative; 324 | } 325 | 326 | .clear-completed:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .info { 331 | margin: 65px auto 0; 332 | color: #bfbfbf; 333 | font-size: 10px; 334 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 335 | text-align: center; 336 | } 337 | 338 | .info p { 339 | line-height: 1; 340 | } 341 | 342 | .info a { 343 | color: inherit; 344 | text-decoration: none; 345 | font-weight: 400; 346 | } 347 | 348 | .info a:hover { 349 | text-decoration: underline; 350 | } 351 | 352 | /* 353 | Hack to remove background from Mobile Safari. 354 | Can't use it globally since it destroys checkboxes in Firefox 355 | */ 356 | @media screen and (-webkit-min-device-pixel-ratio:0) { 357 | .toggle-all, 358 | .todo-list li .toggle { 359 | background: none; 360 | } 361 | 362 | .todo-list li .toggle { 363 | height: 40px; 364 | } 365 | 366 | .toggle-all { 367 | -webkit-transform: rotate(90deg); 368 | transform: rotate(90deg); 369 | -webkit-appearance: none; 370 | appearance: none; 371 | } 372 | } 373 | 374 | @media (max-width: 430px) { 375 | .footer { 376 | height: 50px; 377 | } 378 | 379 | .filters { 380 | bottom: 10px; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /example/Todo.elm: -------------------------------------------------------------------------------- 1 | port module Todo exposing (..) 2 | 3 | {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering. 4 | 5 | This application is broken up into three key parts: 6 | 7 | 1. Model - a full definition of the application's state 8 | 2. Update - a way to step the application state forward 9 | 3. View - a way to visualize our application state with HTML 10 | 11 | This clean division of concerns is a core part of Elm. You can read more about 12 | this in 13 | -} 14 | 15 | import Dom 16 | import Html exposing (..) 17 | import Html.Attributes exposing (..) 18 | import Html.Events exposing (..) 19 | import Html.Keyed as Keyed 20 | import Html.Lazy exposing (lazy, lazy2) 21 | import Json.Decode as Json 22 | import String 23 | import Task 24 | import Rocket exposing (..) 25 | 26 | 27 | main : Program (Maybe Model) Model Msg 28 | main = 29 | Html.programWithFlags 30 | { init = init >> batchInit 31 | , view = view 32 | , update = updateWithStorage >> batchUpdate 33 | , subscriptions = \_ -> Sub.none 34 | } 35 | 36 | 37 | port setStorage : Model -> Cmd msg 38 | 39 | 40 | {-| We want to `setStorage` on every update. This function adds the setStorage 41 | command for every step of the update function. 42 | -} 43 | updateWithStorage : Msg -> Model -> ( Model, List (Cmd Msg) ) 44 | updateWithStorage msg model = 45 | let 46 | ( newModel, cmds ) = 47 | update msg model 48 | in 49 | newModel => [ setStorage newModel ] ++ cmds 50 | 51 | 52 | 53 | -- MODEL 54 | -- The full application state of our todo app. 55 | 56 | 57 | type alias Model = 58 | { entries : List Entry 59 | , field : String 60 | , uid : Int 61 | , visibility : String 62 | } 63 | 64 | 65 | type alias Entry = 66 | { description : String 67 | , completed : Bool 68 | , editing : Bool 69 | , id : Int 70 | } 71 | 72 | 73 | emptyModel : Model 74 | emptyModel = 75 | { entries = [] 76 | , visibility = "All" 77 | , field = "" 78 | , uid = 0 79 | } 80 | 81 | 82 | newEntry : String -> Int -> Entry 83 | newEntry desc id = 84 | { description = desc 85 | , completed = False 86 | , editing = False 87 | , id = id 88 | } 89 | 90 | 91 | init : Maybe Model -> ( Model, List (Cmd Msg) ) 92 | init savedModel = 93 | Maybe.withDefault emptyModel savedModel => [] 94 | 95 | 96 | 97 | -- UPDATE 98 | 99 | 100 | {-| Users of our app can trigger messages by clicking and typing. These 101 | messages are fed into the `update` function as they occur, letting us react 102 | to them. 103 | -} 104 | type Msg 105 | = NoOp 106 | | UpdateField String 107 | | EditingEntry Int Bool 108 | | UpdateEntry Int String 109 | | Add 110 | | Delete Int 111 | | DeleteComplete 112 | | Check Int Bool 113 | | CheckAll Bool 114 | | ChangeVisibility String 115 | 116 | 117 | 118 | -- How we update our Model on a given Msg? 119 | 120 | 121 | update : Msg -> Model -> ( Model, List (Cmd Msg) ) 122 | update msg model = 123 | case msg of 124 | NoOp -> 125 | model => [] 126 | 127 | Add -> 128 | { model 129 | | uid = model.uid + 1 130 | , field = "" 131 | , entries = 132 | if String.isEmpty model.field then 133 | model.entries 134 | else 135 | model.entries ++ [ newEntry model.field model.uid ] 136 | } 137 | => [] 138 | 139 | UpdateField str -> 140 | { model | field = str } 141 | => [] 142 | 143 | EditingEntry id isEditing -> 144 | let 145 | updateEntry t = 146 | if t.id == id then 147 | { t | editing = isEditing } 148 | else 149 | t 150 | 151 | focus = 152 | Dom.focus ("todo-" ++ toString id) 153 | in 154 | { model | entries = List.map updateEntry model.entries } 155 | => [ Task.attempt (\_ -> NoOp) focus ] 156 | 157 | UpdateEntry id task -> 158 | let 159 | updateEntry t = 160 | if t.id == id then 161 | { t | description = task } 162 | else 163 | t 164 | in 165 | { model | entries = List.map updateEntry model.entries } 166 | => [] 167 | 168 | Delete id -> 169 | { model | entries = List.filter (\t -> t.id /= id) model.entries } 170 | => [] 171 | 172 | DeleteComplete -> 173 | { model | entries = List.filter (not << .completed) model.entries } 174 | => [] 175 | 176 | Check id isCompleted -> 177 | let 178 | updateEntry t = 179 | if t.id == id then 180 | { t | completed = isCompleted } 181 | else 182 | t 183 | in 184 | { model | entries = List.map updateEntry model.entries } 185 | => [] 186 | 187 | CheckAll isCompleted -> 188 | let 189 | updateEntry t = 190 | { t | completed = isCompleted } 191 | in 192 | { model | entries = List.map updateEntry model.entries } 193 | => [] 194 | 195 | ChangeVisibility visibility -> 196 | { model | visibility = visibility } 197 | => [] 198 | 199 | 200 | 201 | -- VIEW 202 | 203 | 204 | view : Model -> Html Msg 205 | view model = 206 | div 207 | [ class "todomvc-wrapper" 208 | , style [ "visibility" => "hidden" ] 209 | ] 210 | [ section 211 | [ class "todoapp" ] 212 | [ lazy viewInput model.field 213 | , lazy2 viewEntries model.visibility model.entries 214 | , lazy2 viewControls model.visibility model.entries 215 | ] 216 | , infoFooter 217 | ] 218 | 219 | 220 | viewInput : String -> Html Msg 221 | viewInput task = 222 | header 223 | [ class "header" ] 224 | [ h1 [] [ text "todos" ] 225 | , input 226 | [ class "new-todo" 227 | , placeholder "What needs to be done?" 228 | , autofocus True 229 | , value task 230 | , name "newTodo" 231 | , onInput UpdateField 232 | , onEnter Add 233 | ] 234 | [] 235 | ] 236 | 237 | 238 | onEnter : Msg -> Attribute Msg 239 | onEnter msg = 240 | let 241 | isEnter code = 242 | if code == 13 then 243 | Json.succeed msg 244 | else 245 | Json.fail "not ENTER" 246 | in 247 | on "keydown" (Json.andThen isEnter keyCode) 248 | 249 | 250 | 251 | -- VIEW ALL ENTRIES 252 | 253 | 254 | viewEntries : String -> List Entry -> Html Msg 255 | viewEntries visibility entries = 256 | let 257 | isVisible todo = 258 | case visibility of 259 | "Completed" -> 260 | todo.completed 261 | 262 | "Active" -> 263 | not todo.completed 264 | 265 | _ -> 266 | True 267 | 268 | allCompleted = 269 | List.all .completed entries 270 | 271 | cssVisibility = 272 | if List.isEmpty entries then 273 | "hidden" 274 | else 275 | "visible" 276 | in 277 | section 278 | [ class "main" 279 | , style [ "visibility" => cssVisibility ] 280 | ] 281 | [ input 282 | [ class "toggle-all" 283 | , type_ "checkbox" 284 | , name "toggle" 285 | , checked allCompleted 286 | , onClick (CheckAll (not allCompleted)) 287 | ] 288 | [] 289 | , label 290 | [ for "toggle-all" ] 291 | [ text "Mark all as complete" ] 292 | , Keyed.ul [ class "todo-list" ] <| 293 | List.map viewKeyedEntry (List.filter isVisible entries) 294 | ] 295 | 296 | 297 | 298 | -- VIEW INDIVIDUAL ENTRIES 299 | 300 | 301 | viewKeyedEntry : Entry -> ( String, Html Msg ) 302 | viewKeyedEntry todo = 303 | ( toString todo.id, lazy viewEntry todo ) 304 | 305 | 306 | viewEntry : Entry -> Html Msg 307 | viewEntry todo = 308 | li 309 | [ classList [ ( "completed", todo.completed ), ( "editing", todo.editing ) ] ] 310 | [ div 311 | [ class "view" ] 312 | [ input 313 | [ class "toggle" 314 | , type_ "checkbox" 315 | , checked todo.completed 316 | , onClick (Check todo.id (not todo.completed)) 317 | ] 318 | [] 319 | , label 320 | [ onDoubleClick (EditingEntry todo.id True) ] 321 | [ text todo.description ] 322 | , button 323 | [ class "destroy" 324 | , onClick (Delete todo.id) 325 | ] 326 | [] 327 | ] 328 | , input 329 | [ class "edit" 330 | , value todo.description 331 | , name "title" 332 | , id ("todo-" ++ toString todo.id) 333 | , onInput (UpdateEntry todo.id) 334 | , onBlur (EditingEntry todo.id False) 335 | , onEnter (EditingEntry todo.id False) 336 | ] 337 | [] 338 | ] 339 | 340 | 341 | 342 | -- VIEW CONTROLS AND FOOTER 343 | 344 | 345 | viewControls : String -> List Entry -> Html Msg 346 | viewControls visibility entries = 347 | let 348 | entriesCompleted = 349 | List.length (List.filter .completed entries) 350 | 351 | entriesLeft = 352 | List.length entries - entriesCompleted 353 | in 354 | footer 355 | [ class "footer" 356 | , hidden (List.isEmpty entries) 357 | ] 358 | [ lazy viewControlsCount entriesLeft 359 | , lazy viewControlsFilters visibility 360 | , lazy viewControlsClear entriesCompleted 361 | ] 362 | 363 | 364 | viewControlsCount : Int -> Html Msg 365 | viewControlsCount entriesLeft = 366 | let 367 | item_ = 368 | if entriesLeft == 1 then 369 | " item" 370 | else 371 | " items" 372 | in 373 | span 374 | [ class "todo-count" ] 375 | [ strong [] [ text (toString entriesLeft) ] 376 | , text (item_ ++ " left") 377 | ] 378 | 379 | 380 | viewControlsFilters : String -> Html Msg 381 | viewControlsFilters visibility = 382 | ul 383 | [ class "filters" ] 384 | [ visibilitySwap "#/" "All" visibility 385 | , text " " 386 | , visibilitySwap "#/active" "Active" visibility 387 | , text " " 388 | , visibilitySwap "#/completed" "Completed" visibility 389 | ] 390 | 391 | 392 | visibilitySwap : String -> String -> String -> Html Msg 393 | visibilitySwap uri visibility actualVisibility = 394 | li 395 | [ onClick (ChangeVisibility visibility) ] 396 | [ a [ href uri, classList [ ( "selected", visibility == actualVisibility ) ] ] 397 | [ text visibility ] 398 | ] 399 | 400 | 401 | viewControlsClear : Int -> Html Msg 402 | viewControlsClear entriesCompleted = 403 | button 404 | [ class "clear-completed" 405 | , hidden (entriesCompleted == 0) 406 | , onClick DeleteComplete 407 | ] 408 | [ text ("Clear completed (" ++ toString entriesCompleted ++ ")") 409 | ] 410 | 411 | 412 | infoFooter : Html msg 413 | infoFooter = 414 | footer [ class "info" ] 415 | [ p [] [ text "Double-click to edit a todo" ] 416 | , p [] 417 | [ text "Written by " 418 | , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] 419 | ] 420 | , p [] 421 | [ text "Part of " 422 | , a [ href "http://todomvc.com" ] [ text "TodoMVC" ] 423 | ] 424 | ] 425 | --------------------------------------------------------------------------------