vue-router + apollo + vuetify でページネーションを作る

ページネーションの実装も、vue-routerとapollo、vuetifyを使うと比較的簡単に実装できました。

デモはこちら。
https://vue-sample-blog.firebaseapp.com/pagingDemo
全コードはこちら。
https://github.com/kawamataryo/vue-sample-blog/blob/master/src/views/PagingDemo.vue

このようなページネーションを作成します。
![output1.gif](https://qiita-image-store.s3.amazonaws.com/0/114396/90c4f59a-7826-65ae-b491-19aab0489ebe.gif)

[HeadLessCMS + GraphQL + Vue.js でSPA（ブログ）を作ってみる](https://qiita.com/ryo2132/items/75ed2e3dcb6a1bec91a1)
で作成したブログを元に使っています。

# 前提

下のようなGraphQLのスキーマを想定します

```
#sample blog post
type Post {
    #The status of the record
    status: Status!
    id: ID!
    createdAt: DateTime!#The time the record was updated
    updatedAt: DateTime!
    title: String
    description: String
    content: String
    thumbnail: Asset
}
```

# ページング用のクエリの作成

postモデルのレコード数（投稿総数）とページ単位での取得用のクエリを用意します。

```graphql.js
import gql from 'graphql-tag'

// postモデルのレコード数を取得
export const MAX_POST_COUNT = gql`
    query maxPostCount{
        postsConnection {
            aggregate {
                count
            }
        }
    }
`

// ページ単位で取得
export const FEACH_POST_BY_PAGE = gql`
    query feachPostByPage($displayUnit: Int, $page: Int) {
        posts(first: $displayUnit, skip: $page, orderBy: createdAt_DESC) {
            id
            title
            content
            description
            createdAt
            thumbnail {
                url
            }
        }
    }
`
```

簡単にクエリの説明です。
まず総数の取得は、、

```graphql
    query maxPostCount  {
        # model名の複数形+Connection
        postsConnection  {
            # 集計の意味
            aggregate {
                # 総数を取得
                count 
            }
        }
    }
```

page単位の取得は、、

```graphql
    query feachPostByPage($displayUnit: Int, $skip: Int) {
        # first = 最初から何件取得するか？ $displayUnitで指定
　　　　 # skip = 抜かす件数 $skipで指定
        # orderBy = 並び順 createdAtの降順
        posts(first: $displayUnit, skip: $skip, orderBy: createdAt_DESC) {
            ...
        }
    }
```
なお、この部分はお使いのGraphQLのAPIサーバーの定義により異なるかもしれません。
GraphCMSの場合は、上記で取得できました。

# vuetifyへの組み込み

## サンプルコード
あとはvietifyのテンプレートに流し込めば終了です。

```vue

<template>
  <div>
    <v-container grid-list-xl>
      <!-- ローディングアイコン-->
      <div class="text-xs-center" v-if="loading">
        <v-progress-circular
            :size="50"
            color="primary"
            indeterminate
        ></v-progress-circular>
      </div>
      <!--投稿一覧-->
      <v-layout row wrap v-if="!loading">
        <PostCard
            v-for="post in posts"
            v-bind:key="post.id"
            :post=post
        ></PostCard>
      </v-layout>
      <!--ページネーション-->
      <div class="text-xs-center mt-5">
        <v-pagination
            v-model="pageNumber"
            :length="pageLength"
        ></v-pagination>
      </div>
    </v-container>
  </div>
</template>

<script>
  import HomeMainVisual from '../components/HomeMainVisual'
  import PostCard from '../components/PostCard'
  import {FEACH_POST_BY_PAGE, MAX_POST_COUNT} from "../constants/graphql"

  export default {
    name: "PostList",
    components: {
      PostCard,
      HomeMainVisual
    },
    data: () => ({
      pageNumber: 1, // 表示するページ番号
      pageDisplayUnit: 3, // 1ページの表示件数
      maxPostCount: 0, // 投稿総数（初期化で0）
      posts: [], // 投稿一覧
      loading: 0, // 通信の状態 apolloで制御
    }),
    mounted() {
      // マウント後に、vue-routerでクエリーパラムを取得
      // 取得したクエリーパラムを表示ページに設定
      let queryPage = this.$route.query.page
      this.pageNumber = queryPage != null ? parseInt(queryPage) : 1
    },
    watch: {
      // watchでpageNumberの状態を監視
      // pageNumberがページネーションのクリックで変化したときに、
      // vue-routerでページを変化させる
      pageNumber: function (newNumber) {
        this.$router.push({name: 'PagingDemo', query: {page: newNumber}})
      }
    },
    computed: {
      // 投稿総数を、1ページの表示件数で割ることで総ページ数を計算
      pageLength: function () {
        return Math.ceil(this.maxPostCount / this.pageDisplayUnit)
      }
    },
    apollo: {
      // 投稿総数の取得
      maxPostCount: {
        query: MAX_POST_COUNT,
        update(data) {
          return data.postsConnection.aggregate.count
        }
      },
      // ページごとの投稿を取得
      posts: {
        query: FEACH_POST_BY_PAGE,
        variables() {
          return {
            page: (this.$route.query.page - 1) * this.pageDisplayUnit,
            displayUnit: this.pageDisplayUnit
          }
        }
      },
    }

  }
</script>

<style>
  .v-pagination__navigation .v-icon {
    font-size: 1rem !important;
  }
</style>

```

## 解説
vuetifyとapolloのみでも見た目的にはページングはOKなのですが、URLが変化せず、
実質ajaxで読み込むスライドショーのような状態になってしまいます。
なので、vue-routerを連動させることで、プラウザの戻る、進むの対応、URLでのページ単位のアクセスを実装しています。

それを担っているのが、mountと、watchの処理です。

まず、vueのライフサイクルでDOMにアクセスが可能になるmoutedで、pageNumber（表示ページ）をURLのクエリパラーメーターより取得して設定しています。
（三項分岐で判定し、パラメーターがない場合は1ページ目として設定）

```
    mounted() {
      let queryPage = this.$route.query.page
      this.pageNumber = queryPage != null ? parseInt(queryPage) : 1
    },
```

さらにwatchでpageNumberの状態を監視。
pageNumberがページネーションのUIコンポーネントのクリックで変化したときに、vue-routerでページを変化させるという処理を実行しています。

```vue
    watch: {
      pageNumber: function (newNumber) {
        this.$router.push({name: 'PagingDemo', query: {page: newNumber}})
      }
    },
```

あとは、それぞれの変数に、データをバインディングして、Apolloで取得。表示という流れです。


# おまけ
１件あたりの表示件数を設定しているpageDisplayUnitもリアクティブにすれば、このようなUXも表現もできます。
（使い所は不明）

スライダーで変更する1ページの表示数に追随して、ページリンクも変化しています。
jQueryで実装しようとすると大変だと思うのですが、vueならとても簡易に実装できて驚愕です。

![output1.gif](https://qiita-image-store.s3.amazonaws.com/0/114396/268206dc-785f-a00f-705f-70947c5fc856.gif)