【Rails5】スクロールでページネーションを行う方法
YoutubeやGoogleのように、スクロールしたらどんどんコンテンツが出てくるような動作をこの記事ではご紹介します。前もって書いておきますと、この記事ではInifinite Scroll Javascript は使いません。そもそも私自身が個人的なプロジェクトでYoutubeのようなInfinite Scrollingを実装しよう思い、色々な記事を読み漁ったのですが、ほとんどの記事がKaminari + Inifinite Scroll Javascriptの組み合わせでした。Inifinite Scroll Javascriptは商用利用ですとお金が掛かる上に残念ながら私が求めるような動作では無かったため結局自前で実装しなければならず、その忘備録としてこの記事を書いています。分かりづらい所も多々あるとは思いますが、一つご参考になれば幸いです。
1.準備する
1-1. Gemのインストール
Railsを使ったアプリケーションでページネーションを行う際によく使われるgem Kaminariを使います。
GemfileにKaminariを追加し
1 |
gem 'kaminari' |
コンソールからgemをインストールします。
1 |
bundle |
1-2. Seedデータの用意
ページネーションを行うには何かしらのデータが必要になってきますので、適当にPost Modelを作り、レコードを大量生産します。
1 |
rails g scaffold post title:string |
1 2 3 |
100.times do |index| Post.create(title: "タイトル#{index}") end |
1 |
rake db:seed |
1-3.Viewの用意
一から作るのも面倒だったのでBootstrap4の「Dashboard 」サンプルから一部拝借してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">Company name</a> <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search"> <ul class="navbar-nav px-3"> <li class="nav-item text-nowrap"> <a class="nav-link" href="#">Sign out</a> </li> </ul> </nav> <div class="container-fluid"> <div class="row h-100"> <nav class="col-md-2 d-none d-md-block bg-light sidebar h-100"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" href="#"> <span data-feather="home"></span> Dashboard <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file"></span> Orders </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="shopping-cart"></span> Products </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="users"></span> Customers </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="bar-chart-2"></span> Reports </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="layers"></span> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="#"> <span data-feather="plus-circle"></span> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Year-end sale </a> </li> </ul> </div> </nav> <div id="infiniteScrollingContainer" class="col-md-2 pt-3 px-4 h-100 v-scroll"> <div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> </div> <main role="main" class="col-md-8 pt-3 px-4"> </main> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
html,body { height: 100%; letter-spacing: .03rem; font-family: 游ゴシック体, "Yu Gothic", YuGothic, "ヒラギノ角ゴシック Pro", "Hiragino Kaku Gothic Pro", メイリオ, Meiryo, Osaka, "MS Pゴシック", "MS PGothic", sans-serif; } .v-scroll { position: relative; overflow: scroll; } .container-fluid { height: calc(100vh - 40px) } |
先ほどscaffoldで生成されたPostsControllerに「dashboard」というactionを追加し、Post.allで大量生産したPostレコードを呼び出します。
1 2 3 |
def dashboard @posts = Post.all end |
1 2 3 4 |
Rails.application.routes.draw do get '/dashboard', to: "posts#dashboard" resources :posts end |
consoleからrails sをした後にlocalhost:3000/dashboardにアクセスして見ます。
先ほど作ったデータがずらっと並んでいると思います。
2.ページネーション
準備は整ったので、ここからページネーションを加えて行きます。PostsControllerに以下を加えます。
1 2 3 |
def dashboard @posts = Post.all.page(params[:page]) end |
.pageを付け加えることによって、Kaminariが小分けにしてレコードを呼び出してくれるようになります(デフォルトだと25)。
もう一度localhost:3000/dashboardにアクセスして見ましょう。
25番目のレコードまで呼び出されていますね。
それでは一旦dashboard.html.erbの該当部分に以下を加えて見ましょう
1 2 3 4 5 6 7 8 9 10 |
<div id="infiniteScrollingContainer" class="col-md-2 pt-3 px-4 h-100 v-scroll"> <div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> <%= paginate @posts %> </div> </div> |
もう一度localhost:3000/dashboardにアクセスして見ると、ページネーションのリンクが表示されていると思います。
先ほどrakeしたデータの個数は全部で100個なので、100 / 25の4ページに分割されています。2ページ目のリンクをクリックして見てください。タイトル25からタイトル49が表示されるようになるはずです。更に、URLにも注目して見ましょう。
http://localhost:3000/dashboard?page=2
URLの末尾に「?page=2」と表示されています。ここのパラメーター部分は後ほど使いますので覚えておきましょう。
3.Infinite Scrolling
3-1.目的の言語化
さて、ここからが本番です。YoutubeやGoogleみたいにスクロールした際にどんどんコンテンツを出すようにするには上記でやったページネーションの表現方法を変えているだけに過ぎません。やっていることは同じです。やりたい事を言語化すると、要は
ページネーションリンクをクリックする代わりにリストの一番最後までスクロールしたら次の25件を取得し、リストに加える
ということになります。
やりたいことは分かったので、更に細かく考えて行きましょう。「最後までスクロールしたら」ということはクライアントのスクロールを感知し、それがリストの最後まで行ったかの判定を行わないといけません。そして最後までスクロールした際に「次の25件を取得」するにはcontrollerを呼び出す必要があります。次の25件を取得した後にはそれを「リストに加える」必要があるので、今のリストに新しい25件を加える処理をしないといけません。
上記をまとめると、3つのアクションに分けられると思います。
- リストのスクロール判定
- クライアント側からcontrollerに通信を行い、次の25件を取得
- 取得した次の25件をリストに加える
3-2. ①リストのスクロール判定
これはjavascriptでスクロールされる度に呼び出される便利なon scroll functionがあるのでそれを使います。私はcoffeescriptを書く事を断念した人間なのでassets/javascriptsのposts.coffeeをposts.jsに変更し、以下を追加します。
1 2 3 4 5 6 |
$(document).on('turbolinks:load',function(){ $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { alert("最後までスクロールしました!") } }) |
読み砕いて行きましょう。
1 2 3 |
$('#infiniteScrollingContainer').on('scroll', function(){ }) |
はそのまま、リストがスクロールする度に呼び出される関数です。
次のif分岐を見て行きましょう。
1 2 3 |
if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { alert("最後までスクロールしました!") } |
このif分岐に含まれる4つの要素を説明します:
-
$(‘#infiniteScrollingContainer’).scrollTop() //要素のスクロール位置を返します
-
$(‘#infiniteScrollingContainer’).height() //画面上に表示されている要素の高さを返します
-
$(‘#infiniteScrollingContainer’)[0].scrollHeight //画面上に表示されていない、溢れた(overflow)要素を含めた高さを返します
-
16 //これはリストのpadding-topの高さです
上記を踏まえた上でもう一度if分岐の内容に当てはめて行きましょう。
$(‘#infiniteScrollingContainer’)要素のスクロール位置
+
$(‘#infiniteScrollingContainer’)要素の表示されている高さ(これは固定です)
上二つの値が
$(‘#infiniteScrollingContainer’)要素の画面上に表示されていない、スクローリング領域を含めた高さ
–
16 ( $(‘#infiniteScrollingContainer’)要素のpadding-topの高さ)
と同等の場合、alertが出るになっています。
3-3. ②クライアント側からcontrollerに通信を行い、次の25件を取得
最後までスクロールしたかどうかの判定処理は整いましたので、次はクライアントからcontrollerに通信を行い、次の25件のレコードを取得しないといけません。これを非同期的に行いたいので ajax関数を使います。
if分岐の中に以下を追加してください。
1 2 3 4 5 6 7 8 9 10 11 |
$(document).on('turbolinks:load',function(){ $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { setTimeout(function() { $.ajax({ url: "/infiniteScrolling?page=2" }) }, 500); } }) }); |
細かく見て行きましょう。
1 2 3 |
setTimeout(function(){ }, 500); |
は500ミリ秒(0.5秒)後に中の関数を呼び出しています。
1 2 3 |
$.ajax({ url: "/infiniteScrolling?page=2" }) |
は、非同期的に指定したurlにpostやgetリクエストを行うことができます(デフォルトはgetです)。まずは infiniteScrolling actionをPostsControllerに作り、ルーティングを設定しましょう。
1 2 3 4 5 6 |
def infiniteScrolling @posts = Post.all.page(params[:page]) respond_to do |format| format.js end end |
1 2 3 4 5 |
Rails.application.routes.draw do get '/dashboard', to: "posts#dashboard" get '/infiniteScrolling', to: "posts#infiniteScrolling" resources :posts end |
PostsControllerの infiniteScrolling actionで何をしているのかを見て行きましょう。まず、params[:page]でリクエストと一緒に送られてきているパラメータを取得しています。先ほどajax関数で明示的に指定したURLを例にした場合、
“/infiniteScrolling?page=2”
params[:page]は 「2」になります。
つまり、
@posts = Post.all.page(2)ということになりますので、Postモデルの全てのレコードの2ページ目(次の25件)を取得していることになります。
3-4. ③取得した次の25件をリストに加える
さて、次の25件を取得出来たので、次はそれをリストに加える必要があります。Railsのcontrollerは基本的に指定しない限りはactionと同名のview fileを探そうとします(この場合は /views/posts/infiniteScrolling.html.erbを探します)。しかしリストの最後までスクロールする度にページ全体が更新されるのは少しユーザビリティに欠けるかもしれません。動的に変化するのはリストの部分のみで、リストの最後に次の25件が付け加えられていく形で実装して行きます。
具体的な方法としてはJavascriptとパーシャル(部分テンプレート)の合わせ技で実現します。
RailsではView テンプレートの一部分のみを切り出して使いまわしたい時によく使用されるパーシャル (部分テンプレート)という便利な仕組みが存在します。
リストのフォーマットは統一されているので、
- リストのパーシャル(部分テンプレート)を作成する
- Javascriptを使い、パーシャル(部分テンプレート)をリストの最後に付け加える
これだけです。
①リストのパーシャル(部分テンプレート)を作成する
1 2 3 4 5 6 7 |
<div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> |
②Javascriptを使い、パーシャル(部分テンプレート)をリストの最後に付け加える
1 |
$("#infiniteScrollingContainer > div").append("<%= escape_javascript(render :partial => 'templates/posts/list') %>"); |
Railsのcontrollerでは出力するフォーマットを指定する事ができます。
infiniteScrolling actionの最後の3行を見て行きましょう。
1 2 3 |
respond_to do |format| format.js end |
ここでは明示的に、javascriptのファイルを探すように指定しています。(renderのオプションを使えばファイル名での指定も可能です。)この形式の指定によってcontrollerはinifiteScrolling.html.erbの代わりにinfiniteScrolling.js.erbを優先的に探すようになります。それではinifiniteScrolling.js.erbの中身を分解しながら見ていきましょう。
1 |
$(“#inifiniteScrollingContainer”).append() |
ここではappend メソッドを使って、リストの要素にコンテンツを追加しています。
1 |
<%= escape_javascript(render :partial => 'templates/posts/list') %> |
追加するコンテンツは先ほど作成したリストのパーシャル(部分テンプレート)です。ここで一度テストしましょう。
1 |
rails s |
リストの最後までスクロールするとPostモデルの全てのレコードの2ページ目(次の25件)を取得していることが分かります。ただこのままですとposts.jsのajax関数で明示的に「page=2」と直書きしているため、常に2ページ目を取得し続けます。ですので、pageパラメータを変数化する必要があります。
3-5. 取得するページの変数化
まず、ajax関数でinfiniteScrolling actionを呼び出す時に最後のページであるかどうかの判定を行い、最後のページでは無い場合はpageパラメータに +1 し、最後のページだった場合はこれ以上取得しないように、「”last”」を代入します。
1 2 3 4 5 6 7 8 9 |
<div id="infiniteScrollingContainer" class="col-md-2 pt-3 px-4 h-100 v-scroll" data-page="2"> <div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$(document).on('turbolinks:load',function(){ $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { var page = $("#infiniteScrollingContainer").attr('data-page'); var url = "/infiniteScrolling?page=" + page setTimeout(function() { $.ajax({ url: url }) }, 500); } }) }); |
1 2 3 4 5 6 7 8 9 10 11 |
def infiniteScrolling @posts = Post.all.page(params[:page]) if @posts.total_pages < params[:page].to_i @nextPage = "last" else @nextPage = params[:page].to_i + 1 end respond_to do |format| format.js end end |
1 2 3 4 5 |
if("<%=@nextPage%>" != "last"){ $("#infiniteScrollingContainer > div").append("<%= escape_javascript(render :partial => 'templates/posts/list') %>"); }else{ } $("#infiniteScrollingContainer").attr('data-page', "<%=@nextPage%>") |
一つずつ見ていきましょう。
dashboard.html.erbでは「data-page=2」を付け足しました。data 属性を使い、次に何ページ目を取得するかを管理します。
posts.jsでは最後までスクロールした際に、付け足したdata属性を取得し動的にajax関数のURLを変えています。
infiniteScrolling actionではPostモデルの全ページ数がpageパラメータより少ない場合は@nextPage変数に「”last”」を代入し、そうで無い場合はpageパラメータに +1をして@nextPage変数にそれを代入します。
inifiniteScrolling.js.erbでは@nextPage変数が「”last”」じゃ無い場合はリストのパーシャル(部分テンプレート)をリストに追加し、@nextPage変数が「”last”」の場合は何もしません。そして最後に@nextPage変数をリスト要素のdata属性に代入します。
実際の動作を見て見ましょう。
ちゃんと100件目のレコードまでスクロールされますね。
おまけ:ローディングの表示など
現状だと最後のページまでスクロールした後でも、もう次のページが無いことを分かっているのに毎度ajax関数でinfiniteScrolling actionにリクエストを送ります。無駄なので最後のページの場合はinifiniteScrolling actionへのリクエストを止めましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$(document).on('turbolinks:load',function(){ $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { var page = $("#infiniteScrollingContainer").attr('data-page'); if(page == "last"){return} var url = "/infiniteScrolling?page=" + page setTimeout(function() { $.ajax({ url: url }) }, 500); } }) }); |
もし data-pageが”last”の場合はreturnします。
さらに、現状だとスクロールした後に次の25件を取得しているかどうかが分かりづらいです。ローディングの表示があったほうがユーザビリティが少し向上するかもしれません。
ローディングの表示は@lukehaasさんのcss-loaders プロジェクトの中からお借りします(MITライセンスです)。
1 2 3 4 5 6 7 8 9 10 11 12 |
<div id="infiniteScrollingContainer" class="col-md-2 pt-3 px-4 h-100 v-scroll" data-page="2"> <div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> <div class="loader-container d-none"> <div class="loader"></div> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
.loader-container { height: 80px; padding-top: 20px; } .loader, .loader:before, .loader:after { background: #0dc5c1; -webkit-animation: load1 1s infinite ease-in-out; animation: load1 1s infinite ease-in-out; width: 1em; height: 4em; } .loader { color: #0dc5c1; text-indent: -9999em; margin: 0px auto; position: relative; font-size: 11px; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } .loader:before, .loader:after { position: absolute; top: 0; content: ''; } .loader:before { left: -1.5em; -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } .loader:after { left: 1.5em; } @-webkit-keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0; height: 4em; } 40% { box-shadow: 0 -2em; height: 5em; } } @keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0; height: 4em; } 40% { box-shadow: 0 -2em; height: 5em; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$(document).on('turbolinks:load',function(){ $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { var page = $("#infiniteScrollingContainer").attr('data-page'); if(page == "last"){return} $(".loader-container").removeClass("d-none") var url = "/infiniteScrolling?page=" + page setTimeout(function() { $.ajax({ url: url }) }, 500); } }) }); |
1 2 3 4 5 6 7 |
if("<%=@nextPage%>" != "last"){ $("#infiniteScrollingContainer > div").append("<%= escape_javascript(render :partial => 'templates/posts/list') %>"); $(".loader-container").addClass("d-none") }else{ $(".loader-container").html("<div class='text-center text-muted'>もう記事がありません!</div>") } $("#infiniteScrollingContainer").attr('data-page', "<%=@nextPage%>") |
以下が実際の動作です。
おまけ:その②
スクロールした際に、ajax関数が2回呼び出されることがあった場合は以下を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$(document).on('turbolinks:load',function(){ var ajaxStopper = false; $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { var page = $("#infiniteScrollingContainer").attr('data-page'); if(page == "last" || ajaxStopper){return} ajaxStopper = true; $(".loader-container").removeClass("d-none") var url = "/infiniteScrolling?page=" + page setTimeout(function() { $.ajax({ url: url }).always(function(){ ajaxStopper = false; }) }, 500); } }) }); |
ajaxStopperという変数を作り、ajax関数の2回目の呼び出しを防いでいます。
以上、YoutubeやGoogleのようにスクロールしたらどんどんコンテンツが出てくるInfinite Scrollingのご紹介でした。
コードまとめ
最終コードは以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
html,body { height: 100%; letter-spacing: .03rem; font-family: 游ゴシック体, "Yu Gothic", YuGothic, "ヒラギノ角ゴシック Pro", "Hiragino Kaku Gothic Pro", メイリオ, Meiryo, Osaka, "MS Pゴシック", "MS PGothic", sans-serif; } .v-scroll { position: relative; overflow: scroll; } .container-fluid { height: calc(100vh - 40px) } .loader-container { height: 80px; padding-top: 20px; } .loader, .loader:before, .loader:after { background: #0dc5c1; -webkit-animation: load1 1s infinite ease-in-out; animation: load1 1s infinite ease-in-out; width: 1em; height: 4em; } .loader { color: #0dc5c1; text-indent: -9999em; margin: 0px auto; position: relative; font-size: 11px; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } .loader:before, .loader:after { position: absolute; top: 0; content: ''; } .loader:before { left: -1.5em; -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } .loader:after { left: 1.5em; } @-webkit-keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0; height: 4em; } 40% { box-shadow: 0 -2em; height: 5em; } } @keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0; height: 4em; } 40% { box-shadow: 0 -2em; height: 5em; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class PostsController < ApplicationController before_action :set_post, only: [:show, :edit, :update, :destroy] def dashboard @posts = Post.all.page(params[:page]) end def infiniteScrolling @posts = Post.all.page(params[:page]) if @posts.total_pages < params[:page].to_i @nextPage = "last" else @nextPage = params[:page].to_i + 1 end respond_to do |format| format.js end end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">Company name</a> <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search"> <ul class="navbar-nav px-3"> <li class="nav-item text-nowrap"> <a class="nav-link" href="#">Sign out</a> </li> </ul> </nav> <div class="container-fluid"> <div class="row h-100"> <nav class="col-md-2 d-none d-md-block bg-light sidebar h-100"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" href="#"> <span data-feather="home"></span> Dashboard <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file"></span> Orders </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="shopping-cart"></span> Products </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="users"></span> Customers </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="bar-chart-2"></span> Reports </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="layers"></span> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="#"> <span data-feather="plus-circle"></span> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="#"> <span data-feather="file-text"></span> Year-end sale </a> </li> </ul> </div> </nav> <div id="infiniteScrollingContainer" class="col-md-2 pt-3 px-4 h-100 v-scroll" data-page="2"> <div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> <div class="loader-container d-none"> <div class="loader"></div> </div> </div> <main role="main" class="col-md-8 pt-3 px-4"> </main> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$(document).on('turbolinks:load',function(){ var ajaxStopper = false; $('#infiniteScrollingContainer').on('scroll', function(){ if($('#infiniteScrollingContainer').scrollTop() + $('#infiniteScrollingContainer').height() == $('#infiniteScrollingContainer')[0].scrollHeight - 16) { var page = $("#infiniteScrollingContainer").attr('data-page'); if(page == "last" || ajaxStopper){return} ajaxStopper = true; $(".loader-container").removeClass("d-none") var url = "/infiniteScrolling?page=" + page setTimeout(function() { $.ajax({ url: url }).always(function(){ ajaxStopper = false; }) }, 500); } }) }); |
1 2 3 4 5 6 7 |
<div> <% @posts.each do |post| %> <div> <div><%= post.title %></div> </div> <% end %> </div> |
1 2 3 4 5 6 7 |
if("<%=@nextPage%>" != "last"){ $("#infiniteScrollingContainer > div").append("<%= escape_javascript(render :partial => 'templates/posts/list') %>"); $(".loader-container").addClass("d-none") }else{ $(".loader-container").html("<div class='text-center text-muted'>もう記事がありません!</div>") } $("#infiniteScrollingContainer").attr('data-page', "<%=@nextPage%>") |