├── .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 |
--------------------------------------------------------------------------------