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