Elm0.19でlocalStorage利用のTODOアプリ(3)port
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)を実装する
- 1. Elm0.19でlocalStorage利用のTODOアプリ目次
- 2. 開発環境
- 3. ElmのPortについて
- 4. 新規Elmアプリ作成
- 5. index.htmlの作成
- 6. src/Main.elmの作成(Browser.elementテンプレート)
- 7. elm/jsonパッケージのインストール
- 8. mainについて
- 9. MODELの作成
- 10. UPDATEの作成
- 11. VIEWの作成 (1)
- 12. いろいろ修正
- 13. src/Main.elmをコンパイルしてmain.jsを作成
- 14. VIEWの編集(2)
- 15. ソースコードとDEMOサイト
- 16. Enterを押すとTODO追加とデフォルトでInputが選択されるように変更した改良Version
- 17. 参考にした本
Elm0.19でlocalStorage利用のTODOアプリ目次
- Elm0.19でlocalStorage利用のTODOアプリ(1)フラグflags
- Elm0.19でlocalStorage利用のTODOアプリ(2)elm/timeでMonthの表示
- Elm0.19でlocalStorage利用のTODOアプリ(3)port
開発環境
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を参考にしました。
ディスカッション
コメント一覧
まだ、コメントがありません