Rails + Vue.js でページネーション付きのテーブルを簡単作成

## 概要
* 業務でページネーション機能を実装したので、ほとんどそのままの構成で手順を紹介
* 使用した技術は`kaminari（Rails）`と`Vuetify（Vue.js）`
* api経由でデータを取得し、ページネーション付きで表示する
* [ソースコード](https://github.com/MrFuku/rails-vue-pagination-app)

Railsの開発環境は特に説明しないが、以下の記事を参考に構築しました
[Rails 6 + MySQL on Dockerの環境を秒速で構築する](https://masaki.blog/rails6-on-docker/)

## Bookモデルの定義とサンプルデータの作成
今回はデータベースに保存したBookの一覧をapiで取得します
まずはBookモデルを作成しましょう

```bash
# マイグレーションファイルの作成
bin/rails g model Book name:string

# マイグレーションを実行し、Booksテーブルを作成
bin/rails db:migrate
```
`db/seeds.rb`を編集しサンプルデータを作成

```ruby:db/seeds.rb
100.times do |n|
  name  = "example-#{n+1}"
  Book.create!(name: name)
end
```
seedを実行

```bash
bin/rails db:seed
```
これでBookレコードが100件作成されました

## kaminariのインストールとBook一覧取得用apiの作成
Bookテーブルからデータを取得する際にkaminariを使用します
kaminariをインストールします
https://github.com/kaminari/kaminari

```ruby:Gemfile
# kaminariを追記
gem 'kaminari'
```

```bash
# kaminariのインストール。インストール完了後にサーバーを再起動させましょう
bundle
```

`app/controllers/api/books_controller.rb`を作成し、Book一覧を返すapiを実装します

```ruby:app/controllers/api/books_controller.rb
class Api::BooksController < ApplicationController
  def index
    # 表示するページの番号を指定
    page = params[:page] || 1

    # １ページあたりの表示件数を指定
    per = params[:per] || 10

    # ページネーションで指定レコードを取得
    books = Book.page(page).per(per)

    # ページネーションした時の全ページ数
    total_pages = books.total_pages

    # レスポンスデータの定義
    response = {
      # bookレコードはidとnameフィールドのみ表示する
      books: books.select(:id, :name),
      total_pages: total_pages
    }

    # json形式でレスポンスを返却
    render json: response
  end
end
```

```ruby:config/routes.rb
Rails.application.routes.draw do
  # Book一覧取得用のパス
  get '/api/books', to: 'api/books#index'

  # Book一覧表示用のパス
  get '/books', to: 'books#index'
end
```
`http://localhost:3000/api/books`にアクセスすると次のようなjsonが返ってきます

![スクリーンショット 2020-05-18 22.22.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/ba1b6910-ad79-10c6-c239-7871aa8885fa.png)

## Book一覧表示ページの作成

Book一覧表示用のページを作成します
Vuetifyの`v-data-tableコンポーネント`と`v-pagination`を使い、Axiosでapiを叩きます
※ ここでは面倒を避けるためCDN経由で環境構築をしてあります。適宜ご自身の環境に合わせた環境構築を行なってください

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>AppName</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%# CDNで Vue.js, Vuetify, Axios をインストールする %>
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <%# ここも追加 %>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
  <body>
    <%= yield %>
  </body>
</html>
```

```erb:app/views/books/index.html.erb
<div id="app">
  <v-app>
    <v-container>
      <h2>Book一覧</h2>
      <%# テーブル作成用コンポーネント %>
      <v-data-table
        :headers="headers"
        :items="items"
        :items-per-page="itemsPerPage"
        hide-default-footer
      />
    </v-container>
    <%# ページネーション表示用コンポーネント %>
    <v-pagination
      v-model="currentPage"
      :length="totalPages"
      <%# ページを変更した時にfetchBooksを呼び出す %>
      @input="fetchBooks"
    />
  </v-ap>
</div>

<script>
  new Vue({
    el: "#app",
    vuetify: new Vuetify(),
    data() {
      return {
        // テーブルのヘッダー情報。valueの値がレコードのフィールド名に紐付く
        headers: [
          { text: "ID", value: "id"},
          { text: "本の名前", value: "name"},
        ],
        // テーブルのボディー情報。apiで取得したBook一覧をここに格納する
        items: [],
        // 表示するページの番号
        currentPage: 1,
        // １ページあたりの表示件数
        itemsPerPage: 10,
        // ページネーションした時の全ページ数
        totalPages: null,
      }
    },
    methods: {
      // AxiosでBook取得apiにリクエストを送る
      fetchBooks() {
        const url = `/api/books?page=${this.currentPage}?per=${this.itemsPerPage}`;
        axios
          .get(url)
          .then(res => {
            // Book一覧を取得
            this.items = res.data.books;
            // ページネーションした時の全ページ数を取得
            this.totalPages = res.data.total_pages;
        })
      }
    },
    // DOMが作成された時に fetchBooks を呼び出す
    created() {
      this.fetchBooks()
    },
  });
</script>
```

`http://localhost:3000/books`
これでページネーションは完成です
以下のようにページを切り替える度に表示が変わればOKです！

<img border="1" width="1192" alt="スクリーンショット 2020-05-18 23.38.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/7b3061b5-322c-3ce5-cf36-2421d1fbde29.png">

<img border="1" width="1181" alt="スクリーンショット 2020-05-18 23.38.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366929/67de3f52-acc1-f8ef-4ed3-66cba64a4270.png">