【RailsAPI + Vue.js】Pagyを用いたページネーションの実装

## はじめに

RailsAPIとVuetifyでページネーションを作りました。
gemをどれにしようか調べてみたところ、[Pagyがやたらとシンプル！軽い！](https://ddnexus.github.io/pagination-comparison/gems.html)ということらしいので、Pagyを使いました。


### 環境、使用技術
- Rails  5.2.4.2
- Pagy  3.8.1
- Vue.js  4.3.1
- Vuetify  2.2.21
- axios  0.19.2

Vuetifyは他のものでも置き換え可能かなと思います。

## Rails側
### Pagyの初期設定

[How To | Pagy](https://ddnexus.github.io/pagy/how-to#quick-start)に書いてある通りです。

```:Gemfile
gem 'pagy', '~> 3.5'
```
毎度おなじみ`$ bundle install`を実行し、`config/initializers/pagy.rb`に設定ファイルを作成します。
[テンプレート](https://github.com/ddnexus/pagy/blob/master/lib/config/pagy.rb)をコピペして、必要なところだけコメントアウトを外します。

```config/initializers/pagy.rb
Pagy::VARS[:items] = 3  # 1ページに3件取得する
```

### コントローラ

```diff:app/controllers/api/v1/tweets_controller.rb
class Api::V1::UsersController < Api::V1::BaseController
+ include Pagy::Backend

  def index
-   users = User.all
+   pagy, users = pagy(User.all)
    render json: users
  end
end
```

PostmanでAPIを叩いてレスポンスを確認してみます。
<img width="834" alt="スクリーンショット 2020-05-19 20.34.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/6eaa5a02-837c-7131-5036-a1a52aa2c459.png">
このように、userのデータが3件ずつ取得できていました（シリアライザーを使っているので、カラム名がキャメルケースになっています）。

しかし、これだけでは現在のページや総ページ数がわかりません。フロント側のページネーションコンポーネントではそれらのデータが必要なので、追加で記述していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/c1bec51f-fc50-5771-2707-f6c460c98f25.png)


### ヘッダーにページの情報を入れる

```diff:app/controllers/api/v1/tweets_controller.rb
+ require 'pagy/extras/headers'

class Api::V1::UsersController < Api::V1::BaseController
  include Pagy::Backend

  def index
    pagy, users = pagy(User.all)
+   pagy_headers_merge(pagy)
    render json: users
  end
end
```

引用：[Headers | Pagy](https://ddnexus.github.io/pagy/extras/headers.html)

この記述により、レスポンスヘッダーに以下の情報が格納されます。
<img width="840" alt="スクリーンショット 2020-05-19 21.00.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/b8a0a530-1321-5d5a-0a10-d72e57782316.png">

| KEY |
|------------|-----------
|Link        |最初・最後のページ、前・次のページのリンク
|Current-Page|現在のページ番号
|Page-Items  |1ページのuserの数
|Total-Pages |全てのページ数
|Total-Count |全てのuserの数

"Link"の中身（実際は一行）↓

```
<http://127.0.0.1:3000/api/v1/users?page=1>; rel="first",
<http://127.0.0.1:3000/api/v1/users?page=1>; rel="prev",
<http://127.0.0.1:3000/api/v1/users?page=3>; rel="next",
<http://127.0.0.1:3000/api/v1/users?page=3>; rel="last"
```

これでRails側の処理は終わりです。
共通化する場合は、`after_action`を使う方法もあります（see 公式）。


## Vue側

Vue-routerは使っていません。

### テンプレート部分

[Pagination component — Vuetify.js](https://vuetifyjs.com/ja/components/paginations/)を少しカスタマイズします。

```vue
<template>
  <div class="text-center">
    <v-pagination
      v-model="currentPage"
      :length="page.totalPages"
    ></v-pagination>
  </div>
</template>
<script>
  export default {
    data () {
      return {
        requestUrl: "/api/v1/users",
        page: {
          currentPage: 1,
          totalPages: 5,
        }
      }
    },
  }
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/079d3297-1c16-2542-7510-e6502acc6380.png)
これでひとまずページネーションを表示することはできましたが、まだ、ボタンを押しても`page.currentPage`の値が変わるだけです。


### ボタンを押したときの挙動

コンポーネントから`@input`イベントを受け取り、`changePage`メソッドで処理を行います。

```diff
<template>
  <div class="text-center">
    <v-pagination
      v-model="currentPage"
      :length="page.totalPages"
+     @input="changePage"
    ></v-pagination>
  </div>
</template>
<script>
export default {
  data () {
    return {
+     requestUrl: "/api/v1/users",
      page: {
        currentPage: 1,
        totalPages: 5,
      }
    }
  },
+ methods: {
+   changePage(val) {
+     // 処理
+   }
+ }
}
</script>
```
```js
methods: {
  async changePage(val) {
    // "/api/v1/users?page=2"などにGETリクエストを送る
    const response = await this.$axios.get(`${this.requestUrl}?page=${val}`)
    // 受け取ったusersデータを格納する
    const { users } = response.data
    this.users = users
  }
}
```


### ページ読み込み時のデータ取得

`mounted`で最初の画面描画時の動きを記述します。

```js
async mounted() {
  try {
    // "/api/v1/users"にGETリクエストを送る
    const response = await this.$axios.get(this.requestUrl)
    // それぞれのdataにレスポンスの値を代入する
    this.page.totalPages = Number(response.headers["total-pages"])
    const { users } = response.data
    this.users = users
  }
}
```

### 最終的なコード

```vue
<template>
  <!-- usersの表示部分。省略 -->
  <div class="text-center">
    <v-pagination
      v-model="page.currentPage"
      :length="page.totalPages"
      @input="changePage"
    />
  </div>
</template>

<script>
import goTo from "vuetify/es5/services/goto"  // しれっと追加している
export default {
  data() {
    return {
      requestUrl: "/api/v1/users",
      page: {
        currentPage: 1,
        totalPages: 1,
      },
      users: []
    }
  },
  async mounted() {
    try {
      const response = await this.$axios.get(this.requestUrl)
      this.page.totalPages = Number(response.headers["total-pages"])
      const { users } = response.data
      this.users = users
    }
  },
  methods: {
    async changePage(val) {
      goTo(0)  // ページ最上部までスクロール。Vuetifyのメソッド
      const res = await this.$axios.get(`${this.requestUrl}?page=${val}`)
      const { users } = res.data
      this.users = users
    }
  }
}
</script>
```


## ちなみに

### 追加でヘッダーに情報を渡す場合

以下のように書くことで追加できます。`requestUrl`を初期値のdataで設定するのが難しい場合は、このようにヘッダーに渡して受け取る方法もあります。

```rb
  def index
    pagy, users = pagy(User.all)
    pagy_headers_merge(pagy)
    response.headers.merge!({ 'Request-Url' => request.path_info })
    render json: users
  end
```

<img width="822" alt="スクリーンショット 2020-05-20 18.31.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/47a31683-d348-3630-ad70-24afe54968eb.png">

### 


## 参考リンク

[rails APIでページネーションを実装する](https://masahiro.me/2017/05/rails-api%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B/)
[【vue.js】 Vuetifyで簡単ページネーション(Paginations)](https://reffect.co.jp/vue/vuetify-simple-pagination)