-
Notifications
You must be signed in to change notification settings - Fork 16
/
25-navigation-todos.elm
414 lines (331 loc) · 12.4 KB
/
25-navigation-todos.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
port module Main exposing (main)
-- We need some additional imports from Browser and Browser.Navigation packages.
-- We're going to use functionality from them to keep track of the URL and display
-- the appropriate todos for that URL.
-- For example, "/#incomplete" will show the incomplete todos and
-- "/#completed" will show the completed todos. All other URLs will
-- show all of the todos.
import Browser exposing (Document, UrlRequest(..), application)
import Browser.Navigation exposing (Key)
import Html exposing (..)
import Html.Attributes exposing (autofocus, checked, class, href, placeholder, style, type_, value)
import Html.Events exposing (onClick, onDoubleClick, onInput, onSubmit)
import Random
import Url exposing (Url)
-- We add two new messages:
-- LinkClicked and ChangeUrl which are sent to our application by
-- Browser.application every time when user clicks a link on a page or changes
-- the URL in browser's navigation bar.
type Msg
= UpdateText String
| GenerateTodoId
| AddTodo Int
| RemoveTodo Int
| Edit Int String
| EditSave Int String
| ToggleTodo Int
| SetFilter Filter
| LinkClicked UrlRequest
| ChangeUrl Url
type Filter
= All
| Incomplete
| Completed
type alias TodoEdit =
{ id : Int
, text : String
}
type alias Todo =
{ id : Int
, text : String
, completed : Bool
}
type alias Model =
{ text : String
, todos : List Todo
, editing : Maybe TodoEdit
, filter : Filter
-- We need to store the Browser.Navigation.Key in our model to be able to
-- call browser navigation functions from Browser.Navigation module.
-- The key gets passed to us on application initialization, where we store
-- it in our model.
-- Each function from Browser.Navigation module which manipulates browser
-- URL requires us to pass in this Key as a parameter.
-- This was a design decision made by the core developers
-- or Elm to prevent people from calling browser navigation functions outside
-- Browser.application (e.g. from less featureful applications like those
-- created by Browser.element), which could lead to a lot of insidious bugs.
, navigationKey : Key
}
-- view function for the Browser.application requires us to return a value of
-- type `Document Msg`. This is just a record containing
-- 1. title String, which is used as a page title in the browser toolbar
-- 2. body, which is a list of Html elements that will be children of the page's
-- body element.
view : Model -> Document Msg
view model =
{ title = "Navigation TODOs"
, body = [ viewBody model ]
}
viewBody : Model -> Html Msg
viewBody model =
div [ class "col-12 col-sm-6 offset-sm-3" ]
[ form [ class "row", onSubmit GenerateTodoId ]
[ div [ class "col-9" ]
[ input
[ onInput UpdateText
, value model.text
, autofocus True
, class "form-control"
, placeholder "Enter a todo"
]
[]
]
, div [ class "col-3" ]
[ button
[ class "btn btn-primary form-control" ]
[ text "+" ]
]
]
, viewFilters model.filter
, div [] <|
List.map
(viewTodo model.editing)
(filterTodos model.filter model.todos)
]
filterTodos : Filter -> List Todo -> List Todo
filterTodos filter todos =
case filter of
All ->
todos
Incomplete ->
List.filter (\t -> not t.completed) todos
Completed ->
List.filter (\t -> t.completed) todos
viewFilters : Filter -> Html Msg
viewFilters filter =
div []
[ viewFilter All (filter == All) "All"
, viewFilter Incomplete (filter == Incomplete) "Incomplete"
, viewFilter Completed (filter == Completed) "Completed"
]
viewFilter : Filter -> Bool -> String -> Html Msg
viewFilter filter isFilter filterText =
if isFilter then
span [ class "mr-3" ] [ text filterText ]
else
a
[ class "text-primary mr-3"
-- Whenever the user clicks on a filter link, the
-- hash in the URL changes to the filterText.
-- So if you refresh the page and your URL is
-- "/#completed", the completed todos will be visible.
, href ("#" ++ String.toLower filterText)
, onClick (SetFilter filter)
, style "cursor" "pointer"
]
[ text filterText ]
viewTodo : Maybe TodoEdit -> Todo -> Html Msg
viewTodo editing todo =
case editing of
Just todoEdit ->
if todoEdit.id == todo.id then
viewEditTodo todoEdit
else
viewNormalTodo todo
Nothing ->
viewNormalTodo todo
viewEditTodo : TodoEdit -> Html Msg
viewEditTodo todoEdit =
div [ class "card" ]
[ div [ class "card-block" ]
[ form [ onSubmit (EditSave todoEdit.id todoEdit.text) ]
[ input
[ onInput (Edit todoEdit.id)
, class "form-control"
, value todoEdit.text
]
[]
]
]
]
viewNormalTodo : Todo -> Html Msg
viewNormalTodo todo =
div [ class "card" ]
[ div [ class "card-block" ]
[ input
[ onClick (ToggleTodo todo.id)
, type_ "checkbox"
, checked todo.completed
, class "mr-3"
]
[]
, span
[ onDoubleClick (Edit todo.id todo.text)
, style "text-decoration"
(if todo.completed then
"line-through"
else
"none"
)
]
[ text todo.text ]
, span
[ onClick (RemoveTodo todo.id)
, class "float-right"
]
[ text "✖" ]
]
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateText newText ->
( { model | text = newText }, Cmd.none )
GenerateTodoId ->
( model
, Random.generate AddTodo (Random.int Random.minInt Random.maxInt)
)
AddTodo todoId ->
let
newTodos =
model.todos ++ [ Todo todoId model.text False ]
in
( { model | text = "", todos = newTodos }
, saveTodos newTodos
)
RemoveTodo todoId ->
let
newTodos =
List.filter (\todo -> todo.id /= todoId) model.todos
in
( { model | todos = newTodos }, saveTodos newTodos )
Edit todoId todoText ->
( { model | editing = Just { id = todoId, text = todoText } }
, Cmd.none
)
EditSave todoId todoText ->
let
newTodos =
List.map
(\todo ->
if todo.id == todoId then
{ todo | text = todoText }
else
todo
)
model.todos
in
( { model | editing = Nothing, todos = newTodos }
, saveTodos newTodos
)
ToggleTodo todoId ->
let
newTodos =
List.map
(\todo ->
if todo.id == todoId then
{ todo | completed = not todo.completed }
else
todo
)
model.todos
in
( { model | todos = newTodos }, saveTodos newTodos )
SetFilter filter ->
( { model | filter = filter }, Cmd.none )
-- Whenever user clicks a link on the page, the Elm runtime generates
-- this message for us and gives us the possibility to react.
-- See the docs in the elm/browser package to understand the difference
-- between Internal and External URL:
-- https://package.elm-lang.org/packages/elm/browser/latest/Browser#UrlRequest
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model
-- When user clicks a link in our app (like "Complete")
-- we ask the browser to make the new url part of browsing history
-- WITHOUT reloading the page (i.e. without triggering http
-- request to load a new page from the server).
, Browser.Navigation.pushUrl model.navigationKey (Url.toString url)
)
Browser.External url ->
( model, Browser.Navigation.load url )
-- Whenever the URL changes, the current URL gets passed to
-- ChangeUrl, which gets passed into the update function.
-- We pass the url into urlToFilter, which takes the
-- current location and returns the current filter.
ChangeUrl url ->
( { model | filter = urlToFilter url }, Cmd.none )
-- We only care about url.fragment for determining which filter is set.
-- If the fragment is "incomplete", we want our filter to be Incomplete, so
-- that the todos that are incomplete are shown.
-- We want "complete" to show the completed todos.
-- The clause _ -> catches all other strings, which means that all other
-- URL hashes will show all of the todos.
urlToFilter : Url -> Filter
urlToFilter url =
case url.fragment of
Nothing ->
All
Just hash ->
case String.toLower hash of
"incomplete" ->
Incomplete
"completed" ->
Completed
_ ->
All
port saveTodos : List Todo -> Cmd msg
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- We're now using Browser.application, whose init function requires two additional
-- arguments:
-- 1. Url - This will be passed to us by Elm runtime when the application is
-- initialized.
-- In our app we're using the URL to parse the currently active filter from
-- the URL's fragment (the part of URL following the '#' character).
-- We use (urlToFilter url) which returns the filter that we will use for that URL.
-- So if the page's URL is initially "/#completed", then (urlToFilter url) will
-- return Completed as the filter, so the filter value will be Completed,
-- which will lead to the completed todos to be shown.
-- 2. Key is the navigation key that we need to save into our model. We need it
-- to be able to control the browser's page loading functionality
-- (see LinkClicked branch of the update function for how the Key is used).
init : Flags -> Url -> Key -> ( Model, Cmd Msg )
init flags url navigationKey =
( { text = ""
, todos = flags.todos
, editing = Nothing
, filter = urlToFilter url
, navigationKey = navigationKey
}
, Cmd.none
)
type alias Flags =
{ todos : List Todo }
{-
We're using Browser.application instead of Browser.element, which extends the
latter in 3 important ways:
1. init gets two additional parameters:
- the current Url from the browsers navigation bar.
This allows you to show different things depending on the Url.
- the navigation Key, which you can save in your model and use it later to be able
add items to manipulate browser's navigation history
2. When someone clicks a link, like <a href="/home">Home</a>, it is intercepted
as a UrlRequest. So instead of loading new HTML, onUrlRequest creates a message
for your update where you can decide exactly what to do next.
3. When the URL changes, the new Url is sent to onUrlChange.
The resulting message goes to update where you can decide how to show the new page.
-}
main : Program Flags Model Msg
main =
application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlRequest = LinkClicked
, onUrlChange = ChangeUrl
}