Rails 無限スクロール（gem、ライブラリ無し）

####はじめに
「Rails 無限スクロール」と検索するとたくさんの記事が出てきます。kaminariやjsライブラリを利用して作成している記事をたくさん見かけますが、今回は自分の学習のためにもgemやライブラリなしで実装をしていきたいと思います。（デフォルトのgemであるjbuilderと記法を変更するだけのjqueryは利用します。）

####完成イメージ
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1585805/1cd6c8e5-b3df-16c2-f948-2ab2bace8d42.gif)


####前提条件（無限スクロール実装前）
簡単な例を作成して記事を書いていきます。下記の条件でbooksテーブルに100件レコードを作成しました。
<img width='200' alt='erd' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1585805/bb6b8c50-d100-0cd7-9f2c-080f01dce3fd.png'>

```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index
    @books = Book.all
    @book = Book.new
  end

  def create
    @book = Book.new(book_params)
    @book.user = current_user
    @book.save
    @book_new = Book.new
  end

  private
  def book_params
    params.require(:book).permit(:title, :body)
  end
end
```

```erb:app/views/books/index.html.erb
<h4>新規投稿</h4>
<div class='scroll__new'>
  <%= render 'new', book: @book %>
</div>

<h4>投稿一覧</h4>
<table class='scroll__table'>
  <thead>
    <tr>
      <th class='scroll__th'>タイトル</th>
      <th class='scroll__th'>内容</th>
    </tr>
  </thead>
  <tbody class='scroll__tbody'>
    <% @books.each do |book| %>
      <tr>
        <td class='scroll__td'><%= book.title %></td>
        <td class='scroll__td'><%= book.body %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

```scss:app/assets/stylesheets/scroll.scss
.scroll {
  &__table {
    border-collapse: collapse;
  }

  &__tbody {
    height: 500px;
    display: block;
    overflow-y: scroll;
  }

  &__th,
  &__td {
    height: 40px;
    width: 100px;
    display: inline-block;
    text-align: center;
    line-height: 40px;
    border: 1px solid;
  }
}
```
scssは最低限記述しています。下記2点は機能を満たすために最低限必要です。
1. tbodyのstyle
2. tdのheight
tdのheightについては、scroll出来ればいいので次の条件を満たせば問題ありません。
tdのheight * 初期表示レコード数 > tbodyのheight

####無限スクロール機能の作成
では、現在全てのレコードが表示されているbooks一覧を15件ずつ取得し表示するように変更していきたいと思います。
#####処理の流れ
1. #indexにて最新15件を取得し表示
1. scroll範囲最上部までscrollするとイベント発火
1. #scrollにて追加のレコードを取得
1. jbuilderにてjson形式に変換しデータを返却する
1. 返ってきたデータを元にjs側で処理

#####実装
######1. #indexにて最新15件を取得し表示

```ruby:app/controllers/books_controller.rb
def index
  @books = Book.all.order(id: 'DESC').limit(15).offset(0)
  @book = Book.new
end
```
limitとoffsetについては[こちら](https://railsguides.jp/active_record_querying.html#limit%E3%81%A8offset)を参照してください。今回はこのoffsetを調整することで、追加のレコードを読み込んでいきます。
また、order(id: 'DESC')を指定したことでview側での並び順も逆順となっていますので、viewのeach部分を変更します。

```erb:app/views/books/index.html.erb
<% @books.each do |book| %>
<%# 上の記述を下のように変更します %>
<% @books.reverse_each do |book| %>
```

######2.scroll範囲最上部までscrollするとイベント発火

```js:app/assets/javascripts/scroll.js
$(document).on('turbolinks:load', function() {
  // load時最下部までscroll
  const target = $('.scroll__tbody');
  target.scrollTop(600); //tdのheight * 15件

  target.scroll(function(){
    if (target.scrollTop() == 0){
      const offset = target.children().length;
      $.ajax({
        type: 'GET',
        url: '/books/scroll',
        data:{
          offset: offset
        },
        dataType: 'json'
      })
      .done(function(data){
        // 返ってきたdataに対して処理を実行
      });
    }
  });
});
```
ポイントは2点です。
1. if (target.scrollTop() == 0)
最上部までscrollしたタイミングでajax通信を行なっています。
2. const offset = target.children().length;
tbody内の子要素の数をoffset値として取得し、後ほどコントローラに送っています。こうすることでindex表示後に新規レコードが追加された場合でもoffset値を動的に変更することができます。

###### 3.#scrollにて追加のレコードを取得
```ruby:app/controllers/books_controller.rb
def scroll
  offset = params[:offset] # ajaxで送られてきたパラメータの取得
  @books = Book.all.order(id: 'DESC').limit(15).offset(offset)
end
```
インスタンス化した＠booksはscroll.json.jbuilderに送られます。

###### 4.jbuilderにてjson形式に変換しデータを返却する
```ruby:app/views/books/scroll.json.jbuilder
json.array! @books do |book|
  json.title book.title
  json.body book.body
end
```
jbuilderでの変換例は[こちら](https://github.com/rails/jbuilder)にいくつか載っていますが、検索すると記事もたくさん出てきますので、そちらの方がわかりやすいと思います。

###### 5.返ってきたデータを元にjs側で処理

```js:app/assets/javascripts/scroll.js
// .done(function(data)の中身
// 返ってきたdataに対して処理を実行
if(data.length != 0){
  data.forEach(function(data){
    const html = `
      <tr>
        <td class='scroll__td'>${data.title}</td>
        <td class='scroll__td'>${data.body}</td>
      </tr>
    `;
    target.prepend(html);
  });
  // scroll位置を調整
  $(target).scrollTop(data.length * 40);
}
```
<details>
<summary>最終的なscroll.jsのcodeはこちら</summary><div>

```js:app/assets/javascripts/scroll.js
$(document).on('turbolinks:load', function() {
  // load時最下部までscroll
  const target = $('.scroll__tbody');
  target.scrollTop(600);

  target.scroll(function(){
    if (target.scrollTop() == 0){
      const offset = target.children().length;
      $.ajax({
        type: 'GET',
        url: '/books/scroll',
        data:{
          offset: offset
        },
        dataType: 'json'
      })
      .done(function(data){
        // 返ってきたdataに対して処理を実行
        if(data.length != 0){
          data.forEach(function(data){
            const html = `
              <tr>
                <td class='scroll__td'>${data.title}</td>
                <td class='scroll__td'>${data.body}</td>
              </tr>
            `;
            target.prepend(html);
          });
          // scroll位置を調整
          $(target).scrollTop(data.length * 40);
        }
      });
    }
  });
});
```

</div></details>

以上で終了です。

####おわりに
今回作成した簡単な例の場合、kaminariやjsライブラリを利用した方が早く実装できたのかもしれません。しかし、実装内容が複雑になってくると、gemやライブラリの利用方法を学んで実装するよりも、自分で記述した方が早い場合もあるのかなと思いました。