【jQuery, kaminari】無限スクロールと逆スクロール機能の実装

# 0. これは何？
LINEライクなチャットアプリ作成の一端。
一部jQueryのライブラリやRailsのgem kaminariを使用。

1. 無限スクロールの実装
2. 逆スクロールの実装

に分けて解説。

# 1. 無限スクロールの実装

##  完成形ソースコード
####**js側**
```infinite_scroll.js
$(document).on("turbolinks:load", function() {
  if ($("nav.pagination a[rel=next]").length){
    $(".user_list").infiniteScroll({
      path: "nav.pagination a[rel=next]",
      append: ".user_list ul",
      elementScroll: true,
      history: true,
      prefill: false,
      status: ".page-load-status",
      hideNav: ".pagination"
    })
```
####**view側**

```erb
<div class="user_list">
  <ul>
    <% @rooms.each do |room| %>
      <li>
        <%= link_to (room) do %>
          <div class="icon">
            <img src="<%= room.user.picture_url %>" alt="<%= room.user.display_name %>">
          </div>
          <p data-id="<%= room.id %>"><%= room.user.display_name %></p>
          <span class="content"><%= room.last_message %></span>
          <i class="alert"><%= room.unread_count %></i>
          <time><%= room.updated_at.to_s(:datetime_jp) %></time>
        <% end %>
        <div class="pin">
          <%= link_to "", pin_room_path(room.id), method: :post, remote: true, class: room.pin ? 'icon-star2' : 'icon-star' %>
        </div>
      </li>
    <% end %>
  </ul>
  <%= paginate @rooms, param_name: :rooms_page %>
  <div class="page-load-status">
    <p class="infinite-scroll-request">
    読み込み中...
    </p>
  </div>
</div>
```

## 解説
### turbolinks

```js
$(document).on("turbolinks:load", function() {})
```

- [Rails5でjqueryを動かす方法](https://qiita.com/hiroyayamamo/items/b258acbaa089d9482c8a)

> jqueryを使うと初回ページ読み込み&リロードで動くが、ページ遷移して該当ページに戻ってくるとjqueryが動かない。
デフォルトgemであるturbolinks（ajaxとajaxとhistoryAPIを使ってページ遷移しやすくするもの）が動作しているため、そもそも$(document).ready()が動かない

- [Rails5でTurbolinksとjQuery(document).ready()
](https://www.taillook.tech/entry/2017/02/07/010750)
- [Rails5でTurbolinksの挙動が変わってた。](https://qiita.com/syou007/items/fda40f65634bb2fadf36)

###  DOM要素の存在確認

```js
if ($("nav.pagination a[rel=next]").length){}
```

- [jQueryによる要素の存在チェックまとめ
](http://www.koikikukan.com/archives/2012/04/18-022222.php)
- [DOMの存在確認する方法で処理速度の速いコードは？](https://qiita.com/ketabawo/items/056a487c593be22bd319)

### Infinite Scroll

```js
$(".user_list").infiniteScroll({  // コンテナ(スコープとなるセレクタ)に対し無限スクロールを実行
  path: "nav.pagination a[rel=next]",  // 次ページへのページネーションリンクを指定
  append: ".user_list ul",  // コンテナに追加する要素の指定 ※コンテテナの指定とappendする要素の指定が上手くいかないとレイアウトが崩れるなどの問題が起こる
  elementScroll: true,  // 無限スクロールを適用する範囲の指定。trueだとコンテナを無限スクロール範囲とする。デフォルトでは`window`全体が適用範囲となる。
  history: true,  // 読み込むたびにURLを書き換えるかどうかの指定
  prefill: false,  // 一番下までスクロールする前に読み込むかどうかの指定
  status: ".page-load-status",  // 読み込み中や読み込み終了時に表示するものを指定
  hideNav: ".pagination"  // ページネーションリンクを表示するかどうかを指定(display: noneと同じ)
})
```

- [Infinite Scroll Automatically add next page
](https://infinite-scroll.com)
公式のドキュメントを読む。
- [Rails & kaminariで無限スクロール（Infinite Scroll）する](https://qiita.com/yubachiri/items/0f772c6ecbbc0d7c65a9)
日本語記事の中では詳細なオプションの説明がある。
- Stackover flowで同じような実装をしているサンプルを見つける

### 補足

無限スクロールを実装するにあたって、HTML・CSSのレイアウトが正確であることは確認する必要がある。
> また、infinite scrollの初期仕様上、スクロールして最下部まで行かないと次のロードが始まりません。
つまり、Windowの縦幅が広く、最初の状態よりも長い幅になっているとinfinite scrollが発生しないかもしれないです。
Windowサイズを縦に小さくして確認してみてください。

[Ruby on Rails、 Infinite Scrollとkaminariを使用して無限スクロールの実装について
](https://teratail.com/questions/77892)

# 2. 逆スクロールの実装
## 完成形ソースコード

####**js側**

```reverse_scroll.js
$(document).on("turbolinks:load", function() {
    var i = 2;
    $(".chat").scrollTop(1);
    $(".chat").scroll(function() {
      if (($(".chat").scrollTop() == 0) && (i <= parseInt($(".message_view").find("nav.pagination span.last a").prop("search").match(/[0-9]/), 10))){
        $.ajax({
          url: $(".message_view").find("nav.pagination a[rel=next]").prop("search").replace(/[0-9]/, i)
        }).done(function(data) {
          $(".chat ul").prepend($(data).find(".chat ul").html());
          $(".chat").scrollTop($(".chat").first().height());
          i++;
        })
      }
    })
  }
})
```

####**view側**
順スクロールと同じ。

## 解説

大まかな基本方針はだいたいこんな感じ。

1. 表示されているメッセージページを上までスクロールする。
2. 表示されているメッセージの上に過去のメッセージを追加する。
3. Ajax通信を使用。

- [逆順の無限スクロール(infinite scroll)を3行で実現する
](https://hacknote.jp/archives/30885/)
- [Infinite Scroll in both directions
](https://stackoverflow.com/questions/20477420/infinite-scroll-in-both-directions)
Answerが両方向無限スクロールの簡単なサンプル


### turbolinks

```js
$(document).on("turbolinks:load", function() {})
```

- [Rails5でjqueryを動かす方法](https://qiita.com/hiroyayamamo/items/b258acbaa089d9482c8a)

> jqueryを使うと初回ページ読み込み&リロードで動くが、ページ遷移して該当ページに戻ってくるとjqueryが動かない。
デフォルトgemであるturbolinks（ajaxとajaxとhistoryAPIを使ってページ遷移しやすくするもの）が動作しているため、そもそも$(document).ready()が動かない

- [Rails5でTurbolinksとjQuery(document).ready()
](https://www.taillook.tech/entry/2017/02/07/010750)
- [Rails5でTurbolinksの挙動が変わってた。](https://qiita.com/syou007/items/fda40f65634bb2fadf36)

### 1.  スクロールウィンドウでの初期位置の指定

```js
$(".chat").scrollTop(1);
```
[.scrollTop( )](http://www.jquerystudy.info/reference/css/scrollTop.html):
指定した要素のスクロール位置(Y座標)を取得。引数を設定した場合はjQueryオブジェクトで指定した、要素のスクロール位置を設定

### 2. スクロール時の処理の設定

```js
$(".chat").scroll(function() {})
```

[.scroll( )](http://www.jquerystudy.info/reference/events/scroll.html):
スクロールした時に引数のfunctionを実行する。ブラウザイベントの関数。

### 3. スクロール時に処理を実行する条件の指定

```js
 if (($(".chat").scrollTop() == 0) && (i <= parseInt($(".message_view").find("nav.pagination span.last a").prop("search").match(/[0-9]/), 10)))
```

- `$(".chat").scrollTop() == 0`: 指定した要素のスクロール位置が0かどうか。
- [parseInt()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt): 第1引数の文字列をパースし、第2引数に与えられた基数にもとづく整数を返す。
    - [型変換のいろいろ
](https://qiita.com/zawascript/items/a25eaf7a222ac3671275)
    - [JavaScriptにおける数値⇔文字列の型変換あれこれ](https://lealog.hateblo.jp/entry/2013/02/28/005010)
- `$(".message_view").find("nav.pagination span.last a")`: ページネーションリンクの最後のページを指すリンクを指定。
    - [.find( )](http://www.jquerystudy.info/reference/traversing/find.html): jQueryオブジェクトで指定した要素の子孫要素の中で、引数で指定したセレクタ(jQueryオブジェクト、DOM要素)にマッチする要素を選択。
- `.prop("search")`: リンクのURLパラメータ(eg.`?page=2`)を取得
    - [.prop( )](http://www.jquerystudy.info/reference/attributes/prop.html): 引数にプロパティを指定することでプロパティの値を取得する。指定したプロパティがない場合はundefinedを返す。第2引数に値や処理を設定することでプロパティの値を設定、変更する。
        - [オブジェクトとプロパティ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties)
 
        > JavaScript のオブジェクトは、自身に関連付けられたプロパティを持ちます。オブジェクトのプロパティは、オブジェクトに関連付けられている変数と捉えることができます。オブジェクトのプロパティは、オブジェクトに属するものという点を除けば、基本的に通常の JavaScript 変数と同じようなものです。オブジェクトのプロパティは、オブジェクトの特性を定義します。オブジェクトのプロパティには、単純なドット表記でアクセスします。
        >```object_dot_property.js
        >var myCar = new Object();
        >myCar.make = 'Ford';
        >myCar.model = 'Mustang';
        >myCar.year = 1969;
        >```

- `.match(/[0-9]/), 10)`: 正規表現で数字にマッチするもの(=ページ番号(e.g. `2`))を取得。
    - [.match()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/match): 引数に指定した正規表現に対する文字列オブジェクトへのマッチングの際に、そのマッチの結果を得るために使われる。

### 4. Ajaxによる非同期通信で次ページのHTML要素を取得
```js
$.ajax({
  url: $(".message_view").find("nav.pagination a[rel=next]").prop("search").replace(/[0-9]/, i)
}).done(function(data) {
  $(".chat ul").prepend($(data).find(".chat ul").html());
  $(".chat").scrollTop($(".chat").first().height());
  i++;
})
```

- [.load()](http://www.jquerystudy.info/reference/ajax/load.html): jQueryオブジェクトで指定した要素にサーバーから受信した情報をhtmlコンテンツとして読み込む。第2引数にobjectオブジェクトを設定することで、サーバーにデータを送信することができる。第3引数にfunctionを設定することで、読み込みが完了したら処理を実行することができる。

- [jQuery.get()](http://www.jquerystudy.info/reference/ajax/get.html): `jQuery.ajax`メソッドを簡単に利用できるようにしたメソッド。`load`メソッドはセレクタで指定した要素に強制的に読み込まれたデータを表示するが、データによってはな表示せずに内部的に利用したい場合があるのでその場合に利用する。引数に設定する内容は`load`メソッドと似ている。

- [jQuery.ajax( )](http://www.jquerystudy.info/reference/ajax/ajax.html): `jQuery.get()`メソッドの拡張版。第1引数にurlを設定できる。