vuetify環境を構築してfirebaseにhostするまでをまとめてみました.part2-todoアプリ作成編

#この記事では.
[part1](https://qiita.com/binary2/items/875a01a3beeb16e53338)では,環境構築を行いました.
今回は,firebaseとの連携を行い実際にtodo-appを作ろうと思います.
(最近流行りの構成なので,vue-firebaseで調べるとたくさんありました.特にオリジナリティはないです.勉強なので..)

[作ったもの](https://todo-app-1d748.firebaseapp.com/)
<img width="1314" alt="スクリーンショット 2019-01-20 22.47.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/283053/6bb6c5db-0798-f268-5a6f-f0b193d4deab.png">


#firebase
たくさん機能があります.各機能を３行でまとめてくれてます.
[Firebaseの各機能を3行で説明する](https://qiita.com/shibukk/items/4a015c5b3296563ac19d)

今回は２つの機能を使います.

- Firebase Realtime Database
- Firebase Authentication

## データ構造

- マスターデータ:category,state
- トランザクションデータ：item

(今にして思うと,stateは優先順位なのでpriority,基本的に複数形で命名すべきでした..)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/283053/0224705c-5907-cf86-4e25-fb2235c14d12.png)

## データ操作
最後にコンポーネントごとのソース張り付けてます(github上にあげろという話ですが..)
抜粋して連携部分のみ貼り付けます.少し整形しています.

###データ取得
2通りのパターンで取得します.同期する方法と1回きり取得する方法です.

```javascript:データ取得
getData:function () {
  // firebaseからデータを取得する
  this.database = firebase.database();
  // アイテムはリアルタイムで更新
  this.spotsRef = this.database.ref("item");
  const _this = this;
  this.spotsRef.on("value", function (snapshot) {
    _this.item = snapshot.val();
  });
  // ここからはマスタデータなので１回取得でいい
  this.spotsRef = this.database
    .ref("category")
    .once("value")
    .then(snapshot => {
      this.category = snapshot.val();
    });
}
```

###データ更新
新規の場合はchild("posts").push().keyで新規keyを取得する必要がある.
削除の場合は違うコマンドがありますが,nullでアップデートすると同様な挙動になるらしいので,今回は全てupdateで対応しました.

```javascript:データ更新
    new: function() {
      const data = { ...this.inputData, isDone: false };
      // 新規のkeyを取得
      const key = firebase
          .database()
          .ref("item")
          .child("posts")
          .push().key;
      }
      // 登録内容objを作る
      const updates = {};
      updates["/item/" + key] = data;
      // 更新
      firebase
        .database()
        .ref()
        .update(updates);
```

## auth設定
今回はgoogleの承認を利用します.対象行をクリックして有効にするだけです.
![image.png](https://qiita-image-store.s3.amazonaws.com/0/283053/236e15b1-764f-816f-4e30-38612f80632e.png)

## 連携方法
今回は,signInWithPopupという方法を使います.このおかげで画面などを用意する必要はありませんでした.

```javascript:ログイン,ログアウト
    // ログイン
    login: function() {
      // グーグルの承認
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase.auth().signInWithPopup(provider);
      // authの情報を取得
      this.onAuth();
    },
    // ログアウト
    logout: function() {
      firebase.auth().signOut();
      this.onAuth();
    },
    onAuth: function() {
      // 今回はgoogleの表示名のみをもらう.本来なら全てを保持したい?
      firebase.auth().onAuthStateChanged(user => {
        this.userName = user ? user.displayName : null;
      });
    }
```

# ToDo-app
機能は以下のみです.

- Form
- List

pageがHomeでForm,Listがコンポーネントです.
Homeは最初にFiarebaseからデータを取得します.
Form,ListはFirebaseにデータを直接更新します.
Form,Listの切り替えはそれぞれemitでopenForm,closeFormという関数を呼び出して処理してます.


## Form
登録フォームをコンポーネントにしました.(特に今回の規模では分けなくてもいいですけど練習として.)
<img width="1127" alt="スクリーンショット 2019-01-20 23.00.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/283053/65e8ffe3-3a04-5201-3ce8-bc115173de79.png">

```vuejs:Form.vue
<template>
  <v-container>
    <v-card>
      <v-card-title>
        <h2>Form</h2>
      </v-card-title>
      <v-card-text>
        <v-form>
          <v-text-field v-model="inputData.title" label="タイトル"></v-text-field>
          <v-text-field v-model="inputData.text" label="内容"></v-text-field>
          <v-select
            v-model="inputData.category"
            :items="category"
            item-text="name"
            item-value="id"
            return-masked-value
            single-line
            label="分類"
          ></v-select>
          <v-select
            v-model="inputData.state"
            :items="state"
            item-text="name"
            item-value="id"
            return-masked-value
            single-line
            label="優先度"
          ></v-select>
          <v-btn color="success" @click="commit">commit</v-btn>
          <v-btn color="error" @click="closeForm">cancel</v-btn>
        </v-form>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script>
import firebase from "firebase";

export default {
  name: "Form",
  props: {
    // idはない場合は新規
    id: {
      default: null
    },
    inputData: {},
    category: {},
    state: {}
  },
  methods: {
    commit: function() {
      // 新規でも編集でも登録するならば、ステータスは戻す
      const data = { ...this.inputData, isDone: false };
      let key = this.id;
      // keyがない場合は新規
      if (key == null) {
        // 新規keyを取得
        key = firebase
          .database()
          .ref("item")
          .child("posts")
          .push().key;
      }
      // 登録内容objを作る
      const updates = {};
      updates["/item/" + key] = data;
      // 更新
      firebase
        .database()
        .ref()
        .update(updates);
      // フォーム閉じる
      this.closeForm();
    },
    closeForm: function() {
      // 親にイベント送信
      this.$emit("closeForm");
    }
  }
};
</script>
```

## List
リストです.
<img width="1126" alt="スクリーンショット 2019-01-20 23.11.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/283053/b9de2122-178e-ec1b-c64b-3244f1d6a2a6.png">

```vuejs:Form.vue
<template>
  <v-container>
    <v-layout row wrap>
      <v-flex xs12>
        <v-card>
          <v-card-title>
            <v-flex xs3>
              <h2>リスト</h2>
            </v-flex>
            <v-flex xs6>
              <v-checkbox v-model="displayNotIsDone" label="未実施のみ表示"></v-checkbox>
            </v-flex>
            <v-btn color="pink" dark absolute bottom right fab @click="openForm(null)">
              <v-icon>add</v-icon>
            </v-btn>
          </v-card-title>
        </v-card>
      </v-flex>
      <v-flex xs12 md6 v-for="(data,key) in displayItem" :key="key">
        <v-card>
          <v-card-title>
            <v-flex xs1>
              <v-checkbox v-model="data.isDone" color="red" @change="changeDone(key)"></v-checkbox>
            </v-flex>
            <v-flex xs11 class="indigo--text">
              <v-badge right color="red">
                <span v-if="data.state==0" slot="badge">!</span>
                <h2>{{data.title}}</h2>
              </v-badge>
              <v-badge right color="red"></v-badge>
            </v-flex>
            <v-flex xs12>
              <span>{{data.text}}</span>
            </v-flex>
          </v-card-title>
          <v-card-text>
            <v-layout row wrap>
              <v-flex xs6>{{ getValue(category,data.category)}} / {{ getValue(state,data.state)}}</v-flex>
              <v-flex xs6>
                <v-btn color="error right" fab dark small @click="del(key)">
                  <v-icon>delete</v-icon>
                </v-btn>
                <v-btn color="indigo right" fab dark small @click="openForm(key)">
                  <v-icon>edit</v-icon>
                </v-btn>
              </v-flex>
            </v-layout>
          </v-card-text>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import firebase from "firebase";
export default {
  name: "List",
  props: {
    item: {
      default: ""
    },
    category: {},
    state: {}
  },
  data() {
    return {
      displayNotIsDone: false
    };
  },
  computed: {
    // 未実施のみなどで表示アイテムの条件が変わる
    displayItem: function() {
      const displayItem = {};
      Object.keys(this.item).forEach(key => {
        if (!this.displayNotIsDone || this.item[key].isDone) {
          displayItem[[key]] = this.item[key];
        }
      });
      return displayItem;
    }
  },
  methods: {
    openForm: function(key) {
      // 親にイベント送信、編集時はペイロードでkeyも渡す
      this.$emit("openForm", { id: key });
    },
    del: function(key) {
      const updates = {};
      // nullでアップデートを行うと削除と同じになる
      updates["/item/" + key] = null;
      firebase
        .database()
        .ref()
        .update(updates);
    },
    // チェック変更でも更新する
    changeDone: function(key) {
      const data = this.item[key];
      const updates = {};
      updates["/item/" + key] = data;
      firebase
        .database()
        .ref()
        .update(updates);
    },
    // カテゴリと優先順位マスタからid指定でnameを取得
    getValue: function(data, id) {
      const index = data.findIndex(val => {
        return val.id == id;
      });
      if (index !== -1) {
        return data[index].name;
      } else {
        return null;
      }
    }
  }
};
</script>
```

##Home

```vuejs:Home.vue
<template>
  <v-container grid-list-md>
    <v-layout row wrap>
      <v-flex xs12>
        <v-card>
          <v-card-title>
            <h1>ToDoList</h1>
            <v-btn v-if="userName == null" color="lime" small fab dark @click="login">
              <v-icon>account_circle</v-icon>
            </v-btn>
            <v-btn v-else color="lime" small fab @click="logout">{{userName[0]}}</v-btn>
          </v-card-title>
        </v-card>
      </v-flex>
      <Form
        v-if="addFormFlag"
        :id="id"
        :inputData="inputData"
        :category="category"
        :state="state"
        @closeForm="closeForm"
      ></Form>
      <List v-else :item="item" :category="category" :state="state" @openForm="openForm"></List>
    </v-layout>
  </v-container>
</template>
<script>
import firebase from "firebase";
import Form from "@/components/Form.vue";
import List from "@/components/List.vue";
export default {
  name: "Home",
  components: {
    Form,
    List
  },
  created: function() {
    // firebaseからデータを取得する
    this.database = firebase.database();
    // アイテムはリアルタイムで更新
    this.spotsRef = this.database.ref("item");
    const _this = this;
    this.spotsRef.on("value", function(snapshot) {
      _this.item = snapshot.val();
    });
    // ここからはマスタデータなので１回取得でいい
    this.spotsRef = this.database
      .ref("category")
      .once("value")
      .then(snapshot => {
        this.category = snapshot.val();
      });
    this.spotsRef = this.database
      .ref("state")
      .once("value")
      .then(snapshot => {
        this.state = snapshot.val();
      });
  },
  data() {
    return {
      id: null,
      database: null,
      spotsRef: null,
      item: {},
      category: [],
      state: [],
      inputData: { title: null, text: null, category: null, state: null },
      addFormFlag: false,
      userName: null
    };
  },
  methods: {
    // リスト表示(子供から呼び出される)
    closeForm: function() {
      this.addFormFlag = false;
    },
    // フォーム表示(子供から呼び出される)
    openForm: function(dict) {
      // dictはペイロードでidが入っている
      const id = dict.id;
      // ない場合は新規
      if (id != null) {
        this.inputData = this.item[id];
      } else {
        this.inputData = this.inputInit();
      }
      this.id = id;
      this.addFormFlag = true;
    },
    // 入力データを初期化
    inputInit: function() {
      return { title: null, text: null, category: null, state: null };
    },
    // ログイン
    login: function() {
      // グーグルの承認
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase.auth().signInWithPopup(provider);
      // authの情報を取得
      this.onAuth();
    },
    // ログアウト
    logout: function() {
      firebase.auth().signOut();
      this.onAuth();
    },
    onAuth: function() {
      // 今回はgoogleの表示名のみをもらう.本来なら全てを保持したい?
      firebase.auth().onAuthStateChanged(user => {
        this.userName = user ? user.displayName : null;
      });
    }
  }
};
</script>
```

##ログイン
ただ実装しました.特に機能はないです..
やるとしたら,itemをuserIdでネストして公開todo,プライベートtodoに分ける程度かと.
![loginout.gif](https://qiita-image-store.s3.amazonaws.com/0/283053/36c68502-6603-340e-0c63-c544bfdb5e9f.gif)

##まとめ
vue-firebaseを連携させました.いろいろとお勉強になりました.
ただし,vuexまで使っていないので,Homeからコンポーネントへマスタ情報まで渡したりとしてしまっています.(業務では使ってますが,今回はこの規模ならと思いましたが,いびつなきがしてしまいます.)また,firebaseにアクセスするものどのコンポーネントから行ってしまってます..改善個所はたくさんあります.

今回初めてfirebaseを触りバックエンドの面倒をやらずに楽なことを知りました.
今後はNoSQLの勉強をしてもっと実践的なアプリ作りたいと思います!!!