Ruby
Article

Infinite Scrolling in Rails: The Basics

By Ilya Bodrov-Krukowski

no_load_more

Pagination is a very common and widely-used navigation technique, and with good reason. First of all, consider performance. Loading all the available records in a single query can be very costly. Moreover, a user may be interested only in a couple of the most recent records (i.e., the latest posts in a blog) and does not want to wait for all records to load and render. Also, pagination makes reading the page easier by not flooding it with content.

Nowadays, many websites use a slightly different technique, called infinite scrolling (or endless page). Basically, as the user scrolls down the page, more records are loaded asynchronously using AJAX. In this manner, scrolling seems more natural and can be easier for a user than constantly clicking on a ‘Next page’ link.

In this article, I am going to explain how to implement infinite scrolling in place of classic pagination.

First, we will prepare our demo project, implementing basic pagination using the will_paginate gem. This pagination will become infinite scrolling as we work through the tutorial. This will require writing some JavaScript (and CoffeeScript) code, along side our Ruby.

The provided solution will fallback to the default pagination if a user has javascript disabled in the browser. Finally, our view will require virtually no modifications, so you can easily implement this on any website.

Other items that will be covered:

  • How to implement a “Load more” button instead of infinite scrolling, much like the one
    used on SitePoint.
  • Some gotchas and potential problems, specifically, how the History API and scroll spying can help us.

The working demo can be found at http://sitepoint-infinite-scrolling.herokuapp.com.

The source code can be found on GitHub.

Sound good? Let’s get rolling!

Preparing the Project

For this article I will be using Rails 3.2.16 but you can implement the same solution with Rails 4.

Lets create a very simple blog. For now, it will only display demo posts.

$ rails new infinite_scrolling -T

-T here means that we want to skip generating a test suite (I prefer RSpec but of course you can omit this flag).

We are going to hook up some gems that will come in handy:

Gemfile

gem 'will_paginate', '~> 3.0.5'
gem 'betterlorem', '~> 0.1.2'
gem 'bootstrap-sass', '~> 3.0.3.0'
gem 'bootstrap-will_paginate', '~> 0.0.10'

will_paginate will, well, paginate our records. I will go into more detail about this gem in the next section. betterlorem generates demo text in our records. There are other similar gems that produce “Lorem Ipsum” text, but I’ve found this one to be the most convenient for our case (we are going to use it in seeds.rb, not in the view).

There is no point to create an award-winning design, so as a fast and easy (though not the smallest, considering the weight) solution we will use Twitter Bootstrap 3. The bootstrap-sass gem adds it into our Rails project. bootstrap-will_paginate contains some Bootstrap styling for the pagination itself.

Do not forget to run

$ bundle install

Now add

//= require bootstrap

to the application.js and

@import "bootstrap";

to the application.css.scss to include all the Bootstrap styles and scripts. Of course, in the real application you would choose only the required components.

The Model

There will be only one table: Post. It will be dead simple and contain the following
columns:

  • id (integer, primary key)
  • title (string)
  • body (text)
  • created_at (datetime)
  • updated_at (datetime)

Running

$ rails g model Post title:string body:text
$ rake db:migrate

will create an appropriate migration and then apply it to the database.

The next step is to produce some test data. The easiest way to do this is to use seeds.rb.

seeds.rb

50.times { |i| Post.create(title: "Post #{i}", body: BetterLorem.p(5, false, false)) }

This creates 50 posts with the body generated by BetterLorem. Each set of generated content consists of 5 paragraphs. The last two arguments tell BetterLorem to wrap the text in a p tag and include a trailing period.

Running

$ rake db:seed

will populate our database with some test posts. Awesome!

The last thing is to create a PostsController with index and show methods, along with the corresponding views (index.html.erb and show.html.erb). Also, do not forget to set up the routes:

routes.rb

resources :posts, only: [:index, :show]
root to: 'posts#index'

Finally, if you’re using Rails 3, be sure to delete the public/index.html file.

The Controller

We are ready to move to the fun part. At first, let’s display some paginated posts with a truncated body. For this, we are going to use will_paginate – a simple yet convenient gem by Mislav Marohnić that works with Ruby on Rails, Sinatra, Merb, DataMapper and Sequel.

There is an alternative to this solution – kaminari by Akira Matsuda that is more powerful and more sophisticated. You can also give it a try. Basically, it doesn’t matter which gem you use.

In our controller:

posts_controller.rb

@posts = Post.paginate(page: params[:page], per_page: 15).order('created_at DESC')

The call to the paginate method accepts a page option telling it which GET parameter to use to fetch the required page number. The per_page option specifies how many records should be displayed per page. The per_page option can be specified for the whole model or for the whole project like this:

post.rb

class Post
  self.per_page = 10
end

will_paginate.rb (in an initializer)

WillPaginate.per_page = 10

The paginate method returns a an ActiveRecord::Relation so we can chain on a call to the order method.

The View

index.html.erb

<div class="page-header">
  <h1>My posts</h1>
</div>

<div id="my-posts">
  <%= render @posts %>
</div>

<div id="infinite-scrolling">
  <%= will_paginate %>
</div>

A page header is specified with the help of a Bootstrap class. The next block, #my-posts, contains our paginated posts. Using render @posts displays each post from the array using the _post.html.erb partial. The last block, #infinite-scrolling, houses the pagination controls.

Note that will_paginate is clever enough to understand that we want to paginate @posts. You can specify it explicitly like this: will_paginate @posts.

Here is our post partial.

_post.html.erb

<div>
  <h2><%= link_to post.title, post_path(post) %></h2>

  <small><em><%= post.timestamp %></em></small>

  <p><%= truncate(strip_tags(post.body), length: 600) %></p>
</div>

We wrap every post with a div, then display the title that acts as a link to read the whole post. The timestamp indicates when the post was created. This timestamp method is defined inside the model like this:

post.rb

def timestamp
  created_at.strftime('%d %B %Y %H:%M:%S')
end

Lastly we use the strip_tags method to remove all the tags from the post and truncate to strip all but the first 600 symbols. This ends our work with the views (I have omitted markup for the layout.html.erb and show.html.erb as it is not that important; you can check it in the GitHub repo).

Infinite Scrolling

We are ready to transform our pagination to infinite scrolling. jQuery will help us with this task.

Create a new file pagination.js.coffee inside the javascripts directory.

pagination.js.coffee

jQuery ->
  if $('#infinite-scrolling').size() > 0
    $(window).on 'scroll', ->
      more_posts_url = $('.pagination .next_page a').attr('href')
        if more_posts_url && $(window).scrollTop() > $(document).height() - $(window).height() - 60
            $('.pagination').html('<img src="/assets/ajax-loader.gif" alt="Loading..." title="Loading..." />')
            $.getScript more_posts_url
        return
      return

Here we are binding a scroll event to the window, only if the pagination is present on the page. When the user scrolls, fetch the link to the next page – visiting it will make Rails load the records from that page (we still need to make some modifications to the controller to get this working).

Then, check that the URL is present and the user scrolled to the bottom of the page minus 60px. This is when we want to load more posts. This value of 60px is arbitrary, and you’ll probably want to change it for your case.

If these conditions are true we are replacing our pagination with a “loading” GIF image that can be freely downloaded at ajaxload.info. The last thing to do is to actually perform an asynchronous request using the URL that we’ve fetched previously. $.getScript will load a JS script from the server and then execute it.

Note the two return instructions. By default, CoffeeScript will return the last expression (the same concept applies to Ruby) but here we do not want the jQuery function or event handler to return anything so specifying return means “return nothing”.

The PostController#index method should respond to both HTML and JavaScript. We are going to use respond_to to achieve that:

posts_controller.rb

@posts = Post.paginate(page: params[:page], per_page: 15).order('created_at DESC')
respond_to do |format|
  format.html
  format.js
end

The last thing to do is to create a view that will be rendered when responding with JS:

index.js.erb

$('#my-posts').append('<%= j render @posts %>');
<% if @posts.next_page %>
  $('.pagination').replaceWith('<%= j will_paginate @posts %>');
<% else %>
  $(window).off('scroll');
  $('.pagination').remove();
<% end %>

We are rendering more posts by appending them to the #my-posts block. After that, check to see if there are more pages left. If so, replace the current pagination block (which at this point contains “loading” image) with a new pagination. Otherwise, we remove the pagination controls and unbind the scroll event from window, as there is no point in listening to it anymore.

At this point, our infinite scrolling is ready. Even if a user has JavaScript disabled in the browser, it will be presented with the default pagination that has some styling applied thanks to the bootstrap-will_paginate gem.

One thing worth mentioning is that scrolling will fire loads of scroll events. If you want to delay handling of this event you can use an OpenSource library BindWithDelay written by Brian Grinstead. To use it, simply download it and include it in the project. Then, make the following modifications to the script:

pagination.js.coffee

$(window).bindWithDelay 'scroll', ->
  # the code
, 100

This will delay firing the event by 100ms. $(window).off('scroll'); inside the index.js.erb will still unbind the event, so no modifications is needed there.

This ends the first part of the article. In the next part we are going to talk about the “Load more” button and some problems that arise when using infinite scrolling. Thanks for reading and see you soon!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • heel

    My gem for infinite scroll in rails: https://github.com/heel/scrollinity

    here is the description how it works + sample app: http://kvaziblog.com/posts/7

  • tomwardrop

    I’m not a huge fan of infinite scrolling. It seems like a sound idea in theory, but I find it less practical than pagination in nearly all scenarios, especially on mobile devices where infinite scrolling consumes precious resources, slowing down the browsing experience, and causing all kinds of issues with history states and scroll position.

    If you insist on using infinite scrolling, at least implement it using pushstate so one can navigate back to the appropriate position on the page, and implement it in a way that intelligently removes elements from the DOM so only a certain amount of posts/records are on the page at any given time, to save resources and keep page length manageable. Or, just use pagination and avoid all the problems that come with the infinite scrolling fad.

    • Ilya Bodrov

      Actually, I will write about State/History API and infinite scrolling in the next part which will be published in a week or so :) And I agree that this technique can really bring many problems (which I will also describe). Thanks for the comment!

  • Irakli Koridze

    Pagination JS coffee gives me error :(

  • Ilya Bodrov

    What kind of error?

    • denny

      i get this error

      [stdin]:5:1: error: unexpected indentation
      if more_posts_url && $(window).scrollTop() > $(document).height() – $(window).height() – 60
      ^^^^^^^^

  • Ilya Bodrov

    CoffeeScript relies heavily on indentations and seems you have a problem with it. Please post the whole contents of the pagination.js.coffee file somewhere so that I can check it.

    • Yumiko Huang

      I have a similar issue, and I don’t know how to fix it. please instruct.

      if I include this line on my file,
      true %>

      I will get
      SyntaxError: [stdin]:5:1: unexpected indentation

      The interesting thing is that if I get rid of this line, everything works perfectly.

      I have to implement infinite scrolling into one of my projects. I really want to know how to fix it.

      Thank you very much.

  • Marko Ćilimković

    If you just copied it from here, your text editor might not copy it with the proper indentations. Just correct the indentations and it should work. Or copy it from me:

    jQuery ->
    if $(‘#infinite-scrolling’).size() > 0
    $(window).on ‘scroll’, ->
    more_posts_url = $(‘.pagination .next_page a’).attr(‘href’)
    if more_posts_url && $(window).scrollTop() > $(document).height() – $(window).height() – 60
    $(‘.pagination’).html(”)
    $.getScript more_posts_url
    return
    return

    But I have one other issue. When I scroll down to the end of the page the pagination.js.coffee has an error on line 3, which says: Uncaught type error: Object[object Object] has no method ‘on’. Which means $(window) doesn’t have a method ‘on’ I guess…how to fix it?

  • Armando Braun

    I’m having trouble with the index.js.erb file, apparently it’s generating line breaks between posts tags, which shows an error. any idea? thanks!!!
    I get: SyntaxError: Unexpected token ILLEGAL

  • Armando Braun

    I’m getting an error with the index.js.erb script. Apparently it’s generating line breaking between the posts tags and it’s not passing through. the error Im getting is: SyntaxError: Unexpected token ILLEGAL
    if I copy and paste the html that’s passing and remove the br lines it works fine

    • Ilya Bodrov

      Are copying it directly from here? Maybe some invisible special character is somewhere in the code

  • Armando Braun

    can you specify the necessary adaptations to the coffeescript if using kaminari? Thanks!!!!

    • Ilya Bodrov

      I will try to look into that but I guess only class names will change. Well, maybe selectors will vary a bit as well.

  • RailsZilla

    JS should go like this

    jQuery ->
    **if $(‘#infinite-scrolling’).size() > 0
    ****$(window).on ‘scroll’, ->
    ******more_posts_url = $(‘#infinite-scrolling .next_page a’).attr(‘href’)
    ******if more_posts_url && $(window).scrollTop() > $(document).height() – $(window).height() – 60
    ********$(‘.pagination’).html(”)
    ********$.getScript more_posts_url, ->
    ******return
    **return

    just replace the * (star) with a space. the editor is cutting it away ;-)

  • mikbe

    There’s a pretty serious typo:
    > more_posts_url = $(‘.pagination .next_page a’).attr(‘href’)

    Should be:
    > more_posts_url = $(‘.pagination a.next_page’).attr(‘href’)

    • Ilya Bodrov

      Sorry, but why? In the demo .next_page is actually a class for the li that holds the a itself. a does not have next_page class. Did you use will_paginate or kaminari?

      • mikbe

        Because you are looking for a div of class .pagination with an anchor of class .next_page, not an anchor inside a div of class .next_page:

        … snip …
        a class=”next_page” rel=”next” href=”/recipes?page=2″ Next → /a

        The proof is that my line works and your’s does not. Oh, and I’m using will_paginate 3.x.

        I had to remove the less than / greater than for the comment to show because anchors are actually allowed apparently.

    • Hahns

      Hi, I recently implemented this infinite scrolling based on the guide provided above. However, the infinite scroll refused to work. Changing the ‘.next_page a’ to ‘a.next_page’ did the trick for me.

      Thanks for the tutorial nonetheless.

      God bless.

      • http://mikebethany.com/ Mike Bethany

        You’re welcome for the fix.

        Flying Spaghetti Monster bless.

  • dnd

    Why are you using getScript?

  • Ilya Bodrov

    Well, if you look at this demo http://sitepoint-infinite-scrolling.herokuapp.com/ you will see that a has no class at all. Maybe because will_paginate is a bit old there, but indeed the demo does work.

    Here is the relevant line:

    a rel=”next” href=”/posts?page=2″>Next →</a

    • mikbe

      Ah, OK. So different versions of Will_Paginate do it differently, something to keep an eye out for.

  • rmagnum2002

    Nice duplicate of Ryan Bates screencast.

  • Johnjay

    Getting the error below. Server console shows the right queries being executed etc and am getting no errors there. Anyone else have this issue?
    Refused to execute script from ‘http://localhost:3010/posts?page=2&_=1409793752478’ because its MIME type (‘text/html’) is not executable, and strict MIME type checking is enabled.

    • stumped

      im getting this error too. anybody able to solve it?

  • PSR

    Hey thanks for the post!

    I followed your steps using kaminari instead of will_paginate. In that case I have to use more_posts_url = $(‘.pagination .next a’).attr(‘href’) instead of more_posts_url = $(‘.pagination .next_page a’).attr(‘href’)

    I am able to find the next page link and when I add alert(more_posts_url) to the coffee script it shows me “localhost:3000/Superadmin?page=2”. That is good, however I don’t get a response from the server and when I do a browser inspect I see the following error:

    GET http://localhost:3000/Superadmin?page=2&_=1418554890254 500 (Internal Server Error)

    As you can see somehow “&_=1418554890254” is added to the link. I noticed this number is generated and changed when scrolling the page.. do you have any idea what it could be? And how I could prevent it?

    using ruby 2.1.1p76 rails 4.1.1

    Thanks!

  • blue

    I tried doing this on Rails 4; however, visiting root only gives me the html version? I was wondering if there was something else that needs to be done in order to have the js response that is wanted here? I currently have basically the copy/pasted version of the @post paginate/respond_to stuff in my index action.

  • Guest

    thank you ! works like a charm for me.

  • Rue

    Fix the indentation on line 5, matching it to line 2’s if.

  • Andrew

    how to make same shit in pure html/php (without rails)?

  • Szilard Magyar

    Hey Ilya. Great tut! What if user has no JS. I mean I want to make the will_paginate panel visible/usable only if the js doesn’t work. At the moment I count on js, so I make the will_paginate panel display:none default.

    • Ilya Bodrov

      Hello! It should work with JS disabled as well. Basically, if JS is disabled, user will see the basic pagination controls. You may check that at the demo page by disabling all JS in your browser. If something is not working for you, please let me know.

      • Szilard Magyar

        Hm. I made it display:none by default in html style tag and I have the code below. I made it cuz when I started scrolling the ugly will paginate panel showed up. This way looks nice when js is working, but I’d like to make sure if it would work with no js as well. Do you know a good way to do is? Having it invisible by default when js works and visible when it doesn’t?

        //infinite scrolling for tasks

        if ($(‘#infinite-task-scrolling’).size() > 0) {
        $(window).on(‘scroll’, function() {
        $(‘#infinite-task-scrolling’).hide();
        var more_tasks_url;
        more_tasks_url = $(‘.pagination .next_page a’).attr(‘href’);
        if (more_tasks_url && $(window).scrollTop() > $(document).height() – $(window).height() – 60) {
        $(‘.pagination’).html(”);
        $(‘#infinite-task-scrolling’).show();
        $.getScript(more_tasks_url);
        }
        });
        };

        • Ilya Bodrov

          A link to the project would be really helpful :)

  • gautamkathrotiya

    its a very helpful article…. !!!
    Thanks for sharing

  • Jai Kumar Rajput

    when I’m fetching records using ajax like adding filters etc, infinite
    scrolling stop working however at initial when I’m loading page its
    works well. I’m unable to get the point.

  • Ajdelaf

    Looks like I’m a little late to this party :P

    I’m trying to implement your infinite scroll method into my app. However, I’m paginating a with overflow-y: auto, rather than the entire page. Any thoughts on how I might go about doing that?

    • Ilya Bodrov

      We’re partying hard. Unfortunately, there are problems with notifications about comments, so I don’t always know when someone asks something. As long as I’ve written 35+ articles only on this site, it’s hard to keep track of all questions… Nvm.

      So, I beleive you might just take your element’s height and employ scrollHeight like this: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight

      Note the “Determine if an element has been totally scrolled”.

      • Ajdelaf

        Awesome, I’ll go take a look and give it a shot. Thank you so much!

      • Ajdelaf

        Update: For some reason I couldn’t get the clientHeight portion of that solution ^ to work, and instead used outerHeight. This is the snippet I ended up using:

        $(‘div’)[0].scrollHeight – $(‘div’).scrollTop() <= $('div').outerHeight() + 20

        Thanks for the guidance!

        • Ilya Bodrov

          Great to know you were able to fix that! Unfortunately, I have not tested that solution myself, that was more a like a blind shot :)

  • Rares Salcudean

    Nice article! I have a question, why when I scroll down why does the paginator and ajax-loader both appear. We want only the ajax-loader to appear. Thanks :)

    • Ilya Bodrov

      Well, not sure. Maybe you have some issue with JS? You may contact me via e-mail and share some code for me to look.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.