3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.jsでfirestoreのクエリ範囲をページネーションで変化させる

Last updated at Posted at 2020-05-05

firestoreには日頃から感謝していますが、まだまだクエリには難があるようですね…
ということで、今回はfirestoreのクエリ範囲をページネーションによって操作する方法を備忘録としてまとめていきたいと思います。

使用環境

Nuxt (version: 2.11.0)
Vuetify (version: 2.1.15)
firestore

やりたかったこと

  • 相当数(1000以上)のデータが格納されたDBから、必要に応じた数を指定した範囲でクエリしたい。
  • もちろんソートをかけた時に、DB全てが対象にしたい。
  • ソートをかけた際に、同じ値のものが複数ある時にも対応したい。
  • 部分一致検索も出来るようにしたい。
  • 検索も特定のフィールドだけではなく、必要に応じてフィールドを選べるようにしたい。
  • けど、Algoria等の外部APIは使用したくない。
  • リロードが発生しても、状態を維持できるようにしたい。
  • ページ番号も振りたい。

先に結論だけ言うと

  • ページ遷移によるクエリ範囲の変更は可能。
  • ソートも出来る。ただし、同じ値が複数ある場合はフィールドが2つ必要。
  • 完全な部分一致は出来ないが、前方一致は出来る。
  • Vuexで保存すればリロードを挟んでも維持できる。
  • ページ番号は振れるのですが、ページの総数までは抜き出すことができませんでした。(強がりを言うと、実装することは出来るのですが、データ数が多くなると現実的ではないと考えました…)

となります。

以下見にくいかもしれませんが、成果物のgifを貼っておきます。

タイトルなし.gif

今回省略する部分

  • firebaseのインストール方法
  • nuxtアプリの生成方法

どちらも拙記事に掲載していますので、気になる方はそちらを確認してください。
nuxtアプリの生成方法
firebase関係のインストール方法

前置きが長くなりましたが、本題に入ります。

0.概観

ディレクトリ構造

今回作っていくアプリのディレクトリ構造です。
関係のない部分は端折っています。

<プロジェクト名>
├── components
│   ├── GetData.vue
│   └── Table.vue
├── layouts
│   └── default.vue
├── pages
│   ├── Book.vue
│   └── index.vue
~
~
└── store
    └── index.js

今回 layoutsはシンプルに pagesのみを写すだけのものとします。

DB構造

今回の firestoreのDB構造です。

books
├── <本1>
│   ├── title
│   ├── createdAt
│   └── type
~
~
└── <本n>
    ├── title
    ├── createdAt
    └── type

users
├── <ユーザー1>
│   ├── age
│   ├── name
│   ├── createdAt
│   └── post_count
~
~
└── <ユーザーn>
    ├── age
    ├── name
    ├── createdAt
    └── post_count

このように今回は簡単のため、booksusersの2つのテーブルデータをそれぞれ違うpagesを用いて表示させます。

ファイルの相関

スクリーンショット 2020-05-03 16.03.48.png 雑把に言うとこのような相関関係になります。

1. ページ遷移の方法

今回はクエリした内容をVuetifyv-data-tableに渡していきます。
まずは全体のコードです。

pages/index.vue
<template>
  <div>
    <GetData :seed="seed" :headers="headers" :addBtn="addBtn" />
    <v-btn @click="toBook()" />
  </div>
</template>

<script>
import GetData from '~/components/GetData.vue'
export default {
  components: {
    GetData
  },
  data () {
    return {
      seed: 'users',
      headers: [
        { text: 'アカウント名', sortable: true, value: 'name', align: 'center' },
        { text: '年齢', sortable: true, value: 'age', align: 'center' },
        { text: '投稿数', sortable: true, value: 'post_count', align: 'center' },
        { text: 'ID', sortable: false, value: 'uid', align: 'center' },
        { text: 'createdAt', sortable: false, value: 'createdAtshow', align: 'center' }
      ]
    }
  },
  methods: {
    toBook () {
      this.$router.push('/Book')
    }
  }
}
</script>
pages/Book.vue
<template>
  <div>
    <GetData :seed="seed" :headers="headers" :addBtn="addBtn" />
    <v-btn @click="toIndex()" />
  </div>
</template>

<script>
import GetData from '~/components/GetData.vue'
export default {
  components: {
    GetData
  },
  data () {
    return {
      seed: 'books',
      headers: [
        { text: '題名', sortable: true, value: 'title', align: 'center' },
        { text: '区分', sortable: false, value: 'type', align: 'center' },
        { text: 'ID', sortable: false, value: 'uid', align: 'center' },
        { text: 'createAt', sortable: false, value: 'createdAtshow', align: 'center' }
      ]
    }
  },
  methods: {
    toIndex () {
      this.$router.push('/')
    }
  }
}
</script>
components/GetData.vue
<template>
  <div>
    <Table
      :OriginList="OriginList"
      :seed="seed"
      :page="page"
      :headers="headers"
      :first="first"
      :last="last"
      @handlepage="updatepage"
      @handlesearch="updatesearch"
      :search="search"
      :itemsPerPage="itemsPerPage"
    />
  </div>
</template>

<script>
import firebase from '~/plugins/firebase.js'
import Table from '~/components/Table.vue'
export default {
  name: 'GetData',
  components: {
    Table
  },
  props: {
    seed: {
      type: String,
      default: ''
    },
    headers: {
      type: Array,
      default: []
    }
  },
  data () {
    return {
      itemsPerPage: 6,
      OriginList: [],
      fuid: '',
      luid: '',
      first: true,
      last: false,
      firstitem: '',
      lastitem: '',
      filterseed: '',
      page: 1,
      baseitem: '',
      basetime: '',
      search: '',
      docRef: '',
      lastRef: '',
      orderItem: '>=',
      order: 'asc',
      initial: {
        page: 1,
        baseitem: '',
        basetime: ''
      }
    }
  },
  created () {
    if (this.seed === 'users') {
      this.filterseed = 'name'
    } else if (this.seed === 'books') {
      this.filterseed = 'title'
    }
    this.makeList(this.filterseed, this.orderItem, this.order)
  },
  methods: {
    showTime (time) {
      if (time) {
        const year = new Date(time.seconds * 1000).getFullYear().toLocaleString('ja-JP').replace(/,/, '')
        let month = new Date(time.seconds * 1000).getMonth().toLocaleString('ja-JP')
        month = parseFloat(month) + 1
        const day = new Date(time.seconds * 1000).getDate().toLocaleString('ja-JP')
        const thisDate = year + '/' + month + '/' + day
        return thisDate
      } else {
      }
    },
    makeList (filterseed, orderItem, order) {
      // 初期化
      this.OriginList = []
      this.first = false
      this.last = false


      const db = firebase.firestore().collection(this.seed)

      const t = this
      const invList = []
      const key = 0

      
      // 接続先のDBを選択 lasRefは一番最後のUidを取ってくるやつ
      if (this.search === '') {
        this.docRef = db.orderBy(filterseed, order).orderBy('createdAt', order).startAfter(this.baseitem, this.basetime)
        this.lastRef = db.orderBy(filterseed, 'desc').orderBy('createdAt', 'desc')
      } 
      // DB作成
      this.docRef.limit(this.itemsPerPage).get().then(function (snapshot) {
        snapshot.forEach(function (doc) {
          const dd = doc.data()
          dd.uid = doc.id
          dd.createdAtshow = t.showTime(dd.createdAt)
          t.OriginList.push(dd)
        })

        // 前のページに遷移するときは順番を逆にする
        if (orderItem === '<=') { t.OriginList = t.OriginList.reverse() }

        if (t.OriginList.length !== 0) {
          // 表示されているものの最初と最後取ってくる
          t.lastitem = t.OriginList[t.OriginList.length - 1][filterseed]
          t.lasttime = t.OriginList[t.OriginList.length - 1].createdAt
          t.firstitem = t.OriginList[0][filterseed]
          t.firsttime = t.OriginList[0].createdAt

          // 1ページ目ならDB全体の最初と最後も取ってくる
          if (t.page === 1) {
            t.fuid = t.OriginList[0].uid
            t.lastRef.limit(1).get()
              .then(function (snapshot) {
                snapshot.forEach(function (doc) {
                  invList.push({
                    luid: doc.id
                  })
                  t.luid = invList[0].luid
                  // t.$store.commit("sessionUid", {fuid: t.fuid, luid: t.luid})
                  Object.keys(t.OriginList).forEach(function (key) {
                    if (t.OriginList[key].uid === t.luid) { t.last = true }
                  })
                })
              })
              .catch(function (error) {
                console.log(error)
              })
          }
          // 現在表示しているデータに最初と最後あるか確認
          Object.keys(t.OriginList).forEach(function (key) {
            if (t.OriginList[key].uid === t.fuid) { t.first = true }
            if (t.OriginList[key].uid === t.luid) { t.last = true }
          })
        } else {
          t.first = true
          t.last = true
        }
      })
        .catch(function (error) {
          console.log(error)
        })
    },
    updatepage (handlepage, filterseed, orderItem, order) {
      this.page = handlepage
      if (orderItem === '>=') {
        this.baseitem = this.lastitem
        this.basetime = this.lasttime
      } else if (orderItem === '<=') {
        this.baseitem = this.firstitem
        this.basetime = this.firsttime
      }
      this.makeList(filterseed, orderItem, order)
    }
  }
}
</script>
components/Table.vue
<template>
  <div>
    <v-card v-show="!showDelete" flat style="background: #F8F8F8; padding: 15px;">
      <v-card-title class="card-title">
        {{ listName }}管理
      </v-card-title>
      <v-data-table
        v-model="selected"
        :headers="headers"
        :items="OriginList"
        :page.sync="page"
        :itemsPerPage.sync="itemsPerPage"
        @page-count="pageCount = $event"
        show-select
        light
        hide-default-footer
        item-key="uid"
        item
      />

      <div style="padding-top: 30px; text-align: center">
        <v-btn @click="prenav()" :disabled="first" class="pagenation" small fab>
          <v-icon>mdi-chevron-left</v-icon>
        </v-btn>
        <v-btn class="pagenation-center" small fab disabled>
          <span>{{ page }}</span>
        </v-btn>
        <v-btn @click="nextnav()" :disabled="last" class="pagenation" small fab>
          <v-icon>mdi-chevron-right</v-icon>
        </v-btn>
      </div>
    </v-card>
  </div>
</template>

<script>
import firebase from '~/plugins/firebase.js'
export default {
  name: 'Table',
  props: {
    OriginList: {
      type: Array,
      default: []
    },
    seed: {
      type: String,
      default: ''
    },
    headers: {
      type: Array,
      default: []
    },
    page: {
      type: Number,
      default: 1
    },
    itemsPerPage: {
      type: Number,
      default: 10
    },
    first: {
      type: Boolean,
      default: false
    },
    last: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {
      pageCount: 0,
      filterseed: '',
      totalVisible: 1,
      selectItem: [],
      checkRow: [],
      selected: [],
      dialogItem: {
        uid: '',
        name: '',
        title: '',
        createdAtshow: '設定されていません。'
      },
      InitialItem: {
        uid: '',
        name: '',
        title: '',
        createdAtshow: '設定されていません。'
      },
      showDelete: false
    }
  },

  mounted () {
    if (this.seed === 'users') {
      this.filterName = '名前'
      this.filterseed = 'name'
    } else if (this.seed === 'books') {
      this.filterName = '題名'
      this.filterseed = 'title'
    }
  },
  methods: {
    prenav () {
      const currentpage = this.page - 1
      const orderItem = '<='
      const order = 'desc'
      this.$emit('handlepage', currentpage, this.filterseed, orderItem, order)
    },
    nextnav () {
      const currentpage = this.page + 1
      const orderItem = '>='
      const order = 'asc'
      this.$emit('handlepage', currentpage, this.filterseed, orderItem, order)
    }
  }
}
</script>

少し長くなりましたが、この時点でほぼ全ての実装ができています。

では中身を1つ1つ見ていきましょう。

大まかな流れ

i. index.vueファイル(もしくはBooks.vueにある、固有の変数をGetData.vueに渡す。
ii. Getdataファイルのcreated時にmakeListメソッドが発火。
iii. makeListメソッドによって、firebaseからクエリする条件を整え、クエリした内容を変数OriginListに連想配列として格納する。
iv. v-data-tableに必要な変数OriginList等の情報をTableファイルに渡す。
v. ページ遷移する際に、「戻る」か「進む」かをprenavメソッドかnextnavメソッドで定義して、GetDataファイルに返し、もう一度その条件でクエリする。

i. 固有変数の受け渡し

各テーブル固有の変数をここで作ります。
具体的に言うと、今回はusersbooksという2種類のテーブルで違う値を取らなければならない、headersseedを設定します。

components/GetData.vue
<script>
export default {

// ...略

  data () {
      return {
        seed: 'books',
        headers: [
          { text: '題名', sortable: true, value: 'title', align: 'center' },
          { text: '区分', sortable: false, value: 'type', align: 'center' },
          { text: 'ID', sortable: false, value: 'uid', align: 'center' },
          { text: 'createAt', sortable: false, value: 'createdAtshow', align: 'center' }
        ]
      }
    }

// ...略

}
</script>

ii. mkaeListメソッドの発火

ここではあくまでcreatedしているだけですが、makeListメソッドとして分けている部分がミソになります。
こうすることで、非同期処理が非常になりやすくなり、以降もコードを修正しやすくなります。(個人的な意見ですが)
またここで後に使うfilterseedseed毎に定義しておきます。

components/GetData.vue
<script>
export default {

// ...略

  created () {
    if (this.seed === 'users') {
      this.filterseed = 'name'
    } else if (this.seed === 'books') {
      this.filterseed = 'title'
    }
    this.makeList(this.filterseed, this.orderItem, this.order)
  }

// ...略

}
<script>

iii. 生データをOriginListに整形する

ここで、firebaseから生のデータを受け取ります。
現在は素直に受け取るだけなので特に難しいことはないですが、後々に弄りやすい形に意図的にしています。
ただしクエリ文は、重複を許せるように2つのフィールドでorderByでクエリするようにしています。
詳しくは公式ドキュメントを確認してください。
流れとしては、

  • docRefで順方向の生のデータを取り出し、そのデータにドキュメントIDを加え、createdAtを timestampから任意の形に直した上で、OriginListに現在のページに必要な分だけのデータを格納します。
  • 現在テーブルに表示されている最初のデータのドキュメントIDとcreatedAt、最後のデータのドキュメントIDとcreatedAtをそれぞれfirstitem, firsttime,lastitem, lasttimeと定義します。これがページを遷移する際に、最も大事になります。
  • 現在が1ページ目である場合はdocRefから全ての生データの一番最初のドキュメントIDをfuidと定義します。さらに、lastRefで逆方向から一番最初のデータのドキュメントIDもluidと定義します。
  • 現在のページにfuid, luidがそれぞれある場合に、 first, lasttrueにします。

以上の4ステップです。

components/GetData.vue
<script>
export default {

// ...略

  methods: {
    showTime (time) {
      if (time) {
        const year = new Date(time.seconds * 1000).getFullYear().toLocaleString('ja-JP').replace(/,/, '')
        let month = new Date(time.seconds * 1000).getMonth().toLocaleString('ja-JP')
        month = parseFloat(month) + 1
        const day = new Date(time.seconds * 1000).getDate().toLocaleString('ja-JP')
        const thisDate = year + '/' + month + '/' + day
        return thisDate
      } else {
      }
    },
    makeList (filterseed, orderItem, order) {
      // 初期化
      this.OriginList = []
      this.first = false
      this.last = false


      const db = firebase.firestore().collection(this.seed)

      const t = this
      const invList = []
      const key = 0

      
      // 接続先のDBを選択 lasRefは一番最後のUidを取ってくるやつ
      if (this.search === '') {
        this.docRef = db.orderBy(filterseed, order).orderBy('createdAt', order).startAfter(this.baseitem, this.basetime)
        this.lastRef = db.orderBy(filterseed, 'desc').orderBy('createdAt', 'desc')
      } 
      // DB作成
      this.docRef.limit(this.itemsPerPage).get().then(function (snapshot) {
        snapshot.forEach(function (doc) {
          const dd = doc.data()
          dd.uid = doc.id
          dd.createdAtshow = t.showTime(dd.createdAt)
          t.OriginList.push(dd)
        })

        // 前のページに遷移するときは順番を逆にする
        if (orderItem === '<=') { t.OriginList = t.OriginList.reverse() }

        if (t.OriginList.length !== 0) {
          // 表示されているものの最初と最後取ってくる
          t.lastitem = t.OriginList[t.OriginList.length - 1][filterseed]
          t.lasttime = t.OriginList[t.OriginList.length - 1].createdAt
          t.firstitem = t.OriginList[0][filterseed]
          t.firsttime = t.OriginList[0].createdAt

          // 1ページ目ならDB全体の最初と最後も取ってくる
          if (t.page === 1) {
            t.fuid = t.OriginList[0].uid
            t.lastRef.limit(1).get()
              .then(function (snapshot) {
                snapshot.forEach(function (doc) {
                  invList.push({
                    luid: doc.id
                  })
                  t.luid = invList[0].luid
                  // t.$store.commit("sessionUid", {fuid: t.fuid, luid: t.luid})
                  Object.keys(t.OriginList).forEach(function (key) {
                    if (t.OriginList[key].uid === t.luid) { t.last = true }
                  })
                })
              })
              .catch(function (error) {
                console.log(error)
              })
          }
          // 現在表示しているデータに最初と最後あるか確認
          Object.keys(t.OriginList).forEach(function (key) {
            if (t.OriginList[key].uid === t.fuid) { t.first = true }
            if (t.OriginList[key].uid === t.luid) { t.last = true }
          })
        } else {
          t.first = true
          t.last = true
        }
      })
        .catch(function (error) {
          console.log(error)
        })
    }

// ...略

}

ただしこのクエリ方法は、firestoreの複合インデックスというものを使用しています。
初回はその設定を組む必要があります。

複合インデックスを組む

以上のコードを組むと、コンソールに以下のようなログが出てくると思います。

FirebaseError: The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/*******************/firestore/indexes?create_composite=****************************************************************************************************************************
    at new n (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:160:23)
    at t._i (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:3479:16)
    at t.Fi (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:3638:195)
    at n.onMessage (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:10098:33)
    at eval (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:10051:26)
    at eval (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:10082:37)
    at eval (webpack-internal:///./node_modules/@firebase/firestore/dist/index.cjs.js:5099:31)

このログに書かれているURLに飛ぶと

スクリーンショット 2020-05-05 16.35.07.png

このような画面に飛ばされると思います。
この指示に従い、「インデックスを作成」を押すと複合インデックスが自動生成され、数分後にはエラーが起きずに予期したクエリが行われます。

iv. TableにOriginListを渡す

あとはこれをTableに渡して、表示させるだけです。
ただし、先ほど定義したfirstlastの有無に応じて「←ボタン」や「→ボタン」を押せなくする必要があります。
また、リストに名前を表示させたいので、mounted時に、各seedによってlistNameが変化するようにしましょう。

components/Table.vue
template>
  <div>
    <v-card v-show="!showDelete" flat style="background: #F8F8F8; padding: 15px;">
      <v-card-title class="card-title">
        {{ listName }}管理
      </v-card-title>
    <v-data-table
        v-model="selected"
        :headers="headers"
        :items="OriginList"
        :page.sync="page"
        :itemsPerPage.sync="itemsPerPage"
        @page-count="pageCount = $event"
        show-select
        light
        hide-default-footer
        item-key="uid"
        item
      />

      <div style="padding-top: 30px; text-align: center">
        <v-btn @click="prenav()" :disabled="first" class="pagenation" small fab>
          <v-icon>mdi-chevron-left</v-icon>
        </v-btn>
        <v-btn class="pagenation-center" small fab disabled>
          <span>{{ page }}</span>
        </v-btn>
        <v-btn @click="nextnav()" :disabled="last" class="pagenation" small fab>
          <v-icon>mdi-chevron-right</v-icon>
        </v-btn>
      </div>
    </v-card>
  </div>
</template>

<script>
export default {

// ...略

  mounted () {
    if (this.seed === 'users') {
      this.filterName = '名前'
      this.filterseed = 'name'
    } else if (this.seed === 'books') {
      this.filterName = '題名'
      this.filterseed = 'title'
    }
  },
  methods: {
    prenav () {
      const currentpage = this.page - 1
      const orderItem = '<='
      const order = 'desc'
      this.$emit('handlepage', currentpage, this.filterseed, orderItem, order)
    },
    nextnav () {
      const currentpage = this.page + 1
      const orderItem = '>='
      const order = 'asc'
      this.$emit('handlepage', currentpage, this.filterseed, orderItem, order)
    }
  }

// ...略

}
</script>

v. ページの遷移

ここで本記事のメインであるページ遷移を行います。
**vi.**の工程にあるように、prenavnextnavでページの遷移を行います。
それぞれmakeListに遷移後のページ番号等の必要な値をGetDataファイルに渡します。

その後GetDataファイルのupdatepageメソッドが発火します。

components/GetData.vue
<script>
export default {
// ...略
  methods: {
// ...略
    updatepage (handlepage, filterseed, orderItem, order) {
      this.page = handlepage
      if (orderItem === '>=') {
        this.baseitem = this.lastitem
        this.basetime = this.lasttime
      } else if (orderItem === '<=') {
        this.baseitem = this.firstitem
        this.basetime = this.firsttime
      }
      this.makeList(filterseed, orderItem, order)
    }
  }
// ...略
}
</script>

このように「←ボタン」「→ボタン」に応じて、**iii.**の工程で定めた、firstitemfirsttime, lastitemlasttimeのどちらか1組をbaseitembasetimeに代入します。
こうすることで、次にmakeListメソッドでクエリする際に、baseitembasetimeから始めることができます。

以上で、ページ遷移の部分のコードは完成です。

2. ソートおよび前方一致検索

ここで、検索機能を追加します。
firestoreのクエリ機能はまだ充実してはいないため、完全な部分一致検索はできません。ですが、既存のクエリ機能を組み合わせて前方一致のみは実現可能です。
また、ソート機能はそれ単体であるのではなく、検索機能で「''」を検索した時に作動する感じです。

前方一致検索のクエリの仕組み

if (orderItem === '>=') {
   this.deorderItem = '<='
} else if (orderItem === '<=') {
   this.deorderItem = '>='
}

if (order === 'asc') {
   this.startAtItem = this.search
   this.endAtItem = this.search + '\uF8FF'
} else if (order === 'desc') {
   this.startAtItem = this.search + '\uF8FF'
   this.endAtItem = this.search
}

this.docRef = db.orderBy(filterseed, order).orderBy('createdAt', order).where(filterseed, orderItem, this.startAtItem).where(filterseed, this.deorderItem, this.endAtItem).startAfter(this.baseitem, this.basetime)

**1.- iii.**であったように、最初の2つのorderByfilterseed順、createdAt順に並べた後に、whereで始まるポイントと終わるポイントを決めます。
その後、startAfterでページ遷移する際の基準となるポイントを指定します。
イメージとしては以下のような感じです。

スクリーンショット 2020-05-05 17.32.21.png

ではこれを組み込んでみます。

components/GetData.vue
<script>
export default {
  data () {
    return {

// ...略

      startAtItem: '',
      endAtItem: '',
      filterseed: 'name',

// ...略

    }
  },
  methods: {
    makeList (filterseed, orderItem, order) {
       if (orderItem === '>=') {
        this.deorderItem = '<='
      } else if (orderItem === '<=') {
        this.deorderItem = '>='
      }
      if (order === 'asc') {
        this.startAtItem = this.search
        this.endAtItem = this.search + '\uF8FF'
      } else if (order === 'desc') {
        this.startAtItem = this.search + '\uF8FF'
        this.endAtItem = this.search
      }
      
      if (this.search === '') {
        this.docRef = db.orderBy(filterseed, order).orderBy('createdAt', order).startAfter(this.baseitem, this.basetime)
        this.lastRef = db.orderBy(filterseed, 'desc').orderBy('createdAt', 'desc')
      } else if (this.search !== '') {
        this.docRef = db.orderBy(filterseed, order).orderBy('createdAt', order).where(filterseed, orderItem, this.startAtItem).where(filterseed, this.deorderItem, this.endAtItem).startAfter(this.baseitem, this.basetime)
        this.lastRef = db.orderBy(filterseed, 'desc').orderBy('createdAt', 'desc').where(filterseed, orderItem, this.startAtItem).where(filterseed, this.deorderItem, this.endAtItem)
      }

// ...略

    },
    updatesearch (filterseed, orderItem, order, search) {
      this.page = this.initial.page
      this.baseitem = this.initial.baseitem
      this.search = search
      this.makeList(filterseed, orderItem, order)
    },

// ...略

  }
}
</script>
components/Table.vue
<template>
  <div>
    <v-card v-show="!showDelete" flat style="background: #F8F8F8; padding: 15px;">
      <v-card-title class="card-title">
        {{ listName }}管理
      </v-card-title>
      <v-card-actions>

        <v-col v-if="seed == 'books'" cols="2">
          <v-select
            :items="selectItem"
            v-model="filterseed"
            item-text="name"
            item-value="id"
            light
            outlined
            dense
            chips
            color="black"
            label="検索区分"
            class="v-selectItem"
          />
        </v-col>
        <v-col cols="6">
          <v-text-field
            v-bind:value="search"
            @keyup.enter="searchitem()"
            v-on:input="newsearch = $event"
            v-bind:label="filterName"
            outlined
            style="color: black"
            color="black"
            hide-details
            light
            dense
          />
        </v-col>
        <v-btn
          @click="searchitem()"
          color="blue"
          class="mr-4"
          style="float: right;"
        >
          検索
        </v-btn>
      </v-card-actions>

      <v-data-table

// ...

      />
    </v-card>
  </div>
</template>

<script>
export default {
  props: {

// ...略

    search: {
      type: String,
      default: ''
    },

// ...略

  },

// ...略

  computed: {
    filterName () {
      if (this.filterseed === '') {
        return ''
      } else if (this.selectItem.length === 0) {
        if (this.seed === 'users') {
          return '名前'
        } else {
          return ''
        }
      } else {
        const t = this
        const val = this.selectItem
        let target = ''
        Object.keys(val).forEach(function (key) {
          if (val[key].id === t.filterseed) { target = val[key].name }
        })
        return target
      }
    }
  },
  mounted () {
    console.log(this.seed)
    if (this.seed === 'users') {
      this.listName = 'ユーザー'
      this.filterseed = 'name'
      console.log(this.seed)
    } else if (this.seed === 'books') {
      this.listName = '題名'
      // this.filterseed = 'title'
      this.selectItem = [
        { name: '題名', id: 'title' },
        { name: '区分', id: 'type' }
      ]
    }
  },
  methods: {

// ...略

    searchitem () {
      if (!this.newsearch) { this.newsearch = '' }
      const orderItem = '>='
      const order = 'asc'
      this.$emit('handlesearch', this.filterseed, orderItem, order, this.newsearch)
  }
}
</scirpt>

mountedseedbooksの時にfilterseedの代わりにselectItemを追加しています。
このようにすることで、<v-select>selectItemから自由にfilterseedを変更できるようになります。

3. 状態を保存する

Vue.jsを使う人に関しては既知の内容かもしれませんが、プロジェクト内の状態はVuexを使います。
Nuxt.jsでは、その記述を/store/index.jsに記述していきます。
stoteの詳しい内容はこちらをご覧ください。

今回状態の保存が必要な変数は、fuid, luid, page, path, filterseed, orderItem, order, baseitem, searchです。

ではstore/index.jsに記述していきます。

store/index.js
export const state = () => ({
  fuid: '',
  luid: '',
  page: 1,
  path: '',
  filterseed: '',
  orderItem: '',
  order: 'asc',
  baseitem: '',
  search: ''
})

export const mutations = {
  sessionUid (state, payload) {
    console.log('store の sessionUid')
    state.fuid = payload.fuid
    state.luid = payload.luid
  },
  sessionPage (state, payload) {
    state.page = payload.page
    state.filterseed = payload.filterseed
    state.orderItem = payload.orderItem
    state.order = payload.order
    state.baseitem = payload.baseitem
  },
  sessionSearch (state, payload) {
    console.log('store の sessionSearch')
    state.search = payload.search
  },
  sessionOnlyFilterseed (state, payload) {
    console.log('store の sessionOnlyFilterseed')
    state.filterseed = payload.filterseed
  },
  sessionPath (state, payload) {
    console.log('store の sessionPath')
    state.path = payload.path
  }
}

storeでは状態を保存し続けるため、違うテーブルに変更した時に予期せぬエラーが起きます。
そこで、画面が遷移した時のみ状態をリセットするようにmidlewareを設定します。

/midlware/index.js
export default function ({ route, store, redirect }) {
  if (route.path !== store.state.path) {
    if (route.path === '/') {
      store.commit('sessionPage', { page: 1, filterseed: 'name', orderItem: '>', order: 'asc', baseitem: '' })
      store.commit('sessionSearch', { search: '' })
      store.commit('sessionUid', { luid: '', fuid: '' })
    } else if (route.path === '/Book') {
      store.commit('sessionPage', { page: 1, filterseed: 'title', orderItem: '>', order: 'asc', baseitem: '' })
      store.commit('sessionSearch', { search: '' })
      store.commit('sessionUid', { luid: '', fuid: '' })
    }
  }
}
/nuxt.config.js
export default {

// ...略

  router: {
    middleware: 'index'
  },

// ...略

}

こうすることで、リロード時には状態を保存できるが画面が遷移した時にはリセット出来ます。

ではstoreの値を使うようにcomponentsの記述も変えていきます。

components/Getdata.vue
<script>
export default {

// ...略

  created () {
    this.$store.commit('sessionPath', { path: this.$route.path }) // ルート変わっていたら、その都度初期化
    this.page = this.$store.state.page // ページをstoreから持ってくる
    this.fuid = this.$store.state.fuid// DB全体最初のuidをstoreから持ってくる
    this.luid = this.$store.state.luid// DB全体最初のuidをstoreから持ってくる
    this.baseitem = this.$store.state.baseitem// 前回のデータから今回表示するデータの基準になるものをstoreからもっってくる
    this.makeList(this.$store.state.filterseed, this.$store.state.orderItem, this.$store.state.order)
  },
  methods: {

// ...略

    updatesearch (filterseed, orderItem, order, search) {
      this.page = this.initial.page
      this.baseitem = this.initial.baseitem
      this.search = search
      this.$store.commit('sessionSearch', { search: this.search })
      this.$store.commit('sessionPage', { page: this.page, filterseed, orderItem, order, baseitem: this.baseitem })
      this.makeList(filterseed, orderItem, order)
    },
    updatepage (handlepage, filterseed, orderItem, order) {
      this.page = handlepage
      if (orderItem === '>=') {
        this.baseitem = this.lastitem
        this.basetime = this.lasttime
        console.log('this.baseitem, this.basetime')
        console.log(this.baseitem, this.basetime)
      } else if (orderItem === '<=') {
        this.baseitem = this.firstitem
        this.basetime = this.firsttime
        console.log('this.baseitem, this.basetime')
        console.log(this.baseitem, this.basetime)
      }
      this.$store.commit('sessionPage', { page: this.page, filterseed, orderItem, order, baseitem: this.baseitem })
      this.makeList(filterseed, orderItem, order)
    }
  }
}
</script>

components/Table.vue
<script>
export default {
  mounted () {
    this.filterseed = this.$store.state.filterseed

// ...略
 
  },
  methods: {

// ...略

    searchitem () {
      if (!this.newsearch) { this.newsearch = '' }
      const orderItem = '>='
      const order = 'asc'
      this.$store.commit('sessionOnlyFilterseed', { filterseed: this.filterseed })
      this.$emit('handlesearch', this.filterseed, orderItem, order, this.newsearch)
    }
  }
}
</script>

これでリロードしても検索の値、ページ数等が維持できるのではないでしょうか。

まとめ

出来たことには出来たのですが、Algoriaの方が簡単そうでした…
今度Algoriaで実装してみます。

わからない点・間違えている点・より良い方法などありましたらご指摘いただけると泣いて喜びます!!

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?