Oasist Blog

This blog features Linguistics, Engineering&Programming and Life Career.

Kaminari Pagination with Parameters Sustained

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

Contents

1. Environment

2. Requirements

  • Sustain pagination data for such extensions as Ransack search form introduction
  • Show the number of items of all
  • Default number of items is 20
  • Switch the number of items among 20, 50, 100, 200 and 500

3. Gemfile

Add rails-i18n and kaminari in Gemfile and run 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

Define number_per_page in /config/settings.yml to switch the number of items to show.

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

development:
  <<: *common

test:
  <<: *common

staging:
  <<: *common

production:
  <<: *common

4-2. Initializers

First, efine AppConfig constant in /config/initializers/app_constants.rb to call number_per_page.

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

Next, define the default number of items to show as 20 in /config/initializers/kaminari_config.rb.

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

4-3. Locales

Define translation of English words of kaminari in /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. Controllers

5-1. ApplicationController

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

class ApplicationController < ActionController::Base

...

  private

  # Return the number of items to show if it is assigned
  def paginate_per
    session[:paginate_per] = params[:per] if params[:per].present?
    session[:paginate_per]
  end

  # Sustain query parameters controller name and action name in the key `last_pagination_data` of session
  def keep_last_pagination_data
    session[:last_pagination_data] = {
      params: request.query_parameters,
      controller: controller_name,
      action: action_name
    }
  end

  # Abandon `session[:last_pagination_data]` and return `session[:last_pagination_data]["params"]`
  # if `session[:last_pagination_data]` is NOT `nil`,
  #    `session[:last_pagination_data]["controller"]` is `controller_name` and
  #    `session[:last_pagination_data]["action"]` is `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

 # Redirect to the index page with `action: action_name` and `params: session[:last_pagination_data]["params"]` key & values
  def redirect_with_kept_pagination_params(action:, **args)
    redirect_to({ action: action, params: load_pagination_params(action) }, args)
  end
end

5-2. Apply Pagination to Controllers

Apply paginate_per private methods to the argument of per method of kaminari in the controllers you would like to apply pagination to.

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

6. Views

6-1. Pagination Header

Implement a shared pagination header in /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. Pagination Views

Run docker-compose exec app bin/rails g kaminari:views bootstrap4, and required pagination views will be created.

<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. Apply Pagination to Views

Render shared/pagination_header to show the pagination header and call paginate method to show the pagination footer.

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

Define styles for the pagination header and footer in /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. Deliverables

f:id:oasist:20201123123331p:plain
Pagination Header


f:id:oasist:20201123123401p:plain
Pagination Footer