elm 0.19 で Todoアプリのようなものを試してみたメモ

## 概要

[Elm 0.18で作るTodoアプリ(1)](https://qiita.com/tomluck/items/872787cda6682834a3a1)を参考に、elm0.19でタイトルを追加するサンプルを作ってみた。
以下の部分を気を付ければ、元の記事はelm0.19でも十分に参考になる。

* ` a ! []` が `Tuple.pair a Cmd.None`になった
* `Elm.Main.embed(mountNode)`が`Elm.Main.init({flags: 6, node: mountNode})`になった
* `Html.program`が`Browser.element`になった

ここまでやった感想としては、elmはエラーメッセージがとても親切で嬉しい。
String.fromIntにしたら？ってメソッドまで教えてくれる。

```
elm_1  | 52|             Tuple.pair { model | handoutList = model.handoutList ++ [ newHO ], nextId = model.nextId + 1 } (toJs ("Add Handout : " ++ model.nextId ++ ":" ++ title))
elm_1  |                                                                                                                                           ^^^^^^^^^^^^
elm_1  | Try using String.fromInt to turn it into a string? Or put it in [] to make it a
```

タイトル以外も追加して一応動く形にしたのが以下。

[インセインハンドアウトメイカー](http://asuwa.mistysky.net/inSANe/card.html)

## 開発環境

* Windows 10
* Vagrant 2.1.5
* Virtualbox 5.2.18
* Ubuntu 18.04 LTS (Bionic Beaver)
* Docker version 18.06.0-ce, build 0ffa825
* docker-compose version 1.22.0, build f46880fe

[前回記事](https://qiita.com/hibohiboo/items/311b65987fbe64ce29fe)で作成したものを使う。

## ファイル構成

[githubリポジトリ参照](https://github.com/hibohiboo/develop/tree/750d6ff8930f8323498cb8caf6da5a911bc787ae/tutorial/lesson/elm/elm-card)  


```yaml
+ bin # dockerの起動などのコマンドをシェルスクリプトにして保存
- docker
  - elm
    - Dockerfile # 開発環境ツール
  - docker-compose.yml # dockerコンテナにディレクトリやファイルをマッピングする設定
- app
  - .babelrc # babel用設定
  - package.json # jsのパッケージ
  - elm.json # elmのパッケージ
  - webpack.config.js # webpack設定
  - src
    - styles.scss # スタイルシート
    - card.js # elmを読み込むためのjsファイル
    - html
      - card.html # jsファイルを読み込むhtmlファイル
    - Card
      - Main.elm     # 大元のプログラム
      - HandoutList.elm # ハンドアウト一覧
      - Handout.elm  # ハンドアウト本体
      - HandoutCreate.elm # 入力部分

```

## ファイル

materialize.cssを使おうとしたが、elmで作られた部分にはjsがうまく動作しない模様。
elmのマテリアルデザインのコンポーネントを使ったほうがよさそうだが、0.19対応のものを見つけられなかった。

`<div id="cards"></div>`のノードにelmを紐づける。

```html:app/src/html/card.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css" />
    <script src='https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js'></script>
    <title>ハンドアウト</title>
  </head>
  <h1>ハンドアウト</h1>
  <body>
    <div class="main-content">
        <div id="cards"></div>
    </div>
  </body>
</html>

```

jsファイルはelmの読み込みを主に行う。
上記htmlで用意したnodeへの紐づけを行っている。
elm0.18のときとは、`Elm.Main.init`が書き方が異なっていた。

```js:app/src/card.js
'use strict';

require("./styles.scss");

const {Elm} = require('./Card/Main');
const mountNode = document.getElementById('cards');
const app = Elm.Main.init({flags: 6, node: mountNode});

app.ports.toJs.subscribe(data => {
    console.log(data);
})
// Use ES2015 syntax and let Babel compile it for you
var testFn = (inp) => {
    let a = inp + 1;
    return a;
}
```

Mainで基本的にやっていることは、各要素に渡しているだけ。

```elm:app/src/Card/Main.elm
module Main exposing (main)

import Browser
import Card.HandoutCreator as HandoutCreator
import Card.HandoutList as HandoutList
import Html exposing (..)
import Html.Attributes exposing (..)


main : Program Int Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }



-- model


type alias Model =
    { handoutCreator : HandoutCreator.Model
    , handoutListModel : HandoutList.Model
    }


initialModel : Model
initialModel =
    { handoutCreator = HandoutCreator.initialModel
    , handoutListModel = HandoutList.initialModel
    }


init : Int -> ( Model, Cmd Msg )
init flags =
    ( initialModel, Cmd.none )


type Msg
    = HandoutCreatorMsg HandoutCreator.Msg
    | HandoutListMsg HandoutList.Msg



-- update


update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        HandoutCreatorMsg subMsg ->
            let
                ( updatedCreator, handoutCreatorCmd ) =
                    HandoutCreator.update subMsg model.handoutCreator
            in
            ( { model | handoutCreator = updatedCreator }, Cmd.map HandoutCreatorMsg handoutCreatorCmd )

        HandoutListMsg subMsg ->
            let
                ( updatedHandoutListModel, handoutListCmd ) =
                    HandoutList.update subMsg model.handoutCreator.inputStr model.handoutListModel
            in
            ( { model | handoutListModel = updatedHandoutListModel }, Cmd.map HandoutListMsg handoutListCmd )



-- subscription


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



-- view


view : Model -> Html Msg
view model =
    div [ class "container" ]
        [ Html.map HandoutCreatorMsg (HandoutCreator.view model.handoutCreator)
        , Html.map HandoutListMsg (HandoutList.view model.handoutListModel)
        ]
```

`model ! []`の書き方が使えなくなっていたので、 `Tuple.pair model Cmd.none`に変更している。

```elm:app/src/Card/HandoutCreator.elm
module Card.HandoutCreator exposing (Item, Model, Msg(..), handoutInput, initialModel, update, view)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)


type alias Item =
    String


type alias Model =
    { inputTitle : Item
    }


initialModel : Model
initialModel =
    { inputTitle = ""
    }


type Msg
    = NoOp
    | UpdateInput String



-- update


update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        NoOp ->
            Tuple.pair model Cmd.none

        UpdateInput s ->
            Tuple.pair { model | inputTitle = s } Cmd.none



-- view


view : Model -> Html Msg
view model =
    div []
        [ handoutInput model
        ]


handoutInput : Model -> Html Msg
handoutInput model =
    Html.form [ class "no-autoinit" ]
        [ label [ attribute "for" "inputTitle" ]
            [ text "タイトル"
            ]
        , input
            [ onInput UpdateInput, value model.inputTitle, id "inputTitle" ]
            []
        ]

```

ここは特にいみなくportを試している以外は、参考にしたページとほぼ変わらない。

```elm:app/src/Card/HandoutList.elm
port module Card.HandoutList exposing (Model, Msg(..), initialModel, toJs, update, updateButton, view, viewList)

import Browser
import Browser.Navigation as Nav
import Card.Handout exposing (Handout, Msg, insaneHandout, new, update)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Url exposing (Url)
import Url.Parser as UrlParser


port toJs : String -> Cmd msg


type alias Model =
    { handoutList : List Handout
    , nextId : Int
    }


initialModel : Model
initialModel =
    { handoutList =
        [ Handout 1 "item1" False
        ]
    , nextId = 2
    }



-- UPDATE


type Msg
    = NoOp
    | AddNew
    | HandoutMsg Card.Handout.Msg


update : Msg -> String -> Model -> ( Model, Cmd Msg )
update message title model =
    case message of
        NoOp ->
            ( model, Cmd.none )

        AddNew ->
            let
                newHO =
                    Card.Handout.new model.nextId title False
            in
            Tuple.pair { model | handoutList = model.handoutList ++ [ newHO ], nextId = model.nextId + 1 } (toJs ("Add Handout : " ++ String.fromInt model.nextId ++ ":" ++ title))

        HandoutMsg subMsg ->
            let
                itemIsNotDeleted m =
                    not m.del

                updatedHandoutList =
                    List.map (Card.Handout.update subMsg) model.handoutList
            in
            Tuple.pair { model | handoutList = List.filter itemIsNotDeleted updatedHandoutList } Cmd.none



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ p [] [ text "ハンドアウト一覧" ]
        , updateButton model.handoutList
        , div [ class "print" ]
            [ viewList model.handoutList
            ]
        ]


updateButton : List Handout -> Html Msg
updateButton models =
    div []
        [ button [ onClick AddNew ] [ text "Add" ] ]


viewList : List Handout -> Html Msg
viewList models =
    ul [] (List.map insaneHandout models) |> Html.map HandoutMsg
```

viewで無理やりインセインのハンドアウト作っているのでノイズになってしまっていて申し訳ない。
本質はview以外なので、とはいえこちらも元のページと大きな違いはない。

```elm:app/src/Card/Handout.elm
module Card.Handout exposing (Handout, Msg, insaneHandout, new, update)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)


type alias Handout =
    { id : Int
    , title : String
    , del : Bool
    }


type Msg
    = NoOp
    | OnDelete Int


update : Msg -> Handout -> Handout
update message model =
    case message of
        NoOp ->
            model

        OnDelete id ->
            if id == model.id then
                { model | del = True }

            else
                model


new : Int -> String -> Bool -> Handout
new i s d =
    { id = i
    , title = s
    , del = d
    }


insaneHandout : Handout -> Html Msg
insaneHandout model =
    li []
        [ div [ class "insane-card card" ]
            [ div [ class "f1" ]
                [ open model.title
                ]
            , div [ class "f1" ]
                [ secret
                ]
            ]
        , viewDeleteButton model
        ]


viewDeleteButton : Handout -> Html Msg
viewDeleteButton ho =
    span []
        [ button [ onClick (OnDelete ho.id) ] [ text "x" ]
        ]


secret =
    div [ class "card black insane-wrapper", style "width" "190px", style "height" "300px" ]
        [ div [ class "card-title white-text", style "flex" "1", style "text-align" "center" ] [ text "Handout" ]
        , secretInnerCard
        ]


secretInnerCard =
    div
        [ class "card white"
        , style "display" "flex"
        , style "width" "180px"
        , style "height" "290px"
        , style "flex-direction" "column"
        , style "justify-content" "center"
        , style "align-items" "center"
        ]
        [ div
            [ class "card black"
            , style "width" "170px"
            , style "height" "280px"
            , style "margin-bottom" "0.5rem"
            ]
            [ div [ class "white-text font-s", style "flex" "1", style "text-align" "center" ] [ text "秘密" ]
            , secretInnerWrapper
            , div [ class "white-text font-s insane-secret-footer" ] [ text "この秘密を自分から明らかに" ]
            , div [ class "white-text font-s insane-secret-footer" ] [ text "することはできない" ]
            ]
        ]


secretInnerWrapper =
    div [ class "insane-wrapper" ]
        [ secretMain
        ]


secretMain =
    div
        [ class "card white"
        , style "width" "160px"
        , style "height" "180px"
        , style "margin" "1px"
        ]
        [ secretShock
        , secretContent
        ]


secretShock =
    div
        [ class "row font-m"
        , style "border-bottom" "3px double #bbb"
        , style "margin-bottom" "3px"
        ]
        [ div [ class "col s4", style "border-right" "solid 1px #bbb", style "padding" "0" ] [ text "ショック" ]
        , div [ class "col s8 font-ss" ] [ text "" ]
        ]


secretContent =
    div [ class "card-content font-ss", style "padding" "0", style "margin" "2px" ]
        [ text "てすと"
        ]



--


open title =
    div [ class "card white insane-wrapper", style "width" "190px", style "height" "300px" ]
        [ div [ class "card-title black-text", style "flex" "1", style "text-align" "center" ]
            [ text title
            ]
        , openInnerCard
        ]


openInnerCard =
    div
        [ class "card black"
        , style "display" "flex"
        , style "width" "180px"
        , style "height" "290px"
        , style "flex-direction" "column"
        , style "justify-content" "center"
        , style "align-items" "center"
        ]
        [ div
            [ class "card white"
            , style "width" "170px"
            , style "height" "280px"
            , style "margin-bottom" "0.5rem"
            ]
            [ div [ class "black-text font-s", style "flex" "1", style "text-align" "center" ] [ text "使命" ]
            , openInnerWrapper
            ]
        ]


openInnerWrapper =
    div [ class "insane-wrapper" ]
        [ openMain
        ]


openMain =
    div
        [ class "card white"
        , style "width" "160px"
        , style "height" "220px"
        , style "margin" "1px"
        ]
        [ openContent
        ]


openContent =
    div [ class "card-content font-ss", style "padding" "0", style "margin" "2px" ]
        [ text "てすと"
        ]

```

### 作業履歴
[環境構築](https://github.com/hibohiboo/develop/tree/d10f2da1feb75f090d6714af7c4c73be220c7773/tutorial/lesson/elm/elm-card)
[HTMLファイル追加](https://github.com/hibohiboo/develop/tree/95b139debb63953b660ab5ce0de78b66bec84efe/tutorial/lesson/elm/elm-card)
[jsファイル追加](https://github.com/hibohiboo/develop/tree/e46eeb8980fa124f500081d152c6d3f9eb7ca4b2/tutorial/lesson/elm/elm-card)
[elmを一部適用](https://github.com/hibohiboo/develop/tree/7e64462bc1acecf1a0f780c00e175b6af374069b/tutorial/lesson/elm/elm-card)
[elmを一部適用](https://github.com/hibohiboo/develop/tree/8df449ce1520857f15b76f47eca6f27f80fdfe2b/tutorial/lesson/elm/elm-card)  [*][*6]
[ハンドアウト作成](https://github.com/hibohiboo/develop/tree/1bfcca589058e88007d2cf9f1e52dd67768a3659/tutorial/lesson/elm/elm-card) 
[ハンドアウトを別ファイルに分割](https://github.com/hibohiboo/develop/tree/266d81e5fd35f4893e230bd827a81bcbe68eff32/tutorial/lesson/elm/elm-card)
[ハンドアウト一覧を別ファイルに分割](https://github.com/hibohiboo/develop/tree/6b8b65530d8ed5139516dd5d0fa812c9cd8d013f/tutorial/lesson/elm/elm-card) 
[入力欄表示](https://github.com/hibohiboo/develop/tree/97c609a542e2f52e19ef728a7ab72f360310edc8/tutorial/lesson/elm/elm-card)
[処理の移譲](https://github.com/hibohiboo/develop/tree/98f97438fc3f9bc6610d684da7abed146bca3ce1/tutorial/lesson/elm/elm-card)
[追加メソッド切り出し](https://github.com/hibohiboo/develop/tree/f5ede5a569326bed906de05515d65c7b7dda0f99/tutorial/lesson/elm/elm-card)
[削除機能追加](https://github.com/hibohiboo/develop/tree/750d6ff8930f8323498cb8caf6da5a911bc787ae/tutorial/lesson/elm/elm-card)


## Next
[JSONで通信を行ってみた](https://qiita.com/hibohiboo/items/33a8dcf462bd8e4a55e0)

## 参考

[Elm 0.18で作るTodoアプリ(1)][*7]
[Elm 0.19 の主な変更点][*1]

[*1]:http://jinjor-labo.hatenablog.com/entry/2018/08/23/142026
[*2]:https://qiita.com/hibohiboo/items/b19519b4a9dbb5ec11b0
[*3]:https://ema-hiro.hatenablog.com/entry/2017/10/12/015748
[*4]:https://github.com/elm-community/elm-webpack-loader/pull/142/files
[*5]:https://github.com/jantimon/html-webpack-plugin
[*6]:https://github.com/avh4/elm-upgrade
[*7]:https://qiita.com/tomluck/items/872787cda6682834a3a1