スポンサーリンク

Elm0.19でFirestore利用のTODOアプリ(2)ElmからFirestoreへデータの追加(Create)

ElmでFirestore利用のTodoアプリを作りたいシリーズの第1回では、portsを用いて、Firebase Firestoreに保存した

 { “newTodo” : “”, “todoList” :”[“abc”, “111”, “3333333”]” } 

のような文字列を、index.html(JavaScript)からmain.js(src/Main.elmをコンパイルしたもの)に送り、Elm側でレコードrecordに変換(デコード)して表示してみました。

第2回の今回は、Elm側でnewTodoを入力し、Firestoreにデータを追加してみたいと思います。
以下の本のp171-p178を参考に、いろいろ写経してみたいと思います。

スポンサードリンク

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

  • Elm0.19でFirestore利用のTODOアプリ(1)ElmでJSONを扱う(FirestoreからElmへデータを送る)
  • Elm0.19でFirestore利用のTODOアプリ(2)ElmからFirestoreへデータの追加(Create)

開発環境

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からindex.html(JavaScript)にデータを送る

https://github.com/evancz/elm-todomvc のコードを見てみると、以下のように書くのがよさそうです。

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

{- subscriptionsのところの記載は今回は省略 -}

{- portを介して、Elmからindex.html(JavaScript)にデータを送る -}
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.2
-}
updateWithStorage : Msg -> Model -> ( Model, Cmd Msg )
updateWithStorage msg model =
    let
        ( newModel, cmds ) =
            update msg model
    in
    ( newModel
    , Cmd.batch [ setStorage newModel, cmds ]
    )

setStorageという名前のportを通して、Elmからindex.html(JavaScript)にnewModelを送ります。

index.html(JavaScript)でデータを受け取り、Firestoreに書き込む

全く同じデータをlocalStorageに書き込むときは、以下のようなコードでした。

const DB = firebase.firestore();

var docRef = DB.collection('foo').doc('1111111');
app.ports.setStorage.subscribe(function(state) {
  localStorage.setItem('elm-todo-save', JSON.stringify(state));
});

今回は、この中身で、Firestoreに書き込むようにコードを変更します。

const DB = firebase.firestore();

var docRef = DB.collection('foo').doc('1111111');
app.ports.setStorage.subscribe(function(state) {
  docRef.update(state);
});

一文字書き込むたびに、すべてのnewTodoとtodoListをFirestoreに書き込むというトラフィックを使いまくりな仕様ですが、とりあえず、これでよしとします。

削除機能は自動的に実装されることになります

Elmの方でtodoListの一部を削除すると、それと同時に、その時点でのすべてのデータをFirestoreに送る仕様になっているので、削除機能も実装済みとなります。

以上で、create, read, deleteのみで、通信料も膨大となりますが、Firestore利用のTODOアプリが完成しました。。。

ソースコード

Firebase configの部分は、ご自身のものを入力してください。

また、最初、index.htmlの階層で、コマンドプロンプトで以下のように入力します。

elm init
elm install elm/json

さらに、以下の2つのファイルを作成後に、また、index.htmlの階層で、コマンドプロンプトで以下のように入力し、src/Main.elmをmain.jsにコンパイルし、最後にindex.htmlをChromeで開きます。

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

index.html

<html>
  <head>
    <meta charset="UTF-8" />
    <title>Main</title>
    <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>
    <script src="main.js"></script>
  </head>

  <body>
    <div id="elm"></div>

    <!-- Firebase App is always required and must be first -->
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"></script>

    <!-- Add additional services that you want to use -->
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-database.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-messaging.js"></script>
    <script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-functions.js"></script>

    <script>
      // Your web app's Firebase configuration
      // こちらは、ご自身のものに書き換えてください。
      var firebaseConfig = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        projectId: "<PROJECT_ID>",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<SENDER_ID>"
      };
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);

      const DB = firebase.firestore();

      var docRef = DB.collection('foo').doc('1111111');

      docRef.onSnapshot(function(doc) {
        console.log(doc.data());
        console.log(typeof doc.data());
        console.log(typeof JSON.stringify(doc.data()));
        app.ports.read.send(JSON.stringify(doc.data()));
      });

      var app = Elm.Main.init({
        node: document.getElementById('elm')
      });

      app.ports.setStorage.subscribe(function(state) {
        docRef.update(state);
      });
    </script>
  </body>
</html>

src/Main.elm

port module Main exposing (Model, Msg(..), emptyModel, init, main, setStorage, update, view)

import Browser
import Html exposing (Attribute, Html, a, div, figure, footer, form, h1, img, input, li, p, section, text, ul)
import Html.Attributes exposing (autofocus, class, href, placeholder, src, type_, value)
import Html.Events exposing (keyCode, on, onClick, onInput)
import Json.Decode as Json exposing (Decoder, decodeString, field, list, map2, string)



---- MODEL ----


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


fromJsonModel : Decoder Model
fromJsonModel =
    map2 Model
        (field "newTodo" string)
        (field "todoList" (list string))


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


init : () -> ( Model, Cmd Msg )
init _ =
    ( emptyModel
    , Cmd.none
    )



---- UPDATE ----


type Msg
    = Change String
    | Add
    | Delete Int
    | KeyDown Int
    | Read String


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
            )

        KeyDown key ->
            if key == 13 then
                if isSpace model.newTodo then
                    ( model, Cmd.none )

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

            else
                ( model, Cmd.none )

        Read s ->
            ( case decodeString fromJsonModel s of
                Err e ->
                    model

                Ok newModel ->
                    { newModel
                        | newTodo = model.newTodo
                    }
            , Cmd.none
            )



---- 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" ]
                [ form [ class "field has-addons" ]
                    [ div [ class "control" ]
                        [ input [ class "input", type_ "text", placeholder "input your todo", onKeyDown KeyDown, autofocus True, 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 "https://i-doctor.sakura.ne.jp/font/?p=38214" ] [ text "WordPressでフリーオリジナルフォント2" ]
                    ]
                ]
            ]
        ]


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

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


onKeyDown : (Int -> msg) -> Attribute msg
onKeyDown tagger =
    on "keydown" (Json.map tagger keyCode)



---- MAIN ----


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


subscriptions : Model -> Sub Msg
subscriptions model =
    read Read


port read : (String -> msg) -> Sub msg


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.2
-}
updateWithStorage : Msg -> Model -> ( Model, Cmd Msg )
updateWithStorage msg model =
    let
        ( newModel, cmds ) =
            update msg model
    in
    ( newModel
    , Cmd.batch [ setStorage newModel, cmds ]
    )

Netlifyにデプロイ

デプロイというより、そのままindex.htmlとmain.jsをアップロードすれば終了です。

https://modest-kirch-c3a6d2.netlify.com/

セキュリティ的にかなりよろしくないので、できれば、いつか、Google認証を追加したいと思います。

今回参考にした本とサイト

https://github.com/evancz/elm-todomvc

https://github.com/qnoyxu/chat-room

https://qiita.com/qnoyxu/items/ee479b2b96831907e024

https://github.com/ababup1192/elm-firebase

https://qiita.com/ababup1192/items/f27f9af282d9fa642eb5

https://github.com/Goryudyuma/elm-todo

スポンサーリンク