Nuxt.js - Firebaseを使ったToDoアプリの作成

# はじめに
Nuxt.jsの学習でTodoアプリを作成したので、理解した内容で少し機能の追加をしてみました。
0から作成する手順をまとめたいと思います。初学者のため、コード等お見苦しい箇所が多くあるかもしれません。お許しください:mask:

# 作成物
![todo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/054ed589-bbbe-90c6-51d5-ae66b3648092.gif)

[リポジトリ](https://github.com/so-heee/nuxt-todo)

# 機能
以下が主な機能でnewが追加した部分になります。

- タスクが登録できる
- タスクに備考が登録できる　**<font color="DeepPink">new</font>**
- タスクに日付が登録できる **<font color="DeepPink">new</font>**
- タスクを完了にできる
- タスクを削除できる
- UIフレームワークでVuetifyを使用する **<font color="DeepPink">new</font>**
- 登録の際にダイアログを表示する **<font color="DeepPink">new</font>**
- TodoとDoneは別々の表示領域を用意する **<font color="DeepPink">new</font>**

# 前提
- Googleアカウントを所持していること
- Nuxtの開発環境が用意してあること

**※開発環境の用意については[こちら](https://qiita.com/so_heee_/items/56d795169a7847e462aa)にまとめてみました！**

# Firebaseの設定
データの格納場所としてFirebaseの `CloudFirestore` を使用します。
[Cloud Firestore
](https://firebase.google.com/docs/firestore?hl=ja)

## サイトへアクセス
以下のURLへアクセスします。
https://firebase.google.com/?hl=ja

使ってみるを選択するとGoogleアカウントの認証画面が開くので認証情報を入力します。

![スクリーンショット 2019-10-08 10.59.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/590895b7-2534-9aee-dfa9-a02ac2d08990.png)

## プロプロジェクトの作成
Firebaseブロジェクトの「プロジェクトの追加」を選択します。

プロジェクト名を入力して「続行」を選択します。

![スクリーンショット 2019-10-08 11.02.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/aaca0dfc-5146-16f7-38d6-9dae13c50637.png)

Googleアナリティクスを有効にして「続行」を選択します。

![スクリーンショット 2019-10-08 11.03.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/1283e10a-8cac-3e3d-00c8-15a4323d6759.png)

アナリティクスを日本にして利用規約等を読み「プロジェクトを作成」を選択します。

![スクリーンショット 2019-10-08 11.04.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/3fbca7b4-818d-1fee-6b3f-64b933024fb7.png)

## Database(CloudFirestore)の作成
「開発」 -> 「Database」からCloudFirestoreを開きます。
「データベースを作成」を選択します。

セキュリティ保護ルールの作成にて「テストモードで開始」を選択します。ロックモードでは認証ありのモードです。テストモードは認証がありませんので検証や学習の際に利用します。

**<font color="DeepPink">テストモードの場合、プロジェクトIDのみで利用できてしまうので、外部にプロジェクトIDを出さないよう注意しましょう。検証が終わったらdatabaseは削除します</font>**

![スクリーンショット 2019-10-08 11.43.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/1b653887-711a-26ab-c0ff-74f90929acd9.png)

ロケーション（物理サーバーの配置場所）の設定では「asia-northeast1 （東京）」を選択して完了を選択します。

![スクリーンショット 2019-10-08 11.44.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/900842f9-952c-16a0-5a18-0ce18e4e2781.png)

これでFirebase側の準備はOKです！


#　プロジェクトの作成
```bash:terminal
npx create-nuxt-app nuxt-todo
```

```
作成時以下の内容だけ指定して、他は初期値にします。
// 利用するパッケージマネージャー
? Choose the package manager Npm
- Npm

// 利用するUIフレームワーク
? Choose UI framework None
- Vuetify

// 利用するコード検証ツール（コードのチェックツール）
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
- ESLint
- Prettier

// 利用するレンダリングモード
? Choose rendering mode Universal (SSR)
- SPA
```


# Firebase関連のパッケージをインストール

firebaseを利用するための基本パッケージ（firebase）をインストールします。

```bash:terminal
npm install --save firebase@6.2.4 
```

CloudfireStoreを簡単に利用するためのパッケージ（vuexfire）をインストールします。

```bash:terminal
npm install --save vuexfire@3.0.1
```

# 環境変数の設定
.envから環境変数を読み込むためのパッケージ（dotenv）をインストールします。

```bash:terminal
npm install --save @nuxtjs/dotenv@1.3.0
```
.envを作成

```:.env
FIREBASE_PROJECT_ID = 'project-id'
```
プロジェクトIDはコンソール画面の歯車マークから「プロジェクトの設定」を選択し、開いた「Setting」の画面に表示されています。

.env を使用するための設定を追加

```nuxt.config.js
/*
 ** Nuxt.js modules
 */
modules: ['@nuxtjs/dotenv'],
```

Gitで管理する予定の場合は `.gitignore`に.envが含まれていることを確認します。(Gitの反映対象外とする)

```:.gitignore
# dotenv environment variables file
.env
```

# Firebaseとの連携

pluginsディレクトリの下に連携用のファイルを作成します。ファイル名は自由です。

```javascript:firebase.js
import firebase from 'firebase/app'
import 'firebase/firestore'

// .envからプロジェクトIDを取得して定数に設定
const config = {
  projectId: process.env.FIREBASE_PROJECT_ID
}

// firebaseの初期化処理
if (!firebase.apps.length) {
  firebase.initializeApp(config)
}

export default firebase
```

# ストアの作成

storeディレクトリの下にindex.jsファイルを作成します。

```javascript:index.js
import { vuexfireMutations } from 'vuexfire'

export const mutations = {
  ...vuexfireMutations
}
```

storeディレクトリの下にtask.jsファイルを作成します。

```javascript:task.js
import { firestoreAction } from 'vuexfire'
import firebase from '~/plugins/firebase'

const db = firebase.firestore()
const taskRef = db.collection('task')

export const state = () => ({
  tasks: []
})

export const actions = {
  // 初期化
  init: firestoreAction(({ bindFirestoreRef }) => {
    bindFirestoreRef('tasks', taskRef)
  }),
  // 追加
  add: firestoreAction((context, { title, detail, date }) => {
    if (title.trim()) {
      taskRef.add({
        title,
        detail,
        date,
        status: false
      })
    }
  }),
  // 削除
  remove: firestoreAction((context, id) => {
    taskRef.doc(id).delete()
  }),
  // ステータス更新
  toggle: firestoreAction((context, todo) => {
    taskRef.doc(todo.id).update({
      status: !todo.status
    })
  })
}

// 日付の昇順でソート
export const getters = {
  orderdTodos: (state) => {
    return _.orderBy(state.tasks, 'date', 'asc')
  }
}
```

一覧データを日付の昇順で表示するためにlodashというライブラリを使用しました。
使用するためにはnuxt.config.jsに以下の記載をします。

```javascript:nuxt.config.js
import webpack from 'webpack'

  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {},
    plugins: [
      new webpack.ProvidePlugin({
        _: 'lodash'
      })
    ]
  }
```

# コンポーネントの作成

コンポーネントは全部で3つ作成しました。
もっと細かく分けたかったのですが、ざっくりと分けることにしました。

## タスクコンポーネント

まずはダイアログの部分です。
Vuetifyにはダイアログのコンポーネントがあるのでそれを利用しました。
datepickerのコンポーネントも色々表示のさせ方が豊富で便利ですね:relaxed:
登録を押した時にaddメソッドからtask.jsのaddが実行されてDBに登録されます。

![スクリーンショット 2019-10-09 20.17.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/fcbeee7c-3ee6-843b-7900-1411678ecc3e.png)


```vue:/components/TaskDetail.vue
<template>
  <v-dialog v-model="dialog" persistent max-width="600px">
    <template v-slot:activator="{ on }">
      <v-btn color="#5963F8" dark class="font-weight-bold" v-on="on"
        ><v-icon small class="mr-2">mdi-plus-circle-outline </v-icon
        >新規タスクを追加</v-btn
      >
    </template>
    <v-card>
      <v-card-title>
        <span class="headline">新規タスクを追加</span>
      </v-card-title>
      <v-card-text>
        <v-container>
          <v-row>
            <v-col cols="12">
              <v-text-field v-model="title" label="Title"></v-text-field>
            </v-col>
            <v-col cols="12">
              <v-textarea v-model="detail" label="Detail"></v-textarea>
            </v-col>
            <v-col cols="12">
              <v-dialog
                ref="dialog"
                v-model="modal"
                :return-value.sync="date"
                persistent
                width="290px"
              >
                <template v-slot:activator="{ on }">
                  <v-text-field
                    v-model="date"
                    label="日時"
                    readonly
                    v-on="on"
                  ></v-text-field>
                </template>
                <v-date-picker v-model="date" scrollable>
                  <div class="flex-grow-1"></div>
                  <v-btn text color="primary" @click="modal = false"
                    >Cancel</v-btn
                  >
                  <v-btn text color="primary" @click="$refs.dialog.save(date)"
                    >OK</v-btn
                  >
                </v-date-picker>
              </v-dialog>
            </v-col>
          </v-row>
        </v-container>
      </v-card-text>
      <v-card-actions>
        <div class="flex-grow-1"></div>
        <v-btn color="primary" text @click="dialog = false">キャンセル</v-btn>
        <v-btn color="primary" text @click="add">登録</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script>
export default {
  data() {
    return {
      title: '',
      detail: '',
      dialog: false,
      date: new Date().toISOString().substr(0, 10),
      menu: false,
      modal: false
    }
  },
  methods: {
    add() {
      this.$store.dispatch('task/add', {
        title: this.title,
        detail: this.detail,
        date: this.date
      })
      this.dialog = false
    }
  }
}
</script>
```

## リストコンポーネント

次に一覧を表示するタスクの一覧の部分です。
TodoとDoneで利用するのでtitileとtasklistは親から値をもらうようにしています。
チェックボックスを押した時にtoggle、削除アイコンを押した時にremoveを実行します。

![スクリーンショット 2019-10-09 20.21.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/a0b2022a-670a-4c8a-8889-ef1e3fb044ad.png)


```vue:/components/TaskList.vue
<template>
  <v-row>
    <v-col cols="12" md="12">
      <v-list color="#f4f5fc">
        <v-subheader class="font-weight-bold">{{ title }}</v-subheader>
        <v-col v-for="task in tasklist" :key="task.id" cols="12" class="pt-0">
          <v-card>
            <v-card-title class="headline pb-0">
              <v-checkbox
                :checked="task.status"
                color="primary"
                class="ma-0"
                :label="task.title"
                @change="toggle(task)"
              ></v-checkbox>
            </v-card-title>
            <v-card-text class="pb-0">{{ task.detail }}</v-card-text>
            <v-card-actions class="pt-0">
              <v-col cols="10" md="10" class="pl-0">
                <v-btn text>
                  <v-chip color="grey lighten-3" label>
                    <v-avatar left>
                      <v-icon small color="primary">mdi-calendar</v-icon>
                    </v-avatar>
                    {{ task.date }}
                  </v-chip></v-btn
                >
              </v-col>
              <v-col cols="2" md="2">
                <v-btn icon color="grey" text dark @click="remove(task.id)">
                  <v-icon>mdi-close-circle-outline</v-icon>
                </v-btn>
              </v-col>
            </v-card-actions>
          </v-card>
        </v-col>
      </v-list>
    </v-col>
  </v-row>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    tasklist: {
      type: Array,
      default: null
    }
  },
  methods: {
    remove(id) {
      this.$store.dispatch('task/remove', id)
    },
    toggle(task) {
      this.$store.dispatch('task/toggle', task)
    }
  }
}
</script>

<style>
.theme--light.v-label {
  color: #000;
}
.v-input--selection-controls:not(.v-input--hide-details) .v-input__slot {
  margin-bottom: 0px;
}
.v-application--is-ltr .v-list-item__avatar:first-child {
  margin-right: 0;
}
</style>
```

## ボードコンポーネント

最後にpagesディレクトリにboard.vueを作成します。
上記で作成した、タスクコンポーネント、リストコンポーネントをimportします。
また新規登録用の処理とcreatedのタイミングでfirebaseの初期化処理を実行します。

![スクリーンショット 2019-10-11 12.31.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151022/8c964c31-8b9c-e07a-b7d9-ee47c6d6c5e2.png)


```vue:/pages/board.vue
<template>
  <v-container class="todo">
    <v-form ref="form">
      <v-row>
        <v-col cols="12" md="12">
          <task-detail></task-detail>
        </v-col>
      </v-row>
    </v-form>
    <task-list title="Todo" :tasklist="todolist"></task-list>
    <task-list title="Done" :tasklist="donelist"></task-list>
  </v-container>
</template>

<script>
import TaskList from '../components/TaskList.vue'
import TaskDetail from '../components/TaskDetail.vue'

export default {
  components: {
    TaskList,
    TaskDetail
  },
  computed: {
    todolist() {
      return this.$store.getters['task/orderdTodos'].filter(function(el) {
        return el.status === false
      }, this)
    },
    donelist() {
      return this.$store.getters['task/orderdTodos'].filter(function(el) {
        return el.status === true
      }, this)
    }
  },
  created() {
    this.$store.dispatch('task/init')
  }
}
</script>

<style scoped>
.status.done {
  text-decoration: line-through;
}
</style>
```

# UIの調整

外観の部分を調整するためレイアウトを少しだけ修正しました。

```vue:layout/default.vue
<template>
  <v-app dark>
    <v-app-bar color="#5963F8" fixed app dark>
      <v-toolbar-title>{{ title }}</v-toolbar-title>
    </v-app-bar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      title: 'TodoList'
    }
  }
}
</script>

<style>
.theme--light.v-application {
  background: #fff;
}
.v-toolbar__title {
  font-family: 'Damion', cursive;
  font-size: 2.5rem;
  width: 100%;
  text-align: center;
}
</style>
```

# おわりに
Vueの学習課題としてよく見かけるTodoアプリの作成を、自分が本当に理解できたのか確認するために今回の作業を行いました。編集できるようにしたり、ドラッグ&ドロップできるようにしたりなど意外にやれることは多くて面白い課題だなと感じました。どこかでTrelloやTodoListなどのアプリを参考にして機能を追加していけたらいいなと思っています。

ここまでお読みいただきありがとうございました！！:bow_tone1:


# 参考
[Nuxt.js](https://ja.nuxtjs.org/guide/)
[Vuetify](https://vuetifyjs.com/ja/getting-started/quick-start)