スポンサーリンク

『Firestoreで読み書きする』をElmで写経にtryしてみるがAddさえまともにできず

2019年5月16日

Firestoreを用いてElmでチャットアプリを作りたいと思いました。

Firebase Realtime DatabaseとElmのチャットアプリは、前回、ソースコードをダウンロードして実行してみました。

こちらのコードを、
https://github.com/ababup1192/elm-firebase/blob/master/src/index.js
とあわせて、Firestoreで認証付きチャットアプリを作りたいのですが、今の私には無理です。

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

JavaScriptフレームワークである、VueやAngularには、firebaseとの連携のためのツールが用意されていますが、Elmの場合は、フラグやポートを通して、素のJavaScriptでFirebaseとElmの橋渡しをしなければなりません。

ということで素のJavaScriptでFirestoreとの連携の方法を調べるために、以下のサイトを写経してみたいと思います。

しかし、これなら、Elmいらなくて、素のJavaScriptでやればいいのでは???と思ってしまったり、しまわなかったり、、、まあ、趣味ですから、とりあえず、やってみます。

  https://blog.katsubemakito.net/firebase/firestore_readwrite_1

スポンサードリンク

開発環境

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
    },
create-elm-app 3.0.6

新規Firebaseアプリの作成とFirestoreの初期設定

Googleアカウントが必要です。以下の記載の前半を参考にして、Firebaseアプリを作成し、データベースはFirestoreを設定します。

なお、今回は、Firebaseプロジェクトの名前は”image-upload”とし、セキュリティールールはテストモード(誰でも読み書き可能、、、)とします。

新規create-elm-appアプリの作成

C:/elm/ フォルダをVisualStudioCodeで開き、Ctrl+@でターミナル画面を開いて、以下を入力します。

create-elm-app elm-firestore-crud
cd elm-firestore-crud
elm-app start

参考: https://github.com/halfzebra/create-elm-app

public/index.htmlの編集

Firebaseとbulmaが利用できるように、コードを挿入します。

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

参考: https://firebase.google.com/docs/web/setup

src/index.jsの編集

src/index.jsに、firebaseConfigや、elm用のportなどを記載していきます。

const config = {} の中身は、上記のFirebaseの初期設定でのグレーの部分のご自身のアプリのデータをコピペしてください。

import './main.css';
import { Elm } from './Main.elm';
import registerServiceWorker from './registerServiceWorker';

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

const config = {
  apiKey: "<YOUR-API-KEY>",
  authDomain: "<YOUR-APP>.firebaseapp.com",
  databaseURL: "https://<YOUR-APP>.firebaseio.com",
  projectId: "<YOUR-APP-ID>",
  storageBucket: "<YOUR-APP>.appspot.com",
  messagingSenderId: "<YOUR-APP-SENDERID>"
};
firebase.initializeApp(config);
const DB = firebase.firestore();

registerServiceWorker();

Createの実装

https://blog.katsubemakito.net/firebase/firestore_readwrite_1 によると、以下のように解説されています。

コレクションに対してadd()メソッドを実行すると、コレクション内にドキュメントが作成されます。

指定したコレクションが存在しない場合は自動的に新規作成されるとのことです。

db.collection("users").add({
  name: "マイメロ",
  age: 27
})
.then((doc) => {
  console.log(`追加に成功しました (${doc.id})`);
})
.catch((error) => {
  console.log(`追加に失敗しました (${error})`);
});

これをどうやってElmと関連づけるか、、、

まず、どんなアプリかというと、以下の4つのボタンがあり、TODO(文字列)のリストが表示されたものを作ろうとします。(→写経元サイト

  • 追加
  • 表示
  • 更新
  • 削除

MODELとしては、
https://github.com/adash333/elm-todo-localstorage2/blob/master/src/Main.elm
を参考にすると、newTodoとtodoListでしょうか?

---- MODEL ----


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


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


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

しかし、これだと、どうしても、index.jsで新規todoの内容を受け取って、firestoreに送ることが難しい。

// index.jsはこうなる?
app.ports.postTodo.subscribe(text => {
  //const user = firebase.auth().currentUser;
  DB.collection('users').add({
    todo: text
  });
});
---- Main.elmのUPDATEはこうなる? ----
---- UPDATE ----


type Msg
    = Change String
    | Add


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Change str ->
            ( { model | newTodo = str }
            , Cmd.none
            )

        Add ->
            ( { model
                | todoList = model.newTodo :: model.todoList
                , newTodo = ""
              }
            , Cmd.none
            )

ということで、MODELからやりなおし。

---- MODEL ----


type alias Todo =
    String


type alias Model =
    { todo : Todo, history : List Todo }


emptyModel : Model
emptyModel =
    { todo = "", history = [] }


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

Elmのエラーの言われるがままに、いろいろ変更すると、なんだか、UPDATEはこんな感じでしょうか。。。

---- UPDATE ----


type Msg
    = ChangeTodo Todo
    | Submit


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ChangeTodo todo ->
            ( { model | todo = todo }
            , Cmd.none
            )

        Submit ->
            ( { model
                | todo = ""
                , history = model.history
              }
            , postTodo
                model.todo
            )

たかがAddするだけなのに、何時間もかかっています。。。とほほ。。。

疲れたのでGistに、、、

しかし、FirebaseにログインしてFirebaseのデータベースの内容を見ても、全く反映されず。。。Elm,,,無理かも。。。

次の日に、firebase realtime databaseを用いたTODOアプリのソースコードを見つけました!感謝!

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

その後、、、特定のデータは追加できましたが、、、

むちゃくちゃですが、とりあえず、firestoreにデータを追加することはできました。まだまだ調整が必要ですが、、、

index.html

<!DOCTYPE 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>
  <script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-firestore.js"></script>
  <script>
  var firebaseConfig = {
    apiKey: "XXXXXXXXXXX",
    authDomain: "image-upload-XXXXXXXXXXX.firebaseapp.com",
    databaseURL: "https://image-upload-XXXXXXXXXXX.firebaseio.com",
    projectId: "image-upload-XXXXXXXXXXX",
    storageBucket: "XXXXXXXXXXX.appspot.com",
    messagingSenderId: "1XXXXXXXXXXX",
    appId: "1XXXXXXXXXXX"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

  const DB = firebase.firestore();

  var storedState = localStorage.getItem('elm-todo-save3');
  var startingState = storedState ? JSON.parse(storedState) : null;

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

  app.ports.setStorage.subscribe(function(state) {
    localStorage.setItem('elm-todo-save3', JSON.stringify(state));
    DB.collection("users").add({
        name: "ノラクロ",
        age: 55
      })
      .then((doc) => {
        console.log(`ノラクロ追加に成功しました (${doc.id})`);
      })
      .catch((error) => {
        console.log(`ノラクロ追加に失敗しました (${error})`);
      });
    DB.collection("foo").add(
      state
      )
      .then((doc) => {
        console.log(`foo追加に成功しました (${doc.id})`);
      })
      .catch((error) => {
        console.log(`foo追加に失敗しました (${error})`);
      });
  });
  /*
  app.ports.push.subscribe(text => {
    const user = firebase.auth().currentUser;
    DB.collection('foo').doc(user.uid).set({input: text});
  });
  */
  </script>
</body>

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, field, string)



---- MODEL ----


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


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


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



---- UPDATE ----


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


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 )



---- 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 localStorage3" ]
                    ]
                ]
            ]
        , section [ class "section" ]
            [ div [ class "container" ]
                [ section []
                    [ figure [ class "image container is-128x128" ]
                        [ img [ src "./logo.svg" ] []
                        ]
                    ]
                ]
            ]
        , 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=37627" ] [ 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)



---- PROGRAM ----


main : Program (Maybe Model) Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = updateWithStorage
        , subscriptions = \_ -> 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 ]
    )

上記2つのファイル作成後、

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

してから、index.htmlを開くと、なんか文字を入力するたび(ElmのUPDATEが呼ばれるたび)に、Firestoreにデータが追加されまくっていました。

elm以前に、素のJavaScriptの知識が必要ですね。。。

続きはまた今度、、、

スポンサーリンク