Elm

Elm 0.18で作るTodoアプリ(4)

More than 1 year has passed since last update.

これまでの復習

Elm 0.18で作るTodoアプリ(1)「固定値のTodoをTodoリストへ追加できるようにする」
Elm 0.18で作るTodoアプリ(2)「テキストボックスへ入力したTodoをTodoリストへ追加できるようにする」
Elm 0.18で作るTodoアプリ(3)「Todoのチェック用にチェックボックスを設置する」

前回をベースにTodoアプリを拡張していきます。

今回のテーマ

TodoList側の機能拡張をする。具体的には以下の機能を追加していきます。
 機能1. 一つのTodoを削除できるようにする
 機能2. 完了したTodo数を表示できるようにする
 機能3. 完了したTodoを削除して、未完了のリストを表示できるようにする

ファイル構成

  • Main.elm:大元のプログラム。基本的には配下のモジュールへ委譲するのみ。
  • TodoList.elm:Todoのリスト管理を行う
  • TodoCreator.elm:(今回は変更なし)テキストボックスを管理する。
  • Todo.elm:一つのTodoを管理する。

実装

Todo.elmの修正(機能1への対応)

  • モデルの修正
 type alias Model = 
     { done : Bool
     , item : String
+    , del  : Bool
     }

-new : Bool -> String -> Model
-new b s = 
-    { done = b
+new : Bool -> String -> Bool -> Model
+new do s de = 
+    { done = do
     , item = s
+    , del = de
     }

 type Msg
     = NoOp
     | ToggleDone String
+    | OnDelete String

削除状態を管理するための属性(del)と削除メッセージ(onDelete)を追加します。

  • updateの修正
update : Msg -> Model -> Model
update message model =
    case message of
        NoOp ->
            model

        ToggleDone s -> 
            if s == model.item then
                { model | done = not model.done }
            else
                model

+        OnDelete s -> 
+            if s == model.item then
+                { model | del = True }
+            else
+                model

onDeleteのケースを追加し、引数でTodo相当の文字列をもらい、該当のTodoのみ状態を変更させるようにしています。

  • viewの定義
view : Model -> Html Msg
view model = 
    li [] 
        [ 
            checkbox (ToggleDone model.item) model
        ]

-checkbox : msg -> Model -> Html msg
+checkbox : Msg -> Model -> Html Msg
 checkbox msg model = 
     label []
         [ input [ type_ "checkbox", checked model.done, onClick msg ] []
         , viewItem model
+        , viewDeleteButton model
         ]

-viewItem : Model -> Html msg
+viewItem : Model -> Html Msg
 viewItem model =
     if model.done == False then
         text model.item
    else
        s [] 
             [ span [ style [ ("color", "gray") ] ]
                     [ text model.item ]
             ]
+
+viewDeleteButton : Model -> Html Msg
+viewDeleteButton model =
+    span []
+        [ button [ onClick (OnDelete model.item) ] [ text "x" ]
+        ]

msgの型修正とviewDeleteButton関数の追加をしています。
ボタン押下時にonDeleteメッセージを呼び出します。

一つのTodoを管理しているTodo.elmでは自身の状態を変更するだけです。
リストからTodoを削除するのはTodoList側で処理します。

TodoList.elmの変更(機能2、3への対応)

  • モデルの変更
 initialModel : Model
 initialModel = 
     { todoList = 
-        [ Todo.new False "task1"
-        , Todo.new False "task2"
-        , Todo.new True "task3"
-        , Todo.new False "task4"
+        [ Todo.new False "task1" False
+        , Todo.new False "task2" False
+        , Todo.new True "task3" False
+        , Todo.new False "task4" False
         ]
     }

 type Msg
     = NoOp
     | AddNew
+    | DeleteFinished
     | TodoMsg Todo.Msg

Todo.elmのモデル変更による修正と、機能3.の対応のためにメッセージ(DeleteFinished)を追加しています。

  • updateの変更
 update : Msg -> TodoModel -> Model -> ( Model, Cmd Msg )
 update message todo model =
     case message of
         NoOp ->
             model ! []
         AddNew -> 
             { model | todoList = model.todoList ++ [todo] } ! []

+        DeleteFinished -> 
+            let
+                itemIsNotFinished todoModel = not todoModel.done
+            in
+                { model | 
+                    todoList =  List.filter itemIsNotFinished model.todoList } ! []
+
         TodoMsg subMsg ->
             let
+                itemIsNotDeleted todoModel = 
+                    not todoModel.del
+                    
                 updatedTodoList = 
                     List.map (Todo.update subMsg) model.todoList
             in
-                { model | todoList = updatedTodoList } ! []
+                { model | 
+                    todoList = List.filter itemIsNotDeleted updatedTodoList } ! []

Listの標準関数を使ってListの要素からフィルタリングしたものだけを取り出しています。

DeleteFinishedではtodoModelのdoneがFalseのものだけをフィルタリングしています。

TodoMsgではlet文でTodoへ更新の委譲をした後のリストを作成し、in文で更新後のリストに対してtodoModelのdelがFalseのものだけをフィルタリングしています。

  • viewの変更
 view : Model -> Html Msg
 view model = 
     div []
         [
         div [ class "p2" ]
             [ addButton
+            , viewCounter model
+            , deleteFinishedButton
             , viewList model
             ]
         ]

 addButton : Html Msg 
 addButton =
     div [] 
         [ button [ onClick AddNew ] [ text "Add" ] ]

+deleteFinishedButton : Html Msg
+deleteFinishedButton = 
+    div [] 
+        [ button [ onClick DeleteFinished ] [ text "Delete Finished" ] ]
+
+viewCounter : Model -> Html Msg
+viewCounter model = 
+    div []
+        [ text ("Finished Task: " 
+            ++ toString ( countDoneItems model )
+            ++ "/" 
+            ++ toString ( List.length model.todoList ) ) 
+        ]
+
+countDoneItems : Model -> Int
+countDoneItems model = 
+    List.filter itemIsDone model.todoList
+    |> List.length
+
+itemIsDone : TodoModel -> Bool
+itemIsDone todoModel = todoModel.done 
+
 viewList : Model -> Html Msg
 viewList model = 
     (ul [] 
         (List.map Todo.view model.todoList))
         |> Html.map TodoMsg

viewCounter関数ではTodoの(完了数 / 総数)という表示をしています。
deleteFinishedButton関数はボタン押下でDeleteFinishedメッセージを呼び出しています。

Main.elmの変更(機能1への対応)

  • ファイル全体
 module Main exposing (..)

 import Html exposing (Html, program)
 import TodoCreator
 import TodoList
 import Todo

 main : Program Never Model Msg
 main =
     program
         { init = init
         , view = view
         , update = update
         , subscriptions = subscriptions
         }

 -- model
 type alias Model = 
     { todoCreator : TodoCreator.Model
     , todoList : TodoList.Model
     }

 initialModel : Model
 initialModel = 
     { todoCreator = TodoCreator.initialModel
     , todoList = TodoList.initialModel
     }

 init : ( Model, Cmd Msg)
 init = 
     ( initialModel, Cmd.none)

 type Msg
     = TodoCreatorMsg TodoCreator.Msg
     | TodoListMsg TodoList.Msg

 -- update
 update : Msg -> Model -> ( Model, Cmd Msg )
 update message model = 
     case message of
         TodoCreatorMsg subMsg ->
             let
                 ( updatedCreator, todoCreatorCmd ) =
                     TodoCreator.update subMsg model.todoCreator
             in
                 ( { model | todoCreator = updatedCreator }, Cmd.map TodoCreatorMsg todoCreatorCmd )

         TodoListMsg subMsg ->
             let
                 ( updatedTodoListModel, todoListCmd ) =
-                    TodoList.update subMsg (Todo.new False model.todoCreator.inputStr) model.todoList
+                    TodoList.update subMsg (Todo.new False model.todoCreator.inputStr False) model.todoList
             in
                 ( { model | todoList = updatedTodoListModel }, Cmd.map TodoListMsg todoListCmd )

 -- subscription
 subscriptions : Model -> Sub Msg
 subscriptions model =
     Sub.none

 -- view
 view : Model -> Html Msg
 view model =
     Html.div []
         [ Html.map TodoCreatorMsg (TodoCreator.view model.todoCreator)
         , Html.map TodoListMsg (TodoList.view model.todoList) 
         ]

Todo.elmのモデルの変更に追従した修正のみです。

実行結果

前回と同様にmakeして、作成したindex.htmlをブラウザで見ます。

$ elm-make Main.elm --output index.html --debug

FF_elm_study4.png

Finished TaskでTodoの数が表示され、DeleteFinishedボタンや×ボタンがあり、押下するとTodoが削除されることが分かると思います。

おわりに

主にTodoListで3つの機能を追加してみました。
ただし現在の実装だとTodoモデルのitem属性でupdateするTodoを決めているため、同じitem属性(Todo名)の要素の場合、一緒にupdateされてしまうという問題があります。

次回(おそらく最終回)は一意にupdateするTodoを決められるように機能拡張をし、少し見栄えを整えたいと思います。
次回:Elm 0.18で作るTodoアプリ(5)