【Rails】gem 'kaminari' と Tailwind CSS + daisyUI でページネーションをカスタマイズする

# はじめに
こんにちは、ゆうき(@yuki31100725)と申します。
現在、プログラミングスクールに通ってRuby on Railsを学習しております。

:::note warn
私はプログラミング学習中で、初学者です。
内容に誤りのある場合がございます。
もし間違いがあればご指摘いただけますと幸いです。
:::


# 記事の概要
gem 'kaminari'を導入してページネーションを実装した際、導入済みだったTailwind CSS (+ daisyUI)を用いてレイアウト調整を試みました。

本記事では、実装例とともにレイアウト調整の手順についてご紹介できればと思います。


# 記事のゴール
gem 'kaminari'でページネーション実装後、Tailwind CSS (+ daisyUI)を用いて下記のようなレイアウトにすることを目標としています。

【レイアウト調整前】
<a href="https://gyazo.com/8bd65190b5107504f14d12e156350bed"><img src="https://i.gyazo.com/8bd65190b5107504f14d12e156350bed.png" alt="Image from Gyazo" width="335"/></a>

【レイアウト調整後】
<a href="https://gyazo.com/bf872805932c8985e41fd1a778a4d2cb"><img src="https://i.gyazo.com/bf872805932c8985e41fd1a778a4d2cb.png" alt="Image from Gyazo" width="364"/></a>


# 背景
gem 'kaminari'は、CSSフレームワークのbootstrapと併せて使用することで簡単にレイアウト調整できるため、下の記事のように関連記事が幾つか散見されます。

https://zenn.dev/yukihaga/articles/3d49208638e397

一方、Tailwind CSSを使用している場合の実装例を書かれている記事は見当たらなく、私自身実装に少々時間がかかったため記事執筆に至りました。


# 環境
- Rails 7.1.3.4
- Ruby 3.2.3
- Tailwind CSS 3.4.7
- daisyUI 4.12.10

:::note info
当該記事は、Tailwind CSS、daisyUIの導入手順までは含みませんので、既に導入済みの状態で見て頂けると幸いです。

また、bootstrapやTailwind CSS (+ daisyUI)未導入であっても、下記のような流れでレイアウト調整できるため、途中まではご参考になるかと思います。
1. gem 'kaminari' におけるビューのカスタムテンプレートを導入
2. ページネーションの見た目をCSSで整える 
:::


# ページネーションとgem 'kaminari'について
## ページネーションについて
ページネーションとは、大量のデータを複数のページに分割して表示する技術を指します。
この技術を使用することで、ページの読み込み時間を短縮することが可能になります。
下の画像はGoogleの検索結果画面でよく見るページネーションのデザインになります。

<a href="https://gyazo.com/025f0ff4d4b579f26949284742bd4128"><img src="https://i.gyazo.com/025f0ff4d4b579f26949284742bd4128.png" alt="Image from Gyazo" width="372"/></a>

## gem 'kaminari'について
gem 'kaminari'はページネーションを簡単に実装するためのgemです。


# 実装手順
## 手順概要
1. **ページネーションの導入**
1.1. kaminariをGemfileに追加する
1.2. 設定ファイルを生成し、初期設定を変更する
1.3. コントローラーの記述を修正する
1.4. ビューの設定を記述する

2. **ページネーションのカスタマイズ**
2.1. kaminariにおけるビューのカスタムテンプレートを導入
2.2. カスタムテンプレートの中身を見てみる
2.3. Tailwind CSS適用と必要なテンプレートの選定
2.4. daisyUIのコンポーネントを利用する
2.5. ページ数の表示制限
2.6. 表記をカスタマイズする

## 手順詳細
### 1. ページネーションの導入
#### 1.1 kaminariをGemfileに追加する
以下の通り、Gemfileにkaminariを追加します。

```Gemfile
gem 'kaminari'
```
以下のコマンドを実行してインストールします。

```terminal
bundle install
```


#### 1.2 設定ファイルを生成し、初期設定を変更する
設定ファイルを生成するため、以下のコマンドを実行します。

```terminal
rails g kaminari:config
```

コマンド実行後、```config/initializers```ディレクトリに生成される```kaminari_config.rb```を開きます。
ここでは、アプリにおいてデフォルトで使用したい1ページあたりのレコード数を設定します。
最初に25が記入されているかと思いますが、私はここを5にしました。

```config/initializers/kaminari_config.rb
  Kaminari.configure do |config|
    config.default_per_page = 5 # 1ページあたりの表示件数
  end
```


#### 1.3. コントローラーの記述を修正する
ページネーションを利用したいデータの取得を担っているコントローラーの記述を修正します。
私の場合は、施設の情報一覧を取得するspotsコントローラーのindexアクションに適用してみました。

```app/controllers/spots_controller.rb
class SpotsController < ApplicationController
  def index
-    @spots = Spot.all
+    @spots = Spot.all.page(params[:page])
  end
end
```
<details><summary>pageメソッド</summary>
ページネーションにおけるページ数を指定
</details>

なお、以下のようにperメソッドを用いることでページネーションの設定を上書きすることも可能です。

```ruby
# 1ページごとのデータ数を10に変更
@spots = Spot.all.page(params[:page]).per(10)
```
<details><summary>perメソッド</summary>
1ページあたりに表示する件数を指定
</details>


#### 1.4. ビューの設定を記述する
index.html.erb内のページネーションを表示させたい箇所に、paginateヘルパーを用いて下記のように記述します。

```app/views/spots/index.html.erb
<%= paginate @spots %>
```
これにより、下の画像のようなページネーションが作成されるかと思います(若干異なる可能性もあります)。

<a href="https://gyazo.com/8bd65190b5107504f14d12e156350bed"><img src="https://i.gyazo.com/8bd65190b5107504f14d12e156350bed.png" alt="Image from Gyazo" width="335"/></a>

ここまでで、最低限のページネーションの機能ができました。


### 2. ページネーションのカスタマイズ
#### 2.1. kaminariにおけるビューのカスタムテンプレートを導入
ページネーションのレイアウト調整をする際、kaminari用のビューのカスタムテンプレートを導入します。
ターミナルで下記のコマンドを実行します。

```terminal
rails g kaminari:views default
```

<details><summary>bootstrapを使っている場合</summary>
bootstrapには専用のカスタムテンプレートが用意されているようです(実行するコマンドが若干異なります)。
関係する資料をこちらに記載しておきますので、必要に応じてご確認ください。

https://qiita.com/rio_threehouse/items/313824b90a31268b0074
https://pikawaka.com/rails/kaminari

また、gemを新たに導入することで、カスタムテンプレートを使用せず簡単にレイアウト調整ができるということで、その場合の実装例が書かれた資料もこちらに記載しておきます。

https://zenn.dev/yukihaga/articles/3d49208638e397

</details>


#### 2.2. カスタムテンプレートの中身を見てみる
先ほどのコマンドを実行すると、```app/views/kaminari/```配下にカスタムテンプレートが7ファイル生成されます。
以下は、各ファイルの記述(デフォルト)と役割の説明です。必要なければ読み飛ばしてください。

<details open><summary>app/views/kaminari/_paginator.html.erb</summary>
他のテンプレートファイルを読み込むための骨格となるテンプレートファイルです。
説明のため、こちらで適宜コメントを入れています。

```erb
<%= paginator.render do -%>
  <nav class="pagination" role="navigation" aria-label="pager">
    <%# 現在のページが最初のページでない場合のみ「最初のページ」と「前のページ」へのリンクを表示 %>
    <%= first_page_tag unless current_page.first? %>
    <%= prev_page_tag unless current_page.first? %>
 
    <% each_page do |page| -%>
    　<%# 表示すべきページ番号の場合true %>
      <% if page.display_tag? -%>
        <%# page_tagヘルパーでそのページへのリンクを生成 %>
        <%= page_tag page %>
    　<%# 表示すべきページ番号ではない場合 → 複数ページが連続して省略されている場合（省略記号 “…” などを表示する必要がある場合）にtrue %>
      <% elsif !page.was_truncated? -%>
        <%# ページ番号の間に省略記号（”…”, “…“など）を表示するためのタグ %>
        <%= gap_tag %>
      <% end -%>
    <% end -%>

    <%# 現在のページが最後のページでない場合のみ「次のページ」と「最後のページ」へのリンクを表示 %>
    <% unless current_page.out_of_range? %>
      <%= next_page_tag unless current_page.last? %>
      <%= last_page_tag unless current_page.last? %>
    <% end %>
  </nav>
<% end -%>
```
</details>


<details open><summary>app/views/kaminari/_first_page.html.erb</summary>
「最初のページ」へのリンクを作るテンプレートです。

```erb
<span class="first">
  <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
</span>
```
</details>


<details open><summary>app/views/kaminari/_prev_page.html.erb</summary>
「前のページ」へのリンクを作るテンプレートです。

```erb
<span class="prev">
  <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: true %>
</span>
```
</details>


<details open><summary>app/views/kaminari/_page.html.erb</summary>
「1」「2」「3」などの各ページに対応するテンプレートです。

```erb
<span class="page<%= ' current' if page.current? %>">
  <%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
</span>
```
</details>


<details open><summary>app/views/kaminari/_gap.html.erb</summary>
ページネーションでページ番号が省略された箇所に「…」などの省略記号を表示するためのテンプレートです。

```erb
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>
```
</details>


<details open><summary>app/views/kaminari/_next_page.html.erb</summary>
「次のページ」へのリンクを作るテンプレートです。

```erb
<span class="next">
  <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: true %>
</span>
```
</details>


<details open><summary>app/views/kaminari/_last_page.html.erb</summary>
「最後のページ」へのリンクを作るテンプレートです。

```erb
<span class="last">
  <%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
</span>
```
</details>


#### 2.3. Tailwind CSS適用と必要なテンプレートの選定
ここはお好みの設定になります。
Tailwind CSSのサイトのリンクは下記になります。

https://tailwindcss.com/

ページネーション全体にレイアウトを適用する場合、```app/views/kaminari/_paginator.html.erb```でレイアウト調整をします。

<details open><summary>app/views/kaminari/_paginator.html.erb</summary>

```diff_erb
+ <div class="text-xs mt-8 mx-auto flex justify-center">
  <%= paginator.render do %>
    <nav class="pagination" role="navigation" aria-label="pager">
      <%# 現在のページが最初のページでない場合のみ「最初のページ」と「前のページ」へのリンクを表示 %>
-     <%= first_page_tag unless current_page.first? %>
      <%= prev_page_tag unless current_page.first? %>

      <% each_page do |page| %>
        <%# 表示すべきページ番号の場合true %>
        <% if page.display_tag? %>
          <%# page_tagメソッドでそのページへのリンクを生成 %>
          <%= page_tag page %>
-       <%# 表示すべきページ番号ではない場合 → 複数ページが連続して省略されている場合（省略記号 “…” などを表示する必要がある場合）にtrue %>
-       <% elsif !page.was_truncated? %>
-         <%# ページ番号の間に省略記号（”…”, “…“など）を表示するためのタグ %>
-         <%= gap_tag %>
        <% end %>
      <% end %>

      <%# 現在のページが最後のページでない場合のみ「次のページ」と「最後のページ」へのリンクを表示 %>
      <% unless current_page.out_of_range? %>
        <%= next_page_tag unless current_page.last? %>
-       <%= last_page_tag unless current_page.last? %>
      <% end %>
    </nav>
  <% end %>
+ </div>
```
</details>

今回私の実装では、ページネーション全体にTailwind CSSのclassを割り当てたかった(ページネーション全体の配置を調整するなど)ため、そのためのdivタグを追加しています。
また、私の場合、「最初のページ」「ページの省略を表す部分」「最後のページ」が不要だったため、読み込む部分の記述を削除しています。


#### 2.4. daisyUIのコンポーネントを利用する

daisyUIを利用してレイアウトを簡単に調整します。
daisyUIにおけるページネーションのコンポーネントはこちらから確認できます。

https://daisyui.com/components/pagination/

今回、こちらのコンポーネントを参考に実装しました。
<a href="https://gyazo.com/09f4e839f99a7f1b476cf5009c87406d"><img src="https://i.gyazo.com/09f4e839f99a7f1b476cf5009c87406d.png" alt="Image from Gyazo" width="837"/></a>
<a href="https://gyazo.com/0fa26ff11c38d86ce675205e55dd3d24"><img src="https://i.gyazo.com/0fa26ff11c38d86ce675205e55dd3d24.png" alt="Image from Gyazo" width="844"/></a>


<details open><summary>app/views/kaminari/_paginator.html.erb</summary>

```diff_erb
- <div class="text-xs mt-8 mx-auto flex justify-center">
+ <div class="join text-xs mt-8 mx-auto flex justify-center">
  <%= paginator.render do %>
    <nav class="pagination" role="navigation" aria-label="pager">
      <%# 現在のページが最初のページでない場合のみ「最初のページ」と「前のページ」へのリンクを表示 %>
      <%= prev_page_tag unless current_page.first? %>

      <% each_page do |page| %>
        <%# 表示すべきページ番号の場合true %>
        <% if page.display_tag? %>
          <%# page_tagメソッドでそのページへのリンクを生成 %>
          <%= page_tag page %>
        <% end %>
      <% end %>

      <%# 現在のページが最後のページでない場合のみ「次のページ」と「最後のページ」へのリンクを表示 %>
      <% unless current_page.out_of_range? %>
        <%= next_page_tag unless current_page.last? %>
      <% end %>
    </nav>
  <% end %>
</div>
```
</details>


<details open><summary>app/views/kaminari/_prev_page.html.erb</summary>
class属性を追加します。

```diff_erb
<span class="prev">
-  <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: true %>
+  <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, class: "join-item btn", rel: 'prev', remote: true %>  
</span>
```
</details>



<details open><summary>app/views/kaminari/_page.html.erb</summary>
現在見ているページであれば、"btn-active"となるようにclass属性を追加します。

```diff_erb
- <span class="page<%= ' current' if page.current? %>">
-  <%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
-</span>
+ <%= link_to page, url, class: "join-item btn #{'btn-active' if page.current?}", remote: true, rel: page.rel %>
```
</details>


<details open><summary>app/views/kaminari/_next_page.html.erb</summary>
class属性を追加します。

```diff_erb
<span class="next">
-  <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: true %>
+  <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, class: "join-item btn", rel: 'next', remote: true %>
</span>
```
</details>

今までの実装で、おおよそこのような状態になっているかと思います。
<a href="https://gyazo.com/9ab14b32f6d8f691eb0df421120001ce"><img src="https://i.gyazo.com/9ab14b32f6d8f691eb0df421120001ce.png" alt="Image from Gyazo" width="487"/></a>


<details><summary>サイズ調整もできます！</summary>
前述のdaisyUIのページネーションのサイトに、サイズ調整用のコンポーネントも用意されています。
お好みでサイズ調整してみてください。

<a href="https://gyazo.com/618a81e582087710f6f9783170691877"><img src="https://i.gyazo.com/618a81e582087710f6f9783170691877.png" alt="Image from Gyazo" width="841"/></a>

<a href="https://gyazo.com/c31087b1a6552101559d2f63aff33216"><img src="https://i.gyazo.com/c31087b1a6552101559d2f63aff33216.png" alt="Image from Gyazo" width="841"/></a>

</details>


#### 2.5. ページ数の表示制限
2.4までの実装では、ページネーションの```app/views/kaminari/_page.html.erb```の表示数を指定していないため、ページ数が多いとページネーションが横に長くなってしまいます。
これは、```config/initializers/kaminari_config.rb```で下記のような記述を加えることで設定ができます。

```config/initializers/kaminari_config.rb
Kaminari.configure do |config|
  config.default_per_page = 5 # 1ページあたりの表示件数
+ config.window = 2 # 現在のページの前後に表示するページ数
end
```

私の場合は、現在のページ前後に2ページ分表示させたかったため、上記の設定にしました。
ここまでで、レイアウトは下のようになっているかと思います(サイズ調整もしています)。

<a href="https://gyazo.com/537faf8d75e13203b68eed23ce11b1a3"><img src="https://i.gyazo.com/537faf8d75e13203b68eed23ce11b1a3.png" alt="Image from Gyazo" width="386"/></a>


#### 2.6. 表記をカスタマイズする
現時点では、「Previous」や「Next」といった表記でわかるように、デフォルトでは日本語化されていません。
ここを、日本語で表記されるよう変更してみます。
まず、```config/application.rb```にてデフォルトの言語設定を日本語に設定します。

```config/application.rb
module Myapp
  class Application < Rails::Application
    # Railsアプリケーションのデフォルトの言語設定を日本語に設定
+   config.i18n.default_locale = :ja
  end
end
```

次に、```config/locales/ja.yml```を作成・編集し、「Previous」や「Next」に対応する日本語を設定します。

```config/locales/ja.yml
ja:
  views:
    pagination:
      previous: "前へ"
      next: "次へ"
```

設定ができると下の画像のようになるかと思います。
この後、更にTailwind CSSを利用するなどして、自分好みのレイアウトに調整していただければと思います。

<a href="https://gyazo.com/bf872805932c8985e41fd1a778a4d2cb"><img src="https://i.gyazo.com/bf872805932c8985e41fd1a778a4d2cb.png" alt="Image from Gyazo" width="364"/></a>

これで終わりになります！お疲れ様でした。


# おわりに
以前、bootstrapでページネーションのレイアウト調整をしたことがあったのですが、その時よりも考えることが多く、実装に思ったよりも時間がかかってしまいました。
この記事で同じような実装をお考えの方のお役に立てれば幸いです。


# 参考
## gem 'kaminari'
https://github.com/kaminari/kaminari

## 技術記事
https://qiita.com/rio_threehouse/items/313824b90a31268b0074

https://pikawaka.com/rails/kaminari

https://www.airteams.net/media/articles/1985

https://zenn.dev/yukihaga/articles/3d49208638e397

https://qiita.com/rintaronakamura/items/1ae1e5ceb59c0729c0b9