Oasistブログ

言語学、エンジニアリング、ライフ記事を気まぐれにお届け

Kaminariを使ったページネーションでパラメーターを保持する

f:id:oasist:20200614004721p:plain
Ruby on Rails

目次

1. 環境

2. 要件

  • Ransack検索フォーム導入等の機能拡張に備えたページネーションデータの保持
  • 全体件数と表示件数の表示
  • デフォルトの表示件数は20件
  • 20件、50件、100件、200件、500件の中で表示件数切り替え

3. Gemfile

rails-i18nkaminariGemfileに追加し、docker-compose exec app bin/rails bundle を実行する。

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.7.2'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.2'
...

# Locale
gem 'rails-i18n', '~> 6.0.0'

# Pagination
gem 'kaminari', '~> 1.2.1'

4. Config

4-1. Settings

表示件数の切り替えのための number_per_page/config/settings.ymlに定義する。

common: &common
  number_per_page: [20, 50, 100, 200, 500]

development:
  <<: *common

test:
  <<: *common

staging:
  <<: *common

production:
  <<: *common

4-2. Initializers

最初に、number_per_page を呼び出すための AppConfig 定数を/config/initializers/app_constants.rbに定義する。

AppConfig = YAML.load_file("#{Rails.root}/config/settings.yml")[Rails.env].symbolize_keys

次に、デフォルト表示件数の20件を/config/initializers/kaminari_config.rbに定義する。

Kaminari.configure do |config|
  config.default_per_page = 20
end

4-3. Locales

表示を日本語化するための翻訳を/config/locales/pagination.ja.ymlに定義する。

ja:
  views:
    pagination:
      first: "最初へ"
      last: "最後へ"
      previous: "前へ"
      next: "次へ"
      truncate: "&hellip;"
  helpers:
    page_entries_info:
      one_page:
        zero: "該当データがありません。"
        one: "1-1/全1件"
        display_entries: '1-%{count}/全%{count}件'
      more_pages:
        display_entries: '%{first}-%{last}/全%{total}件'

5. コントローラー

5-1. ApplicationController

Define some private methods in /app/controllers/application_controller.rb.

class ApplicationController < ActionController::Base

...

  private

  # 指定された表示件数を返す
  def paginate_per
    session[:paginate_per] = params[:per] if params[:per].present?
    session[:paginate_per]
  end

  # クエリパラメーター、コントローラー名、アクション名をセッションの `last_pagination_data` に保存する。
  def keep_last_pagination_data
    session[:last_pagination_data] = {
      params: request.query_parameters,
      controller: controller_name,
      action: action_name
    }
  end

  # `session[:last_pagination_data]` を破棄して `session[:last_pagination_data]["params"]` を以下の条件が揃った場合に返す
  # `session[:last_pagination_data]` が `nil` でない場合
  # `session[:last_pagination_data]["controller"]` が `controller_name` と一致する場合
  # `session[:last_pagination_data]["action"]` が `action.to_s` と一致する場合
  def load_pagination_params(action)
    data = session[:last_pagination_data].presence
    if data["controller"] == controller_name && data["action"] == action.to_s
      session[:last_pagination_data] = nil
      ret = data["params"]
    end
  end

 # `action: action_name` and `params: session[:last_pagination_data]["params"]` のキー値で一覧画面委リダイレクトする。
  def redirect_with_kept_pagination_params(action:, **args)
    redirect_to({ action: action, params: load_pagination_params(action) }, args)
  end
end

5-2. コントローラーに適用

ページネーションを適用したいコントローラーで、paginate_per プライベートメソッドをKaminariが提供する per メソッドに渡す。

def index
  @events = Event.page(params[:page]).per(paginate_per).where('start_at >= ?', Time.now).order(:start_at)
end

6. ビュー

6-1. ページネーションヘッダー

共通のページネーションヘッダーを /app/views/shared/_pagination_header.html.erb に実装する。

<% if defined?(objects) %>
<div class="pagination-field">
  <div class="pagination-wrapper">
    <div class="entry-content">
      <%= page_entries_info(objects, entry_name: objects&.model_name.to_s) %>
      <p class="display-items">
        表示件数:
        <% AppConfig[:number_per_page].each do |per| %>
          <%= link_to_unless per == objects.limit_value, per, params.permit!.merge(per: per) %>
        <% end %>
         &nbsp;
      </p>
    </div>
    <div class="pagination-content float-right">
      <%= paginate(objects) %>
    </div>
  </div>
</div>
<% end %>

6-2. ページネーションビュー

docker-compose exec app bin/rails g kaminari:views bootstrap4 を実行し、必要なページネーションビューを生成する。

<li class="page-item">
  <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' %>
</li>
<li class="page-item disabled">
  <%= link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' %>
</li>
<li class="page-item">
  <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, remote: remote, class: 'page-link' %>
</li>
<li class="page-item">
  <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote, class: 'page-link' %>
</li>
<% if page.current? %>
  <li class="page-item active">
    <%= content_tag :a, page, data: { remote: remote }, rel: page.rel, class: 'page-link' %>
  </li>
<% else %>
  <li class="page-item">
    <%= link_to page, url, remote: remote, rel: page.rel, class: 'page-link' %>
  </li>
<% end %>
<%= paginator.render do %>
  <nav>
    <ul class="pagination">
      <%= first_page_tag unless current_page.first? %>
      <%= prev_page_tag unless current_page.first? %>
      <% each_page do |page| %>
        <% if page.left_outer? || page.right_outer? || page.inside_window? %>
          <%= page_tag page %>
        <% elsif !page.was_truncated? %>
          <%= gap_tag %>
        <% end %>
      <% end %>
      <%= next_page_tag unless current_page.last? %>
      <%= last_page_tag unless current_page.last? %>
    </ul>
  </nav>
<% end %>
<li class="page-item">
  <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote, class: 'page-link' %>
</li>

6-3. ビューに適用

shared/pagination_headerを読み込みページネーションヘッダーを、paginateメソッドを呼び出しページネーションフッターをそれぞれ描画する。

<h1>イベント一覧</h1>
<%= render 'shared/pagination_header', objects: @events %>
<div class="list-group">
  <% @events.each do |event| %>
    <%= link_to(event, class: 'list-group-item list-group-item-action') do %>
      <h5 class="list-group-item-heading"><%= event.name %></h5>
      <p class="mb-1"><%= "#{l(event.start_at, format: :long)} - #{l(event.end_at, format: :long)}" %></p>
    <% end %>
  <% end %>
</div>

<div class="pagination-content float-right mt-4">
  <%= paginate(@events) %>
</div>

7. アセット

ページネーションのヘッダーとフッターに適用するスタイルを /app/assets/stylesheets/pagination.css.scss に定義する。

.pagination-field {
  display: table;
  width: 100%;
  margin-top: 10px;
  .pagination-wrapper {
    display: table-row;
    .entry-content {
      display: table-cell;
      width: 220px;
      vertical-align: middle;
    }
    .pagination-content {
      display: table-cell;
      vertical-align: middle;
    }
  }
}

8. 成果物

f:id:oasist:20201123123331p:plain
ページネーションヘッダー


f:id:oasist:20201123123401p:plain
ページネーションフッター