スポンサーリンク

Elm0.19でlocalStorage利用のTODOアプリ(3)port

2019年5月8日

ElmでTodoアプリを作りたいシリーズの第2回では、flagsを用いて、elmアプリ初期化時にindex.html(JavaScript)からmain.js(src/Main.elmをコンパイルしたもの)に複数のデータを送り、Monthを含む日時の表示を行ってみました。

今回は、 https://github.com/evancz/elm-todomvc
https://qiita.com/A_kirisaki/items/4c5343426d9de976ac72 を写経して、localStorage利用のTODOアプリの作成を行ってみたいと思います。

以下の5つを行いたいと思います。

  • src/Main.elmから elm make src/Main.elm --output=main.jsにより、main.jsを作成し、index.html(JavaScript)から読み込む
  • 初期化時に、フラグflagsでindex.html(JavaScript)からElm(正確には、src/Main.elmをコンパイルしたmain.js)に、localStorageに保存されたTODOのリストを送る
  • 初期化時に、src/Main.elmにおいて、flagsでTODOのリストを受け取る
  • 以降、src/Main.elmにおいてTODOリストが更新されたら、その都度、ポートportでTODOリストすべてのデータをindex.html(JavaScript)に送り、index.html(JavaScript)からlocalStorageにそのデータを保存する。
  • Elmアプリ上で、CRUD(Create, Read, Update, Delete)を実装する

スポンサーリンク

Elm0.19でlocalStorage利用のTODOアプリ目次

開発環境

Windows 10 Pro
Chrome
VisualStudioCode 1.32.3
git version 2.20.1.windows.1
nvm 1.1.7
node 10.2.0
npm 6.4.1
elm 0.19.0-bugfix6
elm-format 0.8.1
VisualStudioCodeの拡張機能でelmをインストールして、settings.jsonに以下のようにelmを設定。
(『Alt + Shift + F』と『Ctrl + S』を使用。)

    "[elm]": {
        "editor.formatOnSave": true
    },

ElmのPortについて

ポートPortを使うと、Elmの実行中にJavaScriptとデータをやり取りすることができます。今回は、Elmアプリ上でTODOリストをCreate, Read, Update, Deleteする都度、Elmアプリからindex.html(JavaScript)にTODOリスト全体のデータを送り、localStorageに保存できるようにしたいと思います。 今回は、index.html(JavaScript)からElmへのデータの受け渡しにはportは使わず、Elmアプリ初期化時にflagsのみ利用したいと思います。

参考: https://guide.elm-lang.jp/interop/ports.html

ElmからJavaScriptにデータを送信するとき

port module Main exposing (..)

port 関数名 : 送信するデータの型 -> Cmd msg

Browser.elementを使いたいと思います。

間違っているかもしれませんが、以下のtemplateを作ってみたので、使ってみたいと思います。

新規Elmアプリ作成

C:/elm/フォルダの中にelm-todo-localstorage/ フォルダを作成し、

C:/elm/ フォルダの中にelm-todo-localstorage/ フォルダを作成し、フォルダをVisualStudioCodeで開き、Ctrl+@ でコマンドプロンプトを開き、以下を入力。(何か聞かれたらEnterを押します。)

elm init

すると、src/ フォルダとelm.jsonが作成されます。

index.htmlの作成

index.htmlを新規作成し、 
https://gist.github.com/adash333/ab93b0cc6648fde606b990752844ca87
にある、index.htmlをコピペしてから、余計なコードを削除しておきます。

また、cssフレームワークbulmaを読み込んでおきます。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
  • Elmアプリ初期化時にflagsを経由してJavaScriptからElmへデータを伝えるために、var app = Elm.main.init({});の中に、 flags: startingStateの などのように記載します。
  • ElmアプリでTODOリストが更新されたときに、TODOリストの中身すべてをlocalStorageに保存しなおすために、 app.ports.setStorage.subscribe() のように記載します。
<script type="text/javascript">
  var storedState = localStorage.getItem('elm-todo-save');
  var startingState = storedState ? JSON.parse(storedState) : null;
  var app = Elm.Main.init({ 
    flags: startingState 
  });
  app.ports.setStorage.subscribe(function(state) {
    localStorage.setItem('elm-todo-save', JSON.stringify(state));
  });
</script>

src/Main.elmの作成(Browser.elementテンプレート)

src/ フォルダに、src/Main.elm ファイルを新規作成し 、
https://gist.github.com/adash333/ab93b0cc6648fde606b990752844ca87
にある、Main.elmをコピペします。

Elmでフラグflagsとポートportを扱うには、index.html(JavaScript)とデータをやり取りするために、Elm側でBrowser.element関数(またはBrowser.document関数、Browser.application関数)を使用します。

Elmアプリでは、initでフラグflagsを受け取り、portでデータをindex.html(JavaScript)に送る(and受け取る)ことができるようになるそうです。

elmのエラーが、 elm install elm/json しなさいと言ってくるので、その通りに行います。elmのよいところとしては、『エラーメッセージが丁寧!』というところがあります。

(追記)
→と、ドヤ顔で書いてしまいましたが、elm/jsonパッケージは、最初からindirectには入っていたようです。以下を追加したら、エラーメッセージがなぜか消えました。

参考: https://guide.elm-lang.jp/effects/json.html

import Json.Decode exposing (Decoder, field, string)

elm/jsonパッケージのインストール

エラーメッセージで言われたとおりに、elm/jsonパッケージをインストールします。ターミナル画面に以下を入力します。 (何か聞かれたらEnterを押します。)

elm install elm/json

(elm install elm/jsonしたのに、importのところのエラーが消えていない理由はわかりません、、、(汗))

mainについて

テンプレートにされているmainですが、Elmの初期化の方法として、Browser.element関数を指定しています。

以下のコードのコピペですが、update関数が呼ばれるたびに、localStorageへTodoリスト全体のデータを送っています。

https://github.com/evancz/elm-todomvc/blob/master/src/Main.elm

---- PROGRAM ----


main : Program Flags Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = updateWithStorage
        , subscriptions = subscriptions
        }


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

port setStorage : Model -> Cmd msg


{-| We want to `setStorage` on every update. This function adds the setStorage
command for every step of the update function.
-}
updateWithStorage : Msg -> Model -> ( Model, Cmd Msg )
updateWithStorage msg model =
    let
        ( newModel, cmds ) =
            update msg model
    in
        ( newModel
        , Cmd.batch [ setStorage newModel, cmds ]
        )

参考:https://qiita.com/jinjor/items/245959d2da710eda18fa

@jinjor
2018年12月03日に更新
Elm 0.19 の初期化方法 6 種類

MODELの作成

MODEL, UPDATE, VIEWの順に作成していきます。

Elmアプリがもつ状態として、

  • newTodo
  • todoList

を定義します。

また、elmアプリ初期化時に、index.html(JavaScript)から、flagsを介して json形式のtodoListを受け取り、initに代入します。

-- MODEL

type alias Model =
    { newTodo : String
    , todoList : List String
    }

model : Model
model = { newTodo = ""
        , todoList = []
        }

init : Maybe Model -> ( Model, Cmd Msg )
init maybeModel =
  ( Maybe.withDefault emptyModel maybeModel
  , Cmd.none
  )

参考: https://qiita.com/A_kirisaki/items/4c5343426d9de976ac72

UPDATEの作成

引き続き、 https://qiita.com/A_kirisaki/items/4c5343426d9de976ac72  をコピペします。

UIから受け取るメッセージ群としては、以下の3つとします。 (その結果、UPDATEがMODELを更新する?)

あ、CRUDのUpdate(編集)は今回はパスして、create, read, deleteのみにします。。。

  • Change String (新規Todoの入力中の文字列を変更)
  • Add      (新規Todoの追加)
  • Delete Int   (特定のTodoの削除)

https://qiita.com/A_kirisaki/items/4c5343426d9de976ac72 では、以下のように素晴らしい説明をしてくださっているのでそのままコピペさせていただきます。

(引用ここから)
Change Stringはフォームに入力があったとき、
AddはAdd Todoボタンが押されたとき、
Delete Intは削除ボタンが押されたとき
にそれぞれ送られてくるメッセージとします。
(引用ここまで)

-- UPDATE

type Msg = Change String | Add | Delete Int

update : Msg -> Model -> Model
update msg model =
    let
        isSpace = String.trim >> String.isEmpty
    in
        case msg of
            Change str ->
                { model | newTodo = str }
            Add ->
                if isSpace model.newTodo then
                    model
                else
                    { model | todoList = model.newTodo :: model.todoList
                    , newTodo = "" }
            Delete n ->
                let
                    t = model.todoList
                in          
                    { model |
                          todoList = List.take n t ++ List.drop (n + 1) t}

VIEWの作成 (1)

VIEWも、引き続き、https://qiita.com/A_kirisaki/items/4c5343426d9de976ac72 をコピペさせていただきます。その後に、bulmaで少しだけCSSを追加します。

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input [ type_ "text"
                , placeholder "input your todo"
                , onInput Change
                , value model.newTodo] []
        , button [ onClick Add ] [ text "add todo" ]
        , div [] (showList model.todoList)
         ]

showList : List String -> List (Html Msg)
showList =
    let
        todos = List.indexedMap Tuple.pair
        column (n,s) = div []
                   [ text s
                   , button[ onClick (Delete n) ] [ text "×" ]
                   ]
    in
        todos >> List.map column

うまくいかない。。。

挫折かも、、、

参考:indexedMap関数
https://package.elm-lang.org/packages/elm/core/latest/List#indexedMap

いろいろ修正

まず変更したのがupdateの型

2つのソースコードをパクったため、型が異なってしまっていました。

(変更前)
update : Msg -> Model -> Model

(変更後)
update : Msg -> Model -> ( Model, Cmd Msg )

updateのコードの内容も、", Cmd.none"を追加しまくりました。

--update : Msg -> Model -> Model


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        isSpace =
            String.trim >> String.isEmpty
    in
    case msg of
        Change str ->
            ( { model | newTodo = str }
            , Cmd.none
            )

        Add ->
            if isSpace model.newTodo then
                ( model, Cmd.none )

            else
                ( { model
                    | todoList = model.newTodo :: model.todoList
                    , newTodo = ""
                  }
                , Cmd.none
                )

        Delete n ->
            let
                t =
                    model.todoList
            in
            ( { model
                | todoList = List.take n t ++ List.drop (n + 1) t
              }
            , Cmd.none
            )

上記でelm make src/Main.elm –output=main.js は実行できたのですが、index.htmlを開いても真っ白で泣きそうになりました。

もう一つは、 https://github.com/evancz/elm-todomvc  を自分の環境で実行した後であったため、index.htmlの中のlocalStorageの場所を

getItem('elm-todo-save’)

のままにすると、以下のようなエラーで画面が真っ白になってしまっていました。(ChromeのCtrl+Shift+I でやっと気づきました。)

index.htmlを以下のように変更して、無事、画面を表示することができました。ほっ、、、

しかし、まだこの状態では、index.htmlを更新すると、localStorageの内容がすべて消去されてしまい、localStorageの意味がなくなってしまいました。

flagsという言葉がないなと思っていたので、適当に、以下のように変更したら、うまくいったような感じがします。。。(バグが潜んでいる可能性はあります。)

(変更前)

init : Maybe Model -> ( Model, Cmd Msg )
init maybeModel =
    ( Maybe.withDefault emptyModel flags
    , Cmd.none
    )

(変更後)

init : Maybe Model -> ( Model, Cmd Msg )
init flags =
    ( Maybe.withDefault emptyModel flags
    , Cmd.none
    )

src/Main.elmをコンパイルしてmain.jsを作成

VisualStudioCodeでCtrl+@でターミナル画面を開き、以下を入力します。

elm make src/Main.elm --output=main.js

その後、index.htmlをChromeで開くと、以下のような画面になります。

VIEWの編集(2)

bulmaでlevel-rightとかいろいろやりましたが、うまくいかず、あきらめました。(class “list-item" の中に class “level" を作ろうとしてうまくいかなかったのかもしれません。)

-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ section [ class "hero is-primary" ]
            [ div [ class "hero-body" ]
                [ div [ class "container" ]
                    [ h1 [ class "title" ]
                        [ text "Elm Todo localStorage" ]
                    ]
                ]
            ]
        , section [ class "section" ]
            [ div [ class "container" ]
                [ section []
                    [ figure [ class "image container is-128x128" ]
                        [ img [ src "./logo.svg" ] []
                        ]
                    ]
                ]
            ]
        , section [ class "section" ]
            [ div [ class "container" ]
                [ div [ class "field has-addons" ]
                    [ div [ class "control" ]
                        [ input [ class "input", type_ "text", placeholder "input your todo", onInput Change, value model.newTodo ] []
                        ]
                    , div [ class "control" ]
                        [ a [ class "button is-info", onClick Add ] [ text "add todo" ]
                        ]
                    ]
                , ul [ class "list is-hoverable" ]
                    (showList model.todoList)
                ]
            ]
        , footer [ class "footer" ]
            [ div [ class "content has-text-centered" ]
                [ p []
                    [ a [ href "http://i-doctor.sakura.ne.jp/font/?p=37627" ] [ text "WordPressでフリーオリジナルフォント2" ]
                    ]
                ]
            ]
        ]


showList : List String -> List (Html Msg)
showList =
    let
        todos =
            List.indexedMap Tuple.pair

        column ( n, s ) =
            li [ class "list-item" ]
                [ div []
                    [ text s
                    , a [ class "button is-danger", onClick (Delete n) ] [ text "delete" ]
                    ]
                ]
    in
    todos >> List.map column

さらにいろいと試してみたのですが、TODOリストと削除ボタンの上下方向がずれているのは、どうしてもうまく治せませんでした。。。

ソースコードとDEMOサイト

ソースコード:https://github.com/adash333/elm-todo-localstorage 

DEMOサイト: https://adash333.github.io/elm-todo-localstorage/

Enterを押すとTODO追加とデフォルトでInputが選択されるように変更した改良Version

create-elm-appで作成し、少し改良してみました。ほとんど同じですが、こちらの方がわずかに使いやすいかと思います。

ソースコード2: https://github.com/adash333/elm-todo-localstorage2

DEMOサイト2: https://modest-noether-b47b5e.netlify.com

参考1: https://stackoverflow.com/questions/40113213/how-to-handle-enter-key-press-in-input-field

参考2: https://stackoverflow.com/questions/31901397/how-to-set-focus-on-an-element-in-elm

参考にした本

以下の本のP172-P178を参考にしました。