1port module Todo exposing (..)
2
3{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
4
5This application is broken up into four distinct parts:
6
7  1. Model  - a full description of the application as data
8  2. Update - a way to update the model based on user actions
9  3. View   - a way to visualize our model with HTML
10
11This program is not particularly large, so definitely see the following
12document for notes on structuring more complex GUIs with Elm:
13http://guide.elm-lang.org/architecture/
14-}
15
16import Dom
17import Task
18import Html exposing (..)
19import Html.Attributes exposing (..)
20import Html.Events exposing (..)
21import Html.Lazy exposing (lazy, lazy2)
22import Html.App
23import Navigation exposing (Parser)
24import String
25import String.Extra
26import Todo.Task
27
28
29-- MODEL
30-- The full application state of our todo app.
31
32
33type alias Model =
34    { tasks : List Todo.Task.Model
35    , field : String
36    , uid : Int
37    , visibility : String
38    }
39
40
41type alias Flags =
42    Maybe Model
43
44
45emptyModel : Model
46emptyModel =
47    { tasks = []
48    , visibility = "All"
49    , field = ""
50    , uid = 0
51    }
52
53
54
55-- UPDATE
56-- A description of the kinds of actions that can be performed on the model of
57-- our application. See the following post for more info on this pattern and
58-- some alternatives: http://guide.elm-lang.org/architecture/
59
60
61type Msg
62    = NoOp
63    | UpdateField String
64    | Add
65    | UpdateTask ( Int, Todo.Task.Msg )
66    | DeleteComplete
67    | CheckAll Bool
68    | ChangeVisibility String
69
70
71
72-- How we update our Model on any given Message
73
74
75update : Msg -> Model -> ( Model, Cmd Msg )
76update msg model =
77    case Debug.log "MESSAGE: " msg of
78        NoOp ->
79            ( model, Cmd.none )
80
81        UpdateField str ->
82            let
83                newModel =
84                    { model | field = str }
85            in
86                ( newModel, save model )
87
88        Add ->
89            let
90                description =
91                    String.trim model.field
92
93                newModel =
94                    if String.isEmpty description then
95                        model
96                    else
97                        { model
98                            | uid = model.uid + 1
99                            , field = ""
100                            , tasks = model.tasks ++ [ Todo.Task.init description model.uid ]
101                        }
102            in
103                ( newModel, save newModel )
104
105        UpdateTask ( id, taskMsg ) ->
106            let
107                updateTask t =
108                    if t.id == id then
109                        Todo.Task.update taskMsg t
110                    else
111                        Just t
112
113                newModel =
114                    { model | tasks = List.filterMap updateTask model.tasks }
115            in
116                case taskMsg of
117                    Todo.Task.Focus elementId ->
118                        newModel ! [ save newModel, focusTask elementId ]
119
120                    _ ->
121                        ( newModel, save newModel )
122
123        DeleteComplete ->
124            let
125                newModel =
126                    { model
127                        | tasks = List.filter (not << .completed) model.tasks
128                    }
129            in
130                ( newModel, save newModel )
131
132        CheckAll bool ->
133            let
134                updateTask t =
135                    { t | completed = bool }
136
137                newModel =
138                    { model | tasks = List.map updateTask model.tasks }
139            in
140                ( newModel, save newModel )
141
142        ChangeVisibility visibility ->
143            let
144                newModel =
145                    { model | visibility = visibility }
146            in
147                ( newModel, save model )
148
149
150focusTask : String -> Cmd Msg
151focusTask elementId =
152    Task.perform (\_ -> NoOp) (\_ -> NoOp) (Dom.focus elementId)
153
154
155
156-- VIEW
157
158
159view : Model -> Html Msg
160view model =
161    div
162        [ class "todomvc-wrapper"
163        , style [ ( "visibility", "hidden" ) ]
164        ]
165        [ section
166            [ class "todoapp" ]
167            [ lazy taskEntry model.field
168            , lazy2 taskList model.visibility model.tasks
169            , lazy2 controls model.visibility model.tasks
170            ]
171        , infoFooter
172        ]
173
174
175taskEntry : String -> Html Msg
176taskEntry task =
177    header
178        [ class "header" ]
179        [ h1 [] [ text "todos" ]
180        , input
181            [ class "new-todo"
182            , placeholder "What needs to be done?"
183            , autofocus True
184            , value task
185            , name "newTodo"
186            , onInput UpdateField
187            , Todo.Task.onFinish Add NoOp
188            ]
189            []
190        ]
191
192
193taskList : String -> List Todo.Task.Model -> Html Msg
194taskList visibility tasks =
195    let
196        isVisible todo =
197            case visibility of
198                "Completed" ->
199                    todo.completed
200
201                "Active" ->
202                    not todo.completed
203
204                -- "All"
205                _ ->
206                    True
207
208        allCompleted =
209            List.all .completed tasks
210
211        cssVisibility =
212            if List.isEmpty tasks then
213                "hidden"
214            else
215                "visible"
216    in
217        section
218            [ class "main"
219            , style [ ( "visibility", cssVisibility ) ]
220            ]
221            [ input
222                [ class "toggle-all"
223                , type' "checkbox"
224                , name "toggle"
225                , checked allCompleted
226                , onClick (CheckAll (not allCompleted))
227                ]
228                []
229            , label
230                [ for "toggle-all" ]
231                [ text "Mark all as complete" ]
232            , ul
233                [ class "todo-list" ]
234                (List.map
235                    (\task ->
236                        let
237                            id =
238                                task.id
239
240                            taskView =
241                                Todo.Task.view task
242                        in
243                            Html.App.map (\msg -> UpdateTask ( id, msg )) taskView
244                    )
245                    (List.filter isVisible tasks)
246                )
247            ]
248
249
250controls : String -> List Todo.Task.Model -> Html Msg
251controls visibility tasks =
252    let
253        tasksCompleted =
254            List.length (List.filter .completed tasks)
255
256        tasksLeft =
257            List.length tasks - tasksCompleted
258
259        item_ =
260            if tasksLeft == 1 then
261                " item"
262            else
263                " items"
264    in
265        footer
266            [ class "footer"
267            , hidden (List.isEmpty tasks)
268            ]
269            [ span
270                [ class "todo-count" ]
271                [ strong [] [ text (toString tasksLeft) ]
272                , text (item_ ++ " left")
273                ]
274            , ul
275                [ class "filters" ]
276                [ visibilitySwap "#/" "All" visibility
277                , text " "
278                , visibilitySwap "#/active" "Active" visibility
279                , text " "
280                , visibilitySwap "#/completed" "Completed" visibility
281                ]
282            , button
283                [ class "clear-completed"
284                , hidden (tasksCompleted == 0)
285                , onClick DeleteComplete
286                ]
287                [ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
288            ]
289
290
291visibilitySwap : String -> String -> String -> Html Msg
292visibilitySwap uri visibility actualVisibility =
293    let
294        className =
295            if visibility == actualVisibility then
296                "selected"
297            else
298                ""
299    in
300        li
301            [ onClick (ChangeVisibility visibility) ]
302            [ a [ class className, href uri ] [ text visibility ] ]
303
304
305infoFooter : Html msg
306infoFooter =
307    footer
308        [ class "info" ]
309        [ p [] [ text "Double-click to edit a todo" ]
310        , p []
311            [ text "Written by "
312            , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
313            ]
314        , p []
315            [ text "Part of "
316            , a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
317            ]
318        ]
319
320
321
322-- wire the entire application together
323
324
325main : Program Flags
326main =
327    Navigation.programWithFlags urlParser
328        { urlUpdate = urlUpdate
329        , view = view
330        , init = init
331        , update = update
332        , subscriptions = subscriptions
333        }
334
335
336
337-- URL PARSERS - check out evancz/url-parser for fancier URL parsing
338
339
340toUrl : String -> String
341toUrl visibility =
342    "#/" ++ String.toLower visibility
343
344
345fromUrl : String -> Maybe String
346fromUrl hash =
347    let
348        cleanHash =
349            String.dropLeft 2 hash
350    in
351        if (List.member cleanHash [ "all", "active", "completed" ]) == True then
352            Just cleanHash
353        else
354            Nothing
355
356
357urlParser : Parser (Maybe String)
358urlParser =
359    Navigation.makeParser (fromUrl << .hash)
360
361
362{-| The URL is turned into a Maybe value. If the URL is valid, we just update
363our model with the new visibility settings. If it is not a valid URL,
364we set the visibility filter to show all tasks.
365-}
366urlUpdate : Maybe String -> Model -> ( Model, Cmd Msg )
367urlUpdate result model =
368    case result of
369        Just visibility ->
370            update (ChangeVisibility (String.Extra.toSentenceCase visibility)) model
371
372        Nothing ->
373            update (ChangeVisibility "All") model
374
375
376init : Flags -> Maybe String -> ( Model, Cmd Msg )
377init flags url =
378    urlUpdate url (Maybe.withDefault emptyModel flags)
379
380
381
382-- interactions with localStorage
383
384
385port save : Model -> Cmd msg
386
387
388subscriptions : Model -> Sub Msg
389subscriptions model =
390    Sub.none
391