Ruby Gem Pagy + Infinite-Scroll PluginでAPIレスポンスデータを無限スクロールする

最近、Ruby on Railsで、APIの結果を無限スクロールで表示する画面を作成する機会があり、ページネーション用のGemを探していたところ「Pagy」を知りました。ページネーションのGemで有名なものは、「Kaminari」や「Will Pagenate」ですが、これらのGemに比べて「Pagy」はパフォーマンス面に重点をおいて開発されているとのことで、興味を持ち使ってみました。

# 目的
Pagyの公式ドキュメントはとても充実していますが、日本語の情報がまだ少なかったので、今回は「Pagy」の使用方法について記載していこうと思います。なお、パフォーマスについては、ベンチマークテストの結果がGithub上に掲載されていますので、興味のある方はそちらをご覧下さい。

- https://github.com/ddnexus/pagy

# やること
1. Pagyを用いたページネーション
2. Infinite-Scroll Pluginを用いた無限スクロール

# データ
今回は、Modelから取得したデータではなく、APIレスポンスのデータをページネーションさせます。APIから受け取ったJSONデータをHashに変換後、配列形式でPagyへ渡すイメージです。なお、今回はPagyの利用が目的ですので、実際にAPIを用意することはせず、Controller内に配列データを直接定義します。

# 環境
本記事では、以下の環境を使用します。

- Ruby  2.4.1
- Rails 5.1.6
- Pagy  2.1.3
- Slim  4.0.1

# Pagyのインストール
まずは、Gemfileに「pagy」を追加します。

```:Gemfile
gem 'pagy'
```

Gemを追加したら、インストールします。

```:bash
bundle install --path vendor/bundle
```

# Controllerの設定
ControllerにPagyのBackendモジュールをインクルードします。

```.rb:app/controllers/contents_controller.rb
class ContentsController < ApplicationController
  include Pagy::Backend
end
```

# Helperの設定
HelperにPagyのFrontendモジュールをインクルードします。

```.rb:app/helpers/contents_helper.rb
module ContentsHelper
  include Pagy::Frontend
end
```

# Pagyのイニシャライザファイル作成
イニシャライザファイルを作成することで、Pagyの設定をデフォルトから変更することができます。例えば、1ページに表示するデータ件数を指定したり、国際化対応の設定ができたりします。
今回は、配列に対してページネーションを行うために必要なライブラリの読み込みだけを行います。
その他の設定可能な項目については知りたい方は、Pagyのgithub上に公開されているので、そちらをご覧下さい。

- https://github.com/ddnexus/pagy/blob/master/lib/config/pagy.rb

```.rb:config/initializers/pagy.rb
require 'pagy/extras/array'
```

# ページング処理の実装
配列をページング処理するには、pagy_arrayというメソッドに、ページング処理したい配列を渡し、その返り値をViewで使用します。なお、オプション引数としてハッシュを渡すことで、様々な指定をすることができます。下記サンプルの「items: 3」は1ページあたりの表示データ数を3つにすることを意味しています。オプション引数で指定できる内容は、Pagyのイニシャライザファイルで定義することも可能です。
上記でも説明したように、ページネーションさせるデータはdefine_dataメソッドとして、直接定義してます。

```.rb:app/controllers/contents_controller.rb
class ContentsController < ApplicationController
  include Pagy::Backend

  def search
    @api_results = define_data
    @pagy, @api_results = pagy_array(@api_results, items: 3)
  end

  def define_data
    results = [
      { id:  1, content: 'test data 01', date: '2019/04/01 10:01:00' },
      { id:  2, content: 'test data 02', date: '2019/04/01 10:02:00' },
      { id:  3, content: 'test data 03', date: '2019/04/01 10:03:00' },
      { id:  4, content: 'test data 04', date: '2019/04/01 10:04:00' },
      { id:  5, content: 'test data 05', date: '2019/04/01 10:05:00' },
      { id:  6, content: 'test data 06', date: '2019/04/01 10:06:00' },
      { id:  7, content: 'test data 07', date: '2019/04/01 10:07:00' },
      { id:  8, content: 'test data 08', date: '2019/04/01 10:08:00' },
      { id:  9, content: 'test data 09', date: '2019/04/01 10:09:00' },
      { id: 10, content: 'test data 10', date: '2019/04/01 10:10:00' },
      { id: 11, content: 'test data 11', date: '2019/04/01 10:11:00' },
      { id: 12, content: 'test data 12', date: '2019/04/01 10:12:00' },
      { id: 13, content: 'test data 13', date: '2019/04/01 10:13:00' },
      { id: 14, content: 'test data 14', date: '2019/04/01 10:14:00' },
      { id: 15, content: 'test data 15', date: '2019/04/01 10:15:00' },
      { id: 16, content: 'test data 16', date: '2019/04/01 10:16:00' },
      { id: 17, content: 'test data 17', date: '2019/04/01 10:17:00' },
      { id: 18, content: 'test data 18', date: '2019/04/01 10:18:00' },
      { id: 19, content: 'test data 19', date: '2019/04/01 10:19:00' },
      { id: 20, content: 'test data 20', date: '2019/04/01 10:20:00' },
      { id: 21, content: 'test data 21', date: '2019/04/01 10:21:00' },
      { id: 22, content: 'test data 22', date: '2019/04/01 10:22:00' },
      { id: 23, content: 'test data 23', date: '2019/04/01 10:23:00' },
      { id: 24, content: 'test data 24', date: '2019/04/01 10:24:00' },
      { id: 25, content: 'test data 25', date: '2019/04/01 10:25:00' },
      { id: 26, content: 'test data 26', date: '2019/04/01 10:26:00' },
      { id: 27, content: 'test data 27', date: '2019/04/01 10:27:00' },
      { id: 28, content: 'test data 28', date: '2019/04/01 10:28:00' },
      { id: 29, content: 'test data 29', date: '2019/04/01 10:29:00' },
      { id: 30, content: 'test data 30', date: '2019/04/01 10:30:00' }
    ]
  end
end
```

これでページングさせるデータの準備はできたので、次にビュー側の処理を書いていきます。なお、divタグの「contents」というIDと、「content」というclass名は任意ですが、無限スクロール実装時にJavaScript側で使用するので、変更する場合はご注意下さい。

```.slim:app/views/contents/search.html.slim
#contents
  .content
    - @api_results.map do |result|
      div
        = "内容：#{result[:content]}  |  作成日時：#{result[:date]} "
== pagy_nav @pagy
```

# ページネーション 動作確認
ここまで実装すると、下記のようにページングが行えるようになっているはずです。
「2」や「3」、「Prev」、「Next」が機能することを確認しましょう。
<img width="427" alt="step1.png" src="https://qiita-image-store.s3.amazonaws.com/0/174425/dc04d8fd-0a0f-e3aa-6586-858e151529cf.png">


# 無限スクロールの導入
作成したページングを無限スクロールへ変更していきます。
まずは、下記よりJavaScriptのプラグインを取得し、「app/assets/javascripts/」直下に配置します。

- https://unpkg.com/infinite-scroll@3.0.5/dist/infinite-scroll.pkgd.min.js

次に、無限スクロールを行うViewに対応するJavaScriptを作成します。

```.js:app/assets/javascripts/contents.js
$(document).on('turbolinks:load', function () {
  $('#contents').infiniteScroll({
    path: "nav.pagination a[rel=next]",
    append: ".content",
    hideNav: '.pagination',
    history: false,
    prefill: true,
    status: '.page-load-status'
  })
});
```
指定しているオプションの意味は、下記の通りです。

- path: "nav.pagination a[rel=next]"・・・次のページへのリンクを指定
- append: ".content"・・・追加する要素を指定
- hideNav: '.pagination'・・・ページングのナビゲーションを非表示
- history: false・・・ページの変更ごとにブラウザの履歴とURLを変更しない
- prefill: true・・・初期化時にロードして、要素を追加
- status: '.page-load-status'・・・ページ読み込みの状態を示すステータス要素を表示

最後に、JQueryを読み込むように設定します。

```.js:app/assets/javascripts/application.js
//= require jquery
```

# 無限スクロール 動作確認
再度、画面を確認すると、無限スクロールが実現できているはずです。
うまく動かない場合は、Slimのインデント、divタグのIDやclass名を確認してみて下さい。
<img width="393" alt="step2.png" src="https://qiita-image-store.s3.amazonaws.com/0/174425/7adf8073-88f0-43d9-47e1-8a44b1f31ce0.png">

# ソースコード
実際に手元でコードを動かしたい方は、下記よりcloneしてご使用下さい。

- https://github.com/zetton-31/sample_with_pagy_and_inifite-scroll

# 参考
- https://ddnexus.github.io/pagy/index
- https://github.com/metafizzy/infinite-scroll
- https://techracho.bpsinc.jp/hachi8833/2018_06_05/57481