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