blob: bf1d39db31719f952d3219ed6ed0701914b5aa6f [file] [log] [blame]
port module Todo exposing (..)
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into four distinct parts:
1. Model - a full description of the application as data
2. Update - a way to update the model based on user actions
3. View - a way to visualize our model with HTML
This program is not particularly large, so definitely see the following
document for notes on structuring more complex GUIs with Elm:
http://guide.elm-lang.org/architecture/
-}
import Dom
import Task
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Lazy exposing (lazy, lazy2)
import Html.App
import Navigation exposing (Parser)
import String
import String.Extra
import Todo.Task
-- MODEL
-- The full application state of our todo app.
type alias Model =
{ tasks : List Todo.Task.Model
, field : String
, uid : Int
, visibility : String
}
type alias Flags =
Maybe Model
emptyModel : Model
emptyModel =
{ tasks = []
, visibility = "All"
, field = ""
, uid = 0
}
-- UPDATE
-- A description of the kinds of actions that can be performed on the model of
-- our application. See the following post for more info on this pattern and
-- some alternatives: http://guide.elm-lang.org/architecture/
type Msg
= NoOp
| UpdateField String
| Add
| UpdateTask ( Int, Todo.Task.Msg )
| DeleteComplete
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on any given Message
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case Debug.log "MESSAGE: " msg of
NoOp ->
( model, Cmd.none )
UpdateField str ->
let
newModel =
{ model | field = str }
in
( newModel, save model )
Add ->
let
description =
String.trim model.field
newModel =
if String.isEmpty description then
model
else
{ model
| uid = model.uid + 1
, field = ""
, tasks = model.tasks ++ [ Todo.Task.init description model.uid ]
}
in
( newModel, save newModel )
UpdateTask ( id, taskMsg ) ->
let
updateTask t =
if t.id == id then
Todo.Task.update taskMsg t
else
Just t
newModel =
{ model | tasks = List.filterMap updateTask model.tasks }
in
case taskMsg of
Todo.Task.Focus elementId ->
newModel ! [ save newModel, focusTask elementId ]
_ ->
( newModel, save newModel )
DeleteComplete ->
let
newModel =
{ model
| tasks = List.filter (not << .completed) model.tasks
}
in
( newModel, save newModel )
CheckAll bool ->
let
updateTask t =
{ t | completed = bool }
newModel =
{ model | tasks = List.map updateTask model.tasks }
in
( newModel, save newModel )
ChangeVisibility visibility ->
let
newModel =
{ model | visibility = visibility }
in
( newModel, save model )
focusTask : String -> Cmd Msg
focusTask elementId =
Task.perform (\_ -> NoOp) (\_ -> NoOp) (Dom.focus elementId)
-- VIEW
view : Model -> Html Msg
view model =
div
[ class "todomvc-wrapper"
, style [ ( "visibility", "hidden" ) ]
]
[ section
[ class "todoapp" ]
[ lazy taskEntry model.field
, lazy2 taskList model.visibility model.tasks
, lazy2 controls model.visibility model.tasks
]
, infoFooter
]
taskEntry : String -> Html Msg
taskEntry task =
header
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, onInput UpdateField
, Todo.Task.onFinish Add NoOp
]
[]
]
taskList : String -> List Todo.Task.Model -> Html Msg
taskList visibility tasks =
let
isVisible todo =
case visibility of
"Completed" ->
todo.completed
"Active" ->
not todo.completed
-- "All"
_ ->
True
allCompleted =
List.all .completed tasks
cssVisibility =
if List.isEmpty tasks then
"hidden"
else
"visible"
in
section
[ class "main"
, style [ ( "visibility", cssVisibility ) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul
[ class "todo-list" ]
(List.map
(\task ->
let
id =
task.id
taskView =
Todo.Task.view task
in
Html.App.map (\msg -> UpdateTask ( id, msg )) taskView
)
(List.filter isVisible tasks)
)
]
controls : String -> List Todo.Task.Model -> Html Msg
controls visibility tasks =
let
tasksCompleted =
List.length (List.filter .completed tasks)
tasksLeft =
List.length tasks - tasksCompleted
item_ =
if tasksLeft == 1 then
" item"
else
" items"
in
footer
[ class "footer"
, hidden (List.isEmpty tasks)
]
[ span
[ class "todo-count" ]
[ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left")
]
, ul
[ class "filters" ]
[ visibilitySwap "#/" "All" visibility
, text " "
, visibilitySwap "#/active" "Active" visibility
, text " "
, visibilitySwap "#/completed" "Completed" visibility
]
, button
[ class "clear-completed"
, hidden (tasksCompleted == 0)
, onClick DeleteComplete
]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
]
visibilitySwap : String -> String -> String -> Html Msg
visibilitySwap uri visibility actualVisibility =
let
className =
if visibility == actualVisibility then
"selected"
else
""
in
li
[ onClick (ChangeVisibility visibility) ]
[ a [ class className, href uri ] [ text visibility ] ]
infoFooter : Html msg
infoFooter =
footer
[ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]
-- wire the entire application together
main : Program Flags
main =
Navigation.programWithFlags urlParser
{ urlUpdate = urlUpdate
, view = view
, init = init
, update = update
, subscriptions = subscriptions
}
-- URL PARSERS - check out evancz/url-parser for fancier URL parsing
toUrl : String -> String
toUrl visibility =
"#/" ++ String.toLower visibility
fromUrl : String -> Maybe String
fromUrl hash =
let
cleanHash =
String.dropLeft 2 hash
in
if (List.member cleanHash [ "all", "active", "completed" ]) == True then
Just cleanHash
else
Nothing
urlParser : Parser (Maybe String)
urlParser =
Navigation.makeParser (fromUrl << .hash)
{-| The URL is turned into a Maybe value. If the URL is valid, we just update
our model with the new visibility settings. If it is not a valid URL,
we set the visibility filter to show all tasks.
-}
urlUpdate : Maybe String -> Model -> ( Model, Cmd Msg )
urlUpdate result model =
case result of
Just visibility ->
update (ChangeVisibility (String.Extra.toSentenceCase visibility)) model
Nothing ->
update (ChangeVisibility "All") model
init : Flags -> Maybe String -> ( Model, Cmd Msg )
init flags url =
urlUpdate url (Maybe.withDefault emptyModel flags)
-- interactions with localStorage
port save : Model -> Cmd msg
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none