【Elm】Firebase Firestoreにデータを追加する

## はじめに
[前の記事](https://qiita.com/syobot/items/487f4efa6a8c98e74c5c)でElmでFirebase Authする方法を書いたので、その流れでFirestoreにデータを追加する方法を書きます。
基本的にやってること同じなので前と似たようなことしか書いてないです（なんで書いた？）
間違ったこと書いてたら罵詈雑言でも構いませんので教えてください。

## やりたいこと
Google SignIn後に、フォームにTitleとContentsを入力して、Saveボタンを押すと、Firestoreに文字列を追加します。
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/4a6ef20a-0c43-e490-7898-9a719ba0a309.gif)

以下がFirestoreの画面です。ちゃっかり追加した時刻も入れています。
![スクリーンショット 2021-07-23 17.57.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516892/e6921a47-a82e-5629-033a-df9231233038.png)

## 開発環境
Mac
[create-elm-app](https://github.com/halfzebra/create-elm-app)使用

## コード全体
[こちら](https://github.com/syobott/elm_firebase_add_firestore/tree/master/src)参照ください
index.jsには各自のFirebaseのapiKey等を記載してください。

## 大まかな流れ
1. Firebase FireStoreの初期設定
2. Elm側でコンテンツ入力フォームを作る
3. フォームに入力されたコンテンツ情報をJavaScriptに送る
4. JavaScriptでコンテンツ情報を受け取り、Firestoreに追加する
5. 追加結果をElmで受け取る

## 1. Firebase FireStoreの初期設定
ここでは特に説明しません。ググればすぐに出てきます。[この記事](https://www.techpit.jp/courses/13/curriculums/14/sections/133/parts/515)とか良さそうです（適当）

## 2. Elm側でコンテンツ入力フォームを作る
入力フォームの書き方は[前の記事](https://qiita.com/syobot/items/487f4efa6a8c98e74c5c)の「2. Elm側でemail、パスワード入力フォームを作る」で書いたので省略します
←めんどくさいだけ

## 3. フォームに入力されたコンテンツ情報をJavaScriptに送る

### PORT
フォームに入力したコンテンツ情報を、portを使ってJavaScriptに送り、JavaScriptでfirestoreに追加します。
portに関しての詳しい解説は[公式ドキュメント](https://guide.elm-lang.jp/interop/ports.html)や他の方の記事を読むと良いと思います。

```Elm
port saveContents : Encode.Value -> Cmd msg
```

### UPDATE
例によってコンテンツ情報をJavaScriptに送るためには、"エンコード"する必要があります。
エンコードに関しての詳しい解説は[公式ドキュメント](https://package.elm-lang.org/packages/elm/json/latest/Json.Encode)や他の方の記事を読むと良いと思います。

```Elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg =
    case msg of
        ...
        SaveMsg saveMsg ->
            updateSaveContent saveMsg
        ...

-- エンコードした文字列をportに引数として渡す
updateSaveContent : SaveMsg -> Model -> ( Model, Cmd Msg )
updateSaveContent msg model =
    case msg of
        SaveContent ->
            ( model, saveContents <| contentsInfoEncoder model )

-- titleとcontentのエンコード
contentsInfoEncoder : Model -> Encode.Value
contentsInfoEncoder model =
    Encode.object
        [ ( "title", Encode.string model.contents.title )
        , ( "content", Encode.string model.contents.content )
        ]
```


### VIEW
コンテンツをFirestoreに追加するSaveボタンも作っておきます。

```Elm

view : Model -> Html Msg
view model =
    div []
        [ ...
        , Html.map SaveMsg viewSaveButton
        ...
        ]

viewSaveButton : Html SaveMsg
viewSaveButton =
    div []
        [ button [ onClick SaveContent ] [ text "Save" ] ]

```

## 4. JavaScriptでコンテンツ情報を受け取り、Firestoreに追加する

### 初期設定
JavaScript側の初期設定です。
自分はcreat-elm-appを使っているので、appの定義とかは皆さんと異なるかもしれません。
とりあえずappの定義は忘れずにやってください。
firebase関係のコードは基本的に同じになると思います。

```JavaScript
...
import firebase from "firebase/app";
import "firebase/analytics";
import "firebase/auth";
import "firebase/firestore";

// ここが異なるかも
const app = Elm.Main.init({
  node: document.getElementById("root"),
});

const firebaseConfig = {
  // 各自のapi key等を記載してください
};

// firebaseの初期化
firebase.initializeApp(firebaseConfig);
const Googleprovider = new firebase.auth.GoogleAuthProvider();
const DB = firebase.firestore();
```

### コンテンツ情報を受け取り、Firestoreに追加する

[こちら](https://cloud.google.com/firestore/docs/manage-data/add-data?hl=ja)の公式ドキュメントに従って、Firestoreにデータを追加します。

portを使用してJavaScriptとデータのやり取りをしたい場合、以下のように、app.ports.(Elm側で記載したポート名).subscribe...というように記載します。

```JavaScript
app.ports.saveContents.subscribe((Contents) => {
  //現在ログインしているユーザー
  const user = firebase.auth().currentUser;
  //サーバーの時間
  const time = firebase.firestore.FieldValue.serverTimestamp();
  //コンテンツを追加する参照先（.doc()でidを自動生成）
  const ref = DB.collection("Test").doc(user.uid).collection("Contents").doc();
  ref
    //コンテンツの追加を行う
    .set(
      {
        //Elm側でエンコードしたtitle, content
        Title: Contents.title,
        Content: Contents.content,
        //データ追加時のサーバーの時間
        timestamp: time,
      },
      //コンテンツを上書きするかしないかの指定。Trueにすると上書きせず、ドキュメントにコンテンツが新規追加されていく。
      { merge: true }
    )
    //追加に成功したらPass, 失敗したらFailをElmに返す
    .then(() => {
      app.ports.validateFirestore.send("Pass");
    })
    .catch(() => {
      app.ports.validateFirestore.send("Fail");
    });
});
```

## 5. 追加結果をElmで受け取る

### PORT
JavaScriptから認証状態の文字列を受け取る用のportsを用意します。
文字列を受け取るので(String -> msg)にしています。

```Elm
port validateFirestore : (String -> msg) -> Sub msg
```

### MODEL
追加の状態を表すカスタム型を定義します。

```Elm
type FirestoreState
    = Pass
    | Fail

type alias Model =
    { ...
    , firestoreState : FirestoreState
    ...
    }
```

### SUBSCRIPTIONS
portで受け取ったデータをElmのUPDATEで使用するために、SUBSCRIPTIONに以下のように記載します。
カスタム型を使用しているので、[Sub.map](https://package.elm-lang.org/packages/elm/core/latest/Platform.Sub#map)を使用しています。
また以下のように複数のSbuscriptionを書きたいときは、[Sub.batch](https://package.elm-lang.org/packages/elm/core/latest/Platform.Sub#batch)を使用します。

```Elm
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ validateAuthState ValidateAuthState
            |> Sub.map ValidateAuthStateMsg
        , validateFirestore ValidateFirestore
            |> Sub.map ValidateFirestoreMsg
        ]
```


### UPDATE
JavaScriptから受け取った文字列に応じて、firebaseStateを更新します。

```Elm
type ValidateFirestoreMsg
    = ValidateFirestore String

type Msg
    = ...
    | ValidateFirestoreMsg ValidateFirestoreMsg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg =
    case msg of
        ...
        ValidateFirestoreMsg validateSaveContentsMsg ->
            updateValidateFirestoreState validateSaveContentsMsg

--受け取った文字列に応じてfirestoreStateを更新する
updateValidateFirestoreState : ValidateFirestoreMsg -> Model -> ( Model, Cmd Msg )
updateValidateFirestoreState msg model =
    case msg of
        ValidateFirestore contentsmsg ->
            case contentsmsg of
                "Fail" ->
                    ( { model | firestoreState = Fail }
                    , Cmd.none
                    )

                "Pass" ->
                    ( { model | firestoreState = Pass }
                    , Cmd.none
                    )

                _ ->
                    ( model, Cmd.none )
```

### VIEW
データ追加の結果を表示します。
firebaseStateに応じて表示する文字列を変更しています。

```Elm
view : Model -> Html Msg
view model =
    div []
        [ ...
         , viewValidateFirestore model
        ]

viewValidateFirestore : Model -> Html Msg
viewValidateFirestore model =
    case model.firestoreState of
        Pass ->
            div []
                [ div [] [ text "Firestore Status: Pass" ]
                ]
        Fail ->
            div []
                [ div [] [ text "Firestore Status: Fail" ] ]
```

以上でできました！

##参考文献
・Qiitaに投稿してくださった記事（参考になります本当にありがとうございます！！）
　[Elm portsでFirebase Firestoreを触ろう！](https://qiita.com/ababup1192/items/f27f9af282d9fa642eb5)

・Firebase系
　[Firebase Firestoreの初期設定]
(https://www.techpit.jp/courses/13/curriculums/14/sections/133/parts/515)
　[Firestoreへのデータの追加](https://cloud.google.com/firestore/docs/manage-data/add-data?hl=ja)

・Elm公式ドキュメント
　[型エイリアス](https://guide.elm-lang.jp/types/type_aliases.html)
　[カスタム型](https://guide.elm-lang.jp/types/custom_types.html)
　[ポート](https://guide.elm-lang.jp/interop/ports.html)
　[Json.Encode](https://package.elm-lang.org/packages/elm/json/latest/Json.Encode)
　[Platform.Sub](https://package.elm-lang.org/packages/elm/core/latest/Platform.Sub)

・関連する自分の記事
　[【Elm】Firebase Authenticationでパスワード認証をする](https://qiita.com/syobot/items/487f4efa6a8c98e74c5c)