Published by Dan Cunning on Jun 16, 2014

Infinite Scrolling with AJAX

Ruby on Rails and jQuery team up to allow your visitors to scroll through all your content without initially loading them all.

Filed under Features

Introduction

Infinite scrolling appends more content to the end of the page before the viewer gets to it, so they can just keep scrolling unabated and the application doesn't need to initially load a bunch of records only a small percent of viewers will ever scroll down to.

Demo

The scrollable pane of movie posters automatically adds more rows of posters as you approach the bottom of the page.

This webpage is not hosted by Rails, so the above UX is just a simulation.

How it works

  • Rails renders the first page and a "View More" link during the initial HTML request.
  • The "View More" link is a UI fallback but also lets us know the URL for the next page.
  • Javascript watches the page's scrollbar to break a threshold.
  • An AJAX request is issued when either the threshold is broken or the user clicks the "View More" link.
  • Rails returns javascript that inserts the next page and points the "View More" link to the next unloaded page.

The Code

# Gemfile

gem 'kaminari'
# gem 'will_paginate' # another option
# controllers/posts_controller.rb
class PostsController < ApplicationController

  def index
    @posts = Post.order(published_at: :desc).page(params[:page]).per(25)
  end

end
<!-- views/posts/index.html.erb -->
<div id="content">
  <%= render(partial: 'post', collection: @posts) %>
</div>

<% unless @posts.current_page == @posts.total_pages %>
  <p id="view-more">
    <%= link_to('View More', url_for(page: @posts.current_page + 1), remote: true) %>
  </p>
<% end %>
<!-- views/posts/_post.html.erb -->
<%= div_for(post) do %>
  <h2><%= post.title %></h2>
  <p><%= post.excerpt %></p>
<% end %>
// views/posts/index.js.erb
$('#content').append("<%=j render(partial: 'post', collection: @posts, format: 'html') %>");

<% if @posts.current_page == @posts.total_pages %>
  $('#view-more').remove();
<% else %>
  $('#view-more a').attr('href', '<%= url_for(page: @posts.current_page + 1) %>');
<% end %>
# assets/javascripts/infinite-scroll.js.coffee
$ ->
  content = $('#content')    # where to load new content
  viewMore = $('#view-more') # tag containing the "View More" link

  isLoadingNextPage = false  # keep from loading two pages at once
  lastLoadAt = null          # when you loaded the last page
  minTimeBetweenPages = 5000 # milliseconds to wait between loading pages
  loadNextPageAt = 1000      # pixels above the bottom

  waitedLongEnoughBetweenPages = ->
    return lastLoadAt == null || new Date() - lastLoadAt > minTimeBetweenPages

  approachingBottomOfPage = ->
    return document.documentElement.clientHeight +
        $(document).scrollTop() < document.body.offsetHeight - loadNextPageAt

  nextPage = ->
    url = viewMore.find('a').attr('href')

    return if isLoadingNextPage || !url

    viewMore.addClass('loading')
    isLoadingNextPage = true
    lastLoadAt = new Date()

    $.ajax({
      url: url,
      method: 'GET',
      dataType: 'script',
      success: ->
        viewMore.removeClass('loading');
        isLoadingNextPage = false;
        lastLoadAt = new Date();
    })

  # watch the scrollbar
  $(window).scroll ->
    if approachingBottomOfPage() && waitedLongEnoughBetweenPages()
      nextPage()

  # failsafe in case the user gets to the bottom
  # without infinite scrolling taking affect.
  viewMore.find('a').click (e) ->
    nextPage()
    e.preventDefaults()

Wrap-Up

Simple enough? Rails returning javascript simplifies the client-side code significantly. Your HTML won't be exactly like mine, but the jQuery and Rails code should be straight-forward enough for you to tailor it to your own infinite scrolling page.