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


はじめに

RailsAPIとVuetifyでページネーションを作りました。
gemをどれにしようか調べてみたところ、Pagyがやたらとシンプル!軽い!ということらしいので、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に書いてある通りです。

Gemfile
gem 'pagy', '~> 3.5'

毎度おなじみ$ bundle installを実行し、config/initializers/pagy.rbに設定ファイルを作成します。
テンプレートをコピペして、必要なところだけコメントアウトを外します。

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

コントローラ

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を叩いてレスポンスを確認してみます。

このように、userのデータが3件ずつ取得できていました(シリアライザーを使っているので、カラム名がキャメルケースになっています)。

しかし、これだけでは現在のページや総ページ数がわかりません。フロント側のページネーションコンポーネントではそれらのデータが必要なので、追加で記述していきます。

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

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

この記述により、レスポンスヘッダーに以下の情報が格納されます。

KEY
Link
Current-Page
Page-Items
Total-Pages
Total-Count

"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を少しカスタマイズします。

<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>


これでひとまずページネーションを表示することはできましたが、まだ、ボタンを押してもpage.currentPageの値が変わるだけです。

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

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

<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>
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で最初の画面描画時の動きを記述します。

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
  }
}

最終的なコード

<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で設定するのが難しい場合は、このようにヘッダーに渡して受け取る方法もあります。

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

参考リンク

rails APIでページネーションを実装する
【vue.js】 Vuetifyで簡単ページネーション(Paginations)