Nuxt.jsプロジェクトでFirestoreのデータを変更/削除する

はじめに

数回に分けてNuxt.jsプロジェクトでデータの永続化を行う方法を解説しています。これらの記事は上から順番に読んでいくことを想定しています。

これらの記事で作成したNuxt.jsプロジェクトは以下のGitHubリポジトリーで公開しています。

GitHub - whitia/nuxt-firebase-sample

前回Firestoreを利用してデータを永続化しました。今回はそのデータを変更/削除する方法を解説します。

動的ページの作成

データの変更はドキュメント単位で行うので、変更画面をドキュメントごとに動的に作成する必要があります。ドキュメントを一意にできるidフィールドを使うことにします。

pages/users/の中にedit/というディレクトリと、その中に_id.vueというファイルを作成してください。pages/ディレクトリの中は以下のようになります。

pages
-|users
--|edit       # 追加
---|_id.vue   # 追加
--|create.vue
--|index.vue
-|index.vue

アンダーバーから始まるファイルが動的ページになります。Nuxt.jsはこれだけで動的ルーティングが設定されます。.nuxt/router.jsを見てみてください。

  routes: [{
    path: "/users",
    component: _6bde79c2,
    name: "users"
  }, {
    path: "/users/create",
    component: _c0882902,
    name: "users-create"
  }, {
    path: "/users/edit/:id?",
    component: _f7f74110,
    name: "users-edit-id"
  }, {
    path: "/",
    component: _d509f134,
    name: "index"
  }],

各ルーティングには名前がついています。今回は動的ページのusers-edit-idを使いますので覚えておいてください。

pages/users/index.vueを開き、以下のように追記します。

<template>
  <div class="container mt-5">

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-3">
        <h2>Users</h2>
      </div>
    </div>

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2 font-weight-bold">Name</div>
      <div class="col-12 col-sm-1 font-weight-bold">Age</div>
      <!-- 以下を追記 -->
      <div class="col-12 col-sm-1 font-weight-bold">Edit</div>
    </div>

    <div v-for="(user, key) in $store.getters.getUsers" :key="key"
         class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2">
        {{ user.name.first }} {{ user.name.last }}
      </div>
      <div class="col-12 col-sm-1">
        {{ user.age }}
      </div>
      <!-- 以下を追記 -->
      <div class="col-12 col-sm-1">
        <nuxt-link :to="{ name: 'users-edit-id', params: { id: user.id } }">
          Edit
        </nuxt-link>
      </div>
    </div>

  </div>
</template>

<!-- スクリプトは変更なし -->

ユーザー一覧画面に変更画面へのリンクを追加しました。ルーティングの名称にusers-edit-idを指定し、パラメーターにuser.idを渡しています。変更画面のURLは以下のようになっていると思います。

http://localhost:3000/users/edit/39e85916-899b-4d92-9200-8ceaa307249a

URLに一意なidを入れることで動的ページを実現しています。このidは変更画面に遷移してからも重要な意味を持ちます。

それでは変更画面を作っていきます。pages/users/edit/_id.vueに以下を追記します。

※セキュリティ上、スクリプトタグを書くとコードが消えてしまうので、スクリプトタグをわざと書いていません。コピペする際は補完をお願いします。

export default {
  created() {
    const id = this.$route.params.id

    this.$store.dispatch('fetchUser', { id })
  }
}

this.$routeにはVue Routerによって渡されたいろいろな値が格納されています。ユーザー一覧画面のリンクで渡したuser.idthis.$route.params.idに入っています。この値を使ってユーザー情報を取得しに行きます。

store/index.jsに以下を追記します。

...

export const state = () => ({
  ...

  user: {
    id: '',
    name: {
      first: '',
      last: ''
    },
    age: ''
  }
})

export const actions = {
  ...

  fetchUser({ commit }, payload) {
    return new Promise((resolve, reject) => {
      usersRef.where('id', '==', payload.id).get()
      .then(res => {
        res.forEach((doc) => {
          commit('addUser', doc.data())
          resolve(true)
        })
      })
      .catch(error => {
        console.error('An error occurred in fetchUsers(): ', error)
        reject(error)
      })
    })
  }
}

export const mutations = {
  ...

  addUser(state, user) {
    state.user = user
  }
}

export const getters = {
  ...

  getUser(state) {
    return state.user
  }
}

fetchUserメソッドは受け取ったidを条件にしてFirestoreからユーザー情報を取得します。取得したデータはVuexストアのデータ領域に保存します。また、getUserメソッドを作成して外部から参照可能にしておきます。

pages/users/edit/_id.vueに戻り、テンプレートを追記します。

<template>
  <b-form @submit.prevent="editUser">
    <div class="container mt-5">

      <div class="row justify-content-center mb-3">
        <div class="col-12 col-sm-3">
          <h2>Edit user</h2>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="First name" label-for="first">
            <b-form-input
              id="first"
              :value="$store.getters.getUser.name.first"
              required
              placeholder="Jane"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="Last name" label-for="last">
            <b-form-input
              id="last"
              :value="$store.getters.getUser.name.last"
              required
              placeholder="Doe"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="Age" label-for="age">
            <b-form-input
              id="age"
              :value="$store.getters.getUser.age"
              required
              placeholder="20"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center mt-3">
        <div class="col-12 col-sm-3">
          <b-button block type="submit" variant="primary">Edit</b-button>
        </div>
      </div>

    </div>
  </b-form>
</template>

新規作成画面ではv-modelを使っていましたが、変更画面では:valueを使っていることに注意してください。v-modelを使ってVuexストアのデータをバインドしてしまうと、値を変更したときに「Vuexストアのデータはmutationsを使って変更せよ」というエラーが出てしまいます。

長くなりましたが、ようやく動的ページができました。次からデータの変更処理を実装していきます。

データの変更

pages/users/edit/_id.vueに以下を追記します。

<!-- テンプレートは変更なし -->

export default {
  ...

  methods: {
    editUser(e) {
      const user = {
        id: this.$store.getters.getUser.id,
        name: {
          first: e.target.first.value,
          last: e.target.last.value
        },
        age: e.target.age.value
      }

      this.$store.dispatch('editUser', { user })
      .then(() => {
        setTimeout(() => {
          this.$router.push('/users')
        }, 1000)
      })
    }
  }
}

入力フォームに入力された値を使ってVuexのアクションを呼び出します。

editUserメソッドが完了してから1秒後にユーザー一覧画面に遷移しています。これはなぜかというと、データ変更後にすぐ遷移してしまうとFirestoreにデータ変更が反映されないためです。

store/index.jsに以下を追記します。

...

export const actions = {
  ...

  editUser({ commit }, payload) {
    return new Promise((resolve, reject) => {
      usersRef.where('id', '==', payload.user.id).get()
      .then(snapshot => {
        snapshot.forEach(doc => {
          const user = {
            id: uuidv4(),
            name: payload.user.name,
            age: payload.user.age,
            updated_at: firebase.firestore.FieldValue.serverTimestamp()
          }

          usersRef.doc(doc.id).update(user)
          .then(ref => {
            resolve(true)
          })
          .catch(error => {
            console.error('An error occurred in editUser(): ', error)
            resolve(error)
          })
        })
      })
    })
  }
}

...

これでデータ変更処理ができました。

データの削除

最後にデータの削除処理を実装します。pages/users/index.vueに以下を追記します。

<template>
  <div class="container mt-5">

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-3">
        <h2>Users</h2>
      </div>
    </div>

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2 font-weight-bold">Name</div>
      <div class="col-12 col-sm-1 font-weight-bold">Age</div>
      <div class="col-12 col-sm-1 font-weight-bold">Edit</div>
      <!-- 以下を追記 -->
      <div class="col-12 col-sm-1 font-weight-bold">Delete</div>
    </div>

    <div v-for="(user, key) in $store.getters.getUsers" :key="key"
         class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2">
        {{ user.name.first }} {{ user.name.last }}
      </div>
      <div class="col-12 col-sm-1">
        {{ user.age }}
      </div>
      <div class="col-12 col-sm-1">
        <nuxt-link :to="{ name: 'users-edit-id', params: { id: user.id } }">
          Edit
        </nuxt-link>
      </div>
      <!-- 以下を追記 -->
      <div class="col-12 col-sm-1">
        <a href="#" @click.prevent="deleteUser(user.id)">Delete</a>
      </div>
    </div>

  </div>
</template>

export default {
  ...

  methods: {
    deleteUser(id) {
      if (!confirm('Are you sure?')) return

      this.$store.dispatch('deleteUser', { id })
      .then(() => {
        setTimeout(() => {
          this.$store.dispatch('fetchUsers')
        }, 1000)
      })
    }
  }
}

deleteUserメソッドの引数にuser.idを渡しています。そのidをそのままVuexストアのアクションに投げます。

store/index.jsに以下を追記します。

export const actions = {
  ...

  deleteUser({ commit }, payload) {
    return new Promise((resolve, reject) => {
      usersRef.where('id', '==', payload.id).get()
      .then(snapshot => {
        snapshot.forEach(doc => {
          usersRef.doc(doc.id).delete()
          .then(ref => {
            resolve(true)
          })
          .catch(error => {
            console.error('An error occurred in deleteUser(): ', error)
            reject(error)
          })
        })
      })
    })
  }
}

...

これで削除処理ができました。

まとめ

Firestoreのデータを変更/削除する方法を解説しました。

ここまで実装できればFirestoreの操作はマスターしたと言ってもいいんじゃないでしょうか。Firestoreとの間には必ずVuexストアが入るという流れを覚えておいてください。

また、Vuexストアのデータ領域はコンポーネントをまたいで参照できます。ページをリロードしてDOMを再生成しない限り残り続けます。ログイン情報など、ページをリロードしても残しておきたいデータはブラウザのローカルストレージに保存する方法があります。Nuxt.jsでローカルストレージを利用する方法についてはまた別記事にてまとめたいと思います。