This page looks best with JavaScript enabled

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

 ·   ·  ☕ 4 分で読めます

はじめに

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やaxiosは他のものでも置き換え可能かなと思います。

Rails側

Pagyの初期設定

How To | Pagyに書いてある通りです。

1
gem 'pagy', '~> 3.5'

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

1
Pagy::VARS[:items] = 3  # 1ページに3件取得する

コントローラ

1
2
3
4
5
6
7
8
9
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を叩いてレスポンスを確認してみます。
スクリーンショット 2020-05-19 20.34.28.png
このように、userのデータが3件ずつ取得できていました(シリアライザーを使っているので、カラム名がキャメルケースになっています)。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
+ 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

この記述により、レスポンスヘッダーに以下の情報が格納されます。
スクリーンショット 2020-05-19 21.00.58.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を少しカスタマイズします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<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
これでひとまずページネーションを表示することはできましたが、まだ、ボタンを押してもpage.currentPageの値が変わるだけです。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>
1
2
3
4
5
6
7
8
9
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で最初の画面描画時の動きを記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
  }
}

最終的なコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<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で設定するのが難しい場合は、このようにヘッダーに渡して受け取る方法もあります。

1
2
3
4
5
6
  def index
    pagy, users = pagy(User.all)
    pagy_headers_merge(pagy)
    response.headers.merge!({ 'Request-Url' => request.path_info })
    render json: users
  end
スクリーンショット 2020-05-20 18.31.43.png

参考リンク

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

Share on

aiandrox
Written by
aiandrox
今日も楽しく明日も楽しく

目次