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

これまでの復習

[Elm 0.18で作るTodoアプリ(1)](https://qiita.com/tomluck/items/872787cda6682834a3a1)「固定値のTodoをTodoリストへ追加できるようにする」
[Elm 0.18で作るTodoアプリ(2)](https://qiita.com/tomluck/items/ec95df77fb54c9d54275)「テキストボックスへ入力したTodoをTodoリストへ追加できるようにする」
[Elm 0.18で作るTodoアプリ(3)](https://qiita.com/tomluck/items/d0cf014b80449e919c3c)「Todoのチェック用にチェックボックスを設置する」
[Elm 0.18で作るTodoアプリ(4)](https://qiita.com/tomluck/items/5ba609c72cd35e7959c8)「TodoList側の機能拡張をする。」

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

# 今回のテーマ
(最終回)一意にupdateするTodoを決められるように機能拡張する。

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

# 実装

## Todo.elmの修正

- モデルの修正

```diff
 type alias Model = 
-    { done : Bool
+    { itemID : Int
+    , done : Bool
     , item : String
     , del  : Bool
     }
 
-new : Bool -> String -> Bool -> Model
-new do s de = 
-    { done = do
+new : Int -> Bool -> String -> Bool -> Model
+new itemID do s de = 
+    { itemID = itemID
+    , done = do
     , item = s
     , del = de
     }
     
 type Msg
     = NoOp
-    | ToggleDone String
-    | OnDelete String
+    | ToggleDone Int
+    | OnDelete Int
```

Todoを一意に識別するためにitemID属性を追加します。
それに伴いnew関数の引数やメッセージも修正します。

- updateの修正

```diff
update : Msg -> Model -> Model
update message model =
    case message of
         NoOp ->
             model
 
-        ToggleDone s -> 
-            if s == model.item then
+        ToggleDone itemID -> 
+            if itemID == model.itemID then
                 { model | done = not model.done }
             else
                 model
 
-        OnDelete s -> 
-            if s == model.item then
+        OnDelete itemID -> 
+            if itemID == model.itemID then
                 { model | del = True }
             else
                 model
```

ToggleDoneとonDeleteの引数を文字列からitemIDへ変更し、itemID単位で該当するTodoのみ状態を変更させるようにしています。

- viewの定義

```diff
view : Model -> Html Msg
view model = 
    li [] 
        [ 
-            checkbox (ToggleDone model.item) model
+            checkbox (ToggleDone model.itemID) model
        ]

 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 =
     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" ]
+        [ button [ onClick (OnDelete model.itemID) ] [ text "x" ]
         ]
```

メッセージの定義変更に合わせて引数をitemIDへ変更しています。

## TodoList.elmの変更

- ファイル全体

```diff

 module TodoList exposing (..)
 
 import Html exposing (..)
 import Html.Attributes exposing (class)
 import Html.Events exposing (onClick)
 import Todo
 
 type alias Model = 
     { todoList : List TodoModel
+    , nextID : Int
     }
 
 initialModel : Model
 initialModel = 
     { todoList = 
-        [ Todo.new False "task1" False
-        , Todo.new False "task2" False
-        , Todo.new True "task3" False
-        , Todo.new False "task4" False
+        [ Todo.new 1 False "task1" False
+        , Todo.new 2 False "task2" False
+        , Todo.new 3 True  "task3" False
+        , Todo.new 4 False "task4" False
         ]
+    , nextID = 5
     }
 
 type Msg
     = NoOp
     | AddNew
     | DeleteFinished
     | TodoMsg Todo.Msg
 
 -- update
-update : Msg -> TodoModel -> Model -> ( Model, Cmd Msg )
-update message todo model =
+update : Msg -> String -> Model -> ( Model, Cmd Msg )
+update message item model =
     case message of
         NoOp ->
             model ! []
 
         AddNew -> 
-            { model | todoList = model.todoList ++ [todo] } ! []
+            let
+                todo = 
+                    Todo.new model.nextID False item False
+            in
+                { model | 
+                    todoList = model.todoList ++ [todo] 
+                    , nextID = model.nextID + 1
+                } ! []
 
         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 = List.filter itemIsNotDeleted updatedTodoList } ! []
 
 -- 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

```

モデルにnextID属性(次のTodoのID)を追加しています。
その他の修正はTodo.elmのモデル変更に伴うものです。

updateの引数の型を一部変更しました。
TodoModel型をString型へ変更したことで、AddNewのコードの見通しが悪くなったため、let-in文で書き直しました。nextIDのインクリメントの処理を追加しています。

## Main.elmの変更

- ファイル全体
 
```diff

 module Main exposing (..)
 
-import Html exposing (Html, program)
+import Html exposing (..)
 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 False) model.todoList
+                    TodoList.update subMsg model.todoCreator.inputStr 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) 
-        ]
+    body [] 
+        [ h1 [] [ text "ToDo list" ]
+        , div [] 
+            [ map TodoCreatorMsg (TodoCreator.view model.todoCreator)
+            , map TodoListMsg (TodoList.view model.todoList) 
+            ]
+    ]
```

TodoListのメッセージの引数の型がTodoModel型からString型へ変更されたため、TodoList.updateの引数はtodoCreatorのinputStrをそのまま渡すことができるようになりました。
その結果、Todoをimportせずに済むようになり、Todoの変更に影響されなくなりました。

viewの変更は主に見栄えの変更のみです。

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

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

![FF_elm_study5.png](https://qiita-image-store.s3.amazonaws.com/0/129853/015a4760-4db1-699d-4aae-530139d1e5f7.png)

今回は内部実装変更が主だったので、見た目はそれほど変わらないです。

# おわりに
5回に渡って連載してきたElm 0.18で作るTodoアプリですが、今回が最終回です。

elmに初めて触り、tutorialなどを参考に作成してみました。
AddボタンがTodoList側にあるところ(できればTodoCreator側にボタン設置したい)など設計上気になるところはありますが、JavaScriptに触らずにここまで作れるのはすごいなと思いました。

ElmにはJavaScriptと連携させることもできるようですが、そこまで手が回らなかったので、徐々に勉強していきたいと思います。