rails: kaminariをつかってちょっとだけ付加機能をつけたページネーションの実装

最近kaminariの実装を行ったので学んだことや得たkaminariの理解についてメモがてら書いてみようと思う。

#やりたいこと、目的

![Screen Shot 2016-11-16 at 12.06.35 AM.png](https://qiita-image-store.s3.amazonaws.com/0/87666/25fa0621-e99c-6df1-ec98-c21362028d69.png)


kaminariでできる通常の機能に加えて
1. 自分の今いるページ（以下`current_page`として省略）とページの総数（以下`total_pages`として省略）に加えて二つの誘導リンクを設置したい。例えば`current_page < 10`のときに表示するページは20と40で10を超えると30と50。

#手順

##kaminariのsetupについて
kaminariの基本的な実装はネット上に山ほど落ちているので僕が説明するまでもないと思います。いかに参考リンクを貼っておきます。~~言うまでもないですが~~解説もわかりやすいのですが、わからないことがあったら随時ググっていったほうが構造理解には繋がると思います。

- <a href="http://qiita.com/nysalor/items/77b9d6bc5baa41ea01f3" target="_blank">kaminari徹底入門</a>
- <a href="https://github.com/amatsuda/kaminari" target="_blank">amatsuda/kaminari</a>
- <a href="http://ruby-rails.hatenadiary.com/entry/20141113/1415874683" target="_blank">Railsでkaminariを使ってページネーションを実現する</a>
- <a href="http://morizyun.github.io/blog/kaminari-gem-paginator-rails/" target="_blank">kaminariをBootstrap3、Rails4.2環境で使う！[Ruby 2.3]</a>

#kaminariの基本的な設定

```kaminari_config.rb
#config/initializers/kaminari_config.rb

Kaminari.configure do |config|
   config.default_per_page = 100
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
end
```
他にも方法はあるがここでpaginationの細かい設定を行ってる。ページリンク数を変えたり、リンクの位置を変えることができる。僕は「イヤマテヨ。ソモソモ`outer_window`ッテナニ？？」ってなったので下記のリンクを参考にしながら作業してました。

- <a href="http://www.rubydoc.info/github/amatsuda/kaminari/Kaminari/PageScopeMethods" target="_blank">Module: Kaminari::PageScopeMethods</a>
- <a href="https://richonrails.com/articles/pagination-with-kaminari.prettyprint linenums" target="_blank">Pagination With Kaminari</a>

#kaminariの挙動を探ってみる

例えば
![kaminari_finish copy.png](https://qiita-image-store.s3.amazonaws.com/0/87666/38d094aa-544b-d01d-4b2b-27748a2a9562.png)

のリンクをイジってみたいとする。

![Screen Shot 2016-11-15 at 11.13.36 PM.png](https://qiita-image-store.s3.amazonaws.com/0/87666/e0b4f923-3c4f-4e81-d6a7-7ca4ba7b0c64.png)

`prev kaminari-page-tip`というクラスの階層の下にあるので

```_paginator.html.slim

# app/views/kaminari/_paginator.html.slim

/ The container tag
  - available local variables
    current_page : a page object for the currently displayed page
    total_pages  : total number of pages
    per_page     : number of items to fetch per page
    remote       : data-remote
    paginator    : the paginator that renders the pagination tags inside
ruby:
  one_digit = current_page.to_i % 10
  page_20 = current_page + 20 - one_digit
  page_40 = current_page + 40 - one_digit
  if params[:available].present? || params[:price].present? || params[:usage_state].present?
    url_20 = "#{request.path_info}?available=#{params[:available] if params[:available].present?}&page=#{page_20}&price=#{params[:price] if params[:price].present?}&usage_state=#{params[:usage_state] if params[:usage_state].present?}"
  else
    url_20 = "/items-page-#{page_20}"
  end
  
== paginator.render do
  .pagination
    .pagination-middle
      .pagination-prev
        == prev_page_tag unless current_page.first?
      / == first_page_tag unless current_page.first? || [2,3,4].include?(current_page)
      - each_page do |page|
        - if page.left_outer? || page.right_outer? || page.inside_window?
          == page_tag page
        - elsif !page.was_truncated?
          - unless (current_page >= 5 && page < current_page) || (total_pages - 4 == current_page && page == total_pages)
            - if total_pages >= 20 && total_pages > current_page.number + 20
              == gap_tag
              span.kaminari-page-tip class="page#{' current' if page.current?}"
                == link_to_unless current_page == page_20, page_20, url_20, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}
              == gap_tag
            - else
              == gap_tag
      - if total_pages >= page_40 || total_pages < 20
        == last_page_tag unless current_page == page_40 || current_page == page_20 || [1,2,3].map{|i| total_pages - i}.include?(current_page)
      .pagination-next
        == next_page_tag unless current_page.last?
```

`== paginator.render do`の中の`.paginator.html.slim`クラスの下に何やら沢山書かれている。`prev_page_tag`というのは以下のファイルのこと。

```_prev_page.html.slim
# app/views/kaminari/_prev_page.html.slim

span.prev.kaminari-page-tip
  == link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url.gsub("categories/","").gsub("%2F","\/"), :rel => 'prev', :remote => remote
'
```

つまり`current_page.first?`で`false`判定が出た場合は`prev_page`をレンダリングするように書かれている。多分だが`prev`が`current_page`より先に降りてくるようなデータ構造になっている。

その他`app/views/kaminari`の`_last_page.html.slim`や`_next_page.html.slim`、`_gap.html.slim`、`_page.html.slim`は全ては中枢である`_paginator.html.slim`のファイルによって呼び出し管理を行っている。

##each_pageの存在

[ここ](http://www.rubydoc.info/github/amatsuda/kaminari/Kaminari/Helpers/Paginator)では`each_page`とは

```paginator.rb
# File 'lib/kaminari/helpers/paginator.rb', line 49

def each_relevant_page
#enumerate each page providing PageProxy object as the block parameter Because of performance reason, this doesn't actually enumerate all pages but pages that are seemingly relevant to the paginator.
  return to_enum(:each_relevant_page) unless block_given?

  relevant_pages(@window_options).each do |page|
    yield PageProxy.new(@window_options, page, @last)
  end
end
```

>
- pages inside the left outer window plus one for showing the gap tag
- pages inside the inner window plus one on the left plus one on the right for showing the gap tags
- pages inside the right outer window plus one for showing the gap tag

と書かれている。これはつまり

![kaminari_each_page.png](https://qiita-image-store.s3.amazonaws.com/0/87666/6d52c597-45ed-7aca-ee9e-36f9bf188cb1.png)

こういうことではないのかと仮説を立てております。（本当かどうかわからないけど汗）

```_paginator.html.slim
- each_page do |page|
        - if page.left_outer? || page.right_outer? || page.inside_window?
          == page_tag page
        - elsif !page.was_truncated?
          - unless (current_page >= 5 && page < current_page) || (total_pages - 4 == current_page && page == total_pages)
            - if total_pages >= 20 && total_pages > current_page.number + 20
              == gap_tag
              span.kaminari-page-tip class="page#{' current' if page.current?}"
                == link_to_unless current_page == page_20, page_20, url_20, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}
              == gap_tag
            - else
              == gap_tag
```

で単体の`page`に分けて一つ一つの（言うなれば）所属すべき場所に振り分けるための精査作業を行っているのではないかと。例えば`link_to_unless current_page == page_20`で条件にあえば上の方で定義した`page_20`へのレンダリングを行っているなどなど。

試しに

![Screen Shot 2016-11-16 at 12.18.32 AM.png](https://qiita-image-store.s3.amazonaws.com/0/87666/ef89a7e6-3051-3f41-6d6d-5ab0f5e919eb.png)

以下のようにテストしてみたところ

![Screen Shot 2016-11-16 at 12.17.31 AM.png](https://qiita-image-store.s3.amazonaws.com/0/87666/18c4e7af-acbb-9e54-0d5a-7e12c359fb44.png)

と出たのでどうやらeach_pageの中にcurrent_pageがあるのは事実みたいです。

###蛇足
これはあくまで予想だがここに書いてある`views.pagination.previous`は以下のファイルとつながっている。

`&laquo`と`&raquo`については[&laquo と &raquo ってなんだろうの巻](http://wordpressoboegaki.seesaa.net/article/371696180.html)を参考に。

```kaminari.ja.yml
# config/locales/kaminari.ja.yml

ja:
  views:
    pagination:
      first: "&laquo;"
      last: "&raquo;"
      previous: "前へ"
      next: "次へ"
      truncate: "..."
```

##なぜ_paginator.html.slimがこんなに長いのか

ファイルの最初の方で

```_paginator.html.slim
ruby:
  one_digit = current_page.to_i % 10
  page_20 = current_page + 20 - one_digit
  page_40 = current_page + 40 - one_digit
  if params[:available].present? || params[:price].present? || params[:usage_state].present?
    url_20 = "#{request.path_info}?available=#{params[:available] if params[:available].present?}&page=#{page_20}&price=#{params[:price] if params[:price].present?}&usage_state=#{params[:usage_state] if params[:usage_state].present?}"
  else
    url_20 = "/items-page-#{page_20}"
  end
```

`page_20`と`page_40`を定義することで画像で言う

![Screen Shot 2016-11-16 at 12.06.35 AM.png](https://qiita-image-store.s3.amazonaws.com/0/87666/75e02b23-22bd-6742-4991-64d167b499ab.png)

を作るために予め変数にリンク先の情報を入れているわけです。あとは上で書いたファイルの中のレンダリングで呼んであげるだけ。