[Rails]タブと無限スクロール機能の両立(kaminariとjscroll)

今回はタブ機能と無限スクロール機能を実装したけど、無限スクロールで表示される要素がダブったり、表示されなかったりといった不具合が発生している方向けに発信していきます。

ぼく自身がこの不具合にだいぶハマってしまったので、同じような不具合にハマってる方の役にたてれば幸いです。

#開発環境

> ruby 2.6.3
> Rails 5.2.6

#前提

- 投稿サイトを想定
- 無限スクロールはkaminariとjscrollで実装
- タブはCSSとJavaScriptで実装

無限スクロールとタブ機能の実装は以下記事の実装方法を使用。
[[Rails]kaminariとjscrollを使って無限スクロールを実装](https://qiita.com/nakachan1994/items/1edc7a03e36bc00992c5)
[[Rails]CSSとJavaScriptを使ったタブ切り替え機能](https://qiita.com/nakachan1994/items/566cfcb60376feb3fe8c)


モデル名、変数名等は適宜、ご自身の開発環境に変換してお考えください。

user  ユーザー
post  投稿
like  いいね

new_posts(新着順)
Popular_posts(人気順)
Pick_up_posts(今週のおすすめ）

#まずは現状の確認

まずはタブと無限スクロール機能のコードの確認をしていきます。

コントローラーは、popular_postsとpick_up_postsに1行でpage(params[:page]).per(3)を入れると`NoMethodError in PostsController#index
undefined method 'page' for #<Array:0x00007fddb86668e8>`とエラーが出るので、arrayオブジェクトに対してページネーションしています。

```controllers/posts_controller.rb
def index
  @new_posts = Post.page(params[:page]).per(3)
  # 2行に分ける
  @popular_posts = Post.find(Like.group(:post_id).order('count(post_id) desc').pluck(:post_id))
  @popular_posts = Kaminari.paginate_array(@popular_posts).page(params[:page]).per(3)
  # 2行に分ける
  @pick_up_posts = Post.find(Like.group(:post_id).where(created_at: Time.current.all_week).order('count(post_id) desc').pluck(:post_id))
  @pick_up_posts = Kaminari.paginate_array(@pick_up_posts).page(params[:page]).per(3)
  end
```

次はビュー
ビューでは、ページ表示部分に各変数を渡しています。

```erb:views/posts/index.html.erb
      <!--タブ部分-->
      <ul class="tab-list d-flex justify-content-around text-center list-unstyled mx-3">
        <li class="tab tab-active heading">
          New
        </li>
        <li class="tab heading">
          Popular
        </li>
        <li class="tab heading">
          Pick up
        </li>
      </ul>
      <!--タブで選択された要素部分-->
      <div class="tabbox-contents">
        <div class="tabbox box-show">
          <!--無限スクロール-->
          <div class="scroll-list jscroll">
            <%= render 'posts/list', posts: @new_posts %>
            <!--ページ表示部分-->
            <%= paginate @new_posts %>
          </div>
        </div>
        <div class="tabbox">
          <!--無限スクロール-->
          <div class="scroll-list jscroll">
            <%= render 'posts/list', posts: @popular_posts %>
            <!--ページ表示部分-->
            <%= paginate @popular_posts %>
          </div>
        </div>
        <div class="tabbox">
          <!--無限スクロール-->
          <div class="scroll-list jscroll">
            <%= render 'posts/list', posts: @pick_up_posts %>
            <!--ページ表示部分-->
            <%= paginate @pick_up_posts %>
          </div>
        </div>
      </div>
```

次にjavascriptファイル（タブの処理は割愛）

```assets/javascripts/application.js
// 無限スクロール
$(window).on('scroll', function() {
    scrollHeight = $(document).height();
    scrollPosition = $(window).height() + $(window).scrollTop();
    if ( (scrollHeight - scrollPosition) / scrollHeight <= 0.05) {
          $('.jscroll').jscroll({
            contentSelector: '.scroll-list',
            nextSelector: 'span.next:last a'
          });
    }
});
```

今回変更するのはコントローラー、ビュー、jsの無限スクロールの処理だけなので他のコードは割愛しています。

以下記事に詳しい実装方法ありますので、ご確認を
無限スクロールとタブ機能の実装は以下記事の実装方法を使用。
[[Rails]kaminariとjscrollを使って無限スクロールを実装](https://qiita.com/nakachan1994/items/1edc7a03e36bc00992c5)
[[Rails]CSSとJavaScriptを使ったタブ切り替え機能](https://qiita.com/nakachan1994/items/566cfcb60376feb3fe8c)

#変更するポイント

変更するポイントは３つです。

- コントローラーのparams[:page]を変更
- ビューで受け取れるようにする
- 選択されたタブのみを無限スクロールを発動

ではポイントをみていきましょう!

#コントローラーのparams[:page]を変更

まずは現状全てのページネーションがparams[:page]になっています。
これでは、newなのかpopularなのかの判断が出来ないので、表示されるデータがダブって出てきてしまいます。

なので、判別できるようにキー名を変更していきます。

```controllers/posts_controller.rb
def index
  # new_pageに変更
  @new_posts = Post.page(params[:new_page]).per(3)
  @popular_posts = Post.find(Like.group(:post_id).order('count(post_id) desc').pluck(:post_id))
  # popular_pageに変更
  @popular_posts = Kaminari.paginate_array(@popular_posts).page(params[:popular_page]).per(3)
  @pick_up_posts = Post.find(Like.group(:post_id).where(created_at: Time.current.all_week).order('count(post_id) desc').pluck(:post_id))
  # pick_up_pageに変更
  @pick_up_posts = Kaminari.paginate_array(@pick_up_posts).page(params[:pick_up_page]).per(3)
  end
```

#ビューで受け取れるようにする

コントローラーから送られてくるキー名が変わったのでビューの受け取るキー名も変更します。
`<%= paginate 変数名, param_name: "キー名" %>`とすることで、受け取るキー名を指定できます。

あと、scroll-listにクラス名、タブ名を追加しておきます。（次のステップで解説します）

```erb:views/posts/index.html.erb
:
      <div class="tabbox-contents">
        <div class="tabbox box-show">
          <!--classにnew-tab,tabに.new-tab追加-->
          <div class="scroll-list jscroll new-tab" tab=".new-tab">
            <%= render 'posts/list', posts: @new_posts %>
            <!--param_name追加-->
            <%= paginate @new_posts, param_name: "new_page" %>
          </div>
        </div>
        <div class="tabbox">
          <!--classにpopular-tab,tabに.popular-tab追加-->
          <div class="scroll-list jscroll popular-tab" tab=".popular-tab">
            <%= render 'posts/list', posts: @popular_posts %>
            <!--param_name追加-->
            <%= paginate @popular_posts, param_name: "popular_page" %>
          </div>
        </div>
        <div class="tabbox">
          <!--classにpick-up-tab,tabに.pick-up-tab追加-->
          <div class="scroll-list jscroll pick-up-tab" tab=".pick-up-tab">
            <%= render 'posts/list', posts: @pick_up_posts %>
            <!--param_name追加-->
            <%= paginate @pick_up_posts, param_name: "pick_up_page" %>
          </div>
        </div>
      </div>
:
```

#選択されたタブのみを無限スクロールを発動

最後にjsファイルを変更していきます。

jsファイルでは、今選択されているタブを判断して、そのタブのを無限スクロールして要素を追加していきます。

```assets/javascripts/application.js
// 無限スクロール
$(window).on('scroll', function() {
    scrollHeight = $(document).height();
    scrollPosition = $(window).height() + $(window).scrollTop();
    if ( (scrollHeight - scrollPosition) / scrollHeight <= 0.05) {
          // .box-show > を追加
          $('.box-show > .jscroll').jscroll({
            // 以下１行を変更
            contentSelector: $('.box-show > .scroll-list').attr('tab'),
            nextSelector: 'span.next:last a'
          });
    }
});
```

ちょっと難しいので細かくみていきます

```js:解説
// ページ下の5%の範囲に来たら発火
// 選択されたtabboxは.box-showになるので
// .box-show > .jscrollで現在選択中のタブを指定
$('.box-show > .jscroll').jscroll({
  // 無限スクロールした要素のどこを使うか
  // attr('tab')で.box-show > .scroll-listにtabを追加(tabの中身はclass名）
  contentSelector: $('.box-show > .scroll-list').attr('tab'),
  nextSelector: 'span.next:last a'
});
```

tabで今選択されているタブを判断できるから、データがぐちゃぐちゃにならずに、無限スクロールして要素の追加ができます。

以上で確認すると正常に無限スクロールが動作しているはずです。

#まとめ

変更するポイントは３つです。

>- コントローラーのparams[:page]を変更
>- ビューで受け取れるようにする
>- 選択されたタブのみを無限スクロールを発動

ぼくはこの実装にかなりハマってしまいました。
そもそも、無限スクロールとタブ機能を実装した時点でちゃんと動作していないことにすら、気づいていませんでしたｗ
別の動作の確認をしているときに、「あれ？なんかめっちゃスクロールできるんやけどなんで？」となって見てみるとデータが重複して表示されていることに気づきました。

まずページネーションでエラーが出たところからはじまり、ページネーションのキー名の指定はKaminariのGitHubを見に行ったり、jsの処理はJavaScriptやjQueryで検索しまくってやっと実装できました。
調べまくったらなんとかなるもんですな。

同じようなエラーにハマってる方の役に立てたらいいな〜

最後までありがとうございました。