We are all used to common content pagination, where you have tons of links pointing to other pages with more results. The main reason behind this is performance, after all, you’re not mad to render tons of records at once and slow down your application on each gigantic request. But then we come to a discussion about usability and how wouldn’t it be a better user experience to load more records asynchronously as the user scrolls down instead of requiring him to click a link and re-render the entire page, specially when it comes to things such as feeds, where people are already scrolling down a ton of content and wouldn’t like to hit any sort of ‘STOP’ sign on their scrolling.

After spending some time dealing with this on my side project, I came up with a nice and simple endless page scrolling solution with jQuery and Rails 3. I created an example application for a more practical view on our problem and hosted all its code at this Github repository for anyone to check out.

First off, the whole endless scrolling Javascript front-end is a courtesy of this plugin, although it lacks a huge improvement which I’ll talk about later, so case you want to use it, I recommend downloading my version instead.

On the back-end side of things, I decided it was better to to ditch pagination plugins such as will_paginate altogether and load records not based on an ‘offset’ parameter, but on a ‘last timestamp’ parameter, this way if the feed happens to update while you’re scrolling down you won’t get duplicated results on the bottom due to the database offset pushing older (and already rendered) records to the bottom, thus loading them again. Also, it’s important to somehow stop the endless scrolling when there are no more results to show, so the user doesn’t spam our server with useless requests. So we basically need to manage 3 things: appending new records to our already existent list through AJAX, update the ‘last timestamp’ parameter somewhere with each re-render and know when to stop asking for more.

Let’s get down to the code, starting with the JS:

$('ul').endlessScroll({
  fireOnce: true,
  fireDelay: 500,
	ceaseFire: function(){
	  return $('#infinite-scroll').length ? false : true;
	},
	callback: function(){
	  $.ajax({
		  url: '/posts',
		  data: {
			  last: $(this).attr('last')
		  },
		  dataType: 'script'
		});
	}
});

Assuming we have an UL with a fixed height, an overflow:auto and a 'last' attribute, this code will make requests to the specified URL on each scroll event (you can configure the scrolling distance with the bottomPixels property) and stop making them when we don't have an #infinite-scroll div. The original plugin would not check for this ceaseFire condition on EACH scrolling event, which is crucial here (or else, what's the point in having it?), that's why using my patched version is key. This is how our page should look like to be ready to receive the upcoming updates:

<% unless @posts.blank?%>
  <ul class='list' last="<%=@posts.to_a.last.created_at%>">
    <%=render :partial => "post", :collection => @posts%>
    <div id="infinite-scroll"></div>
  </ul>
<% end %>

Now we move to the back-end, where we need to respond with new records based on that 'last' timestamp attribute and remove the #infinite-scroll div case the record collection returns empty.

###### - Model (post.rb)
class Post < ActiveRecord::Base
	def self.feed(last)
		self.where("created_at < ? ", last).order('created_at desc').limit(5)
	end
end

###### - Controller (posts_controller.rb)
respond_to :html, :js
def index
	last = params[:last].blank? ? Time.now + 1.second : Time.parse(params[:last])
	@posts = Post.feed(last)
end

###### - View (index.js.erb)
<% unless @posts.blank? %>
	$('.endless_scroll_inner_wrap').append("<%=escape_javascript(render :partial => 'post', :collection => @posts)%>");
	$('ul').attr('last', '<%=@posts.to_a.last.created_at%>')
<% else %>
	$('#infinite-scroll').detach();
<% end %>

Notice how we append the post partial to an .endless_scroll_inner_wrap div instead of the original UL, that's because the endless-scroll plugin creates this new div wrapping our original div whenever we scroll said div, so we need to work around that as well. The model/controller logic is pretty dull, no need to further explain that, just make sure to always pass a future timestamp when there isn't a parameter yet so that latest (up to the second) updates will still show.

Well, that basically covers it, the idea behind this post is to be a quick tutorial, if you're a Rails beginner then I highly recommend you download the entire repository code and check it out (although I pasted most of it here already for explanation's sake). Be sure to give some feedback if you have a more robust solution, this was my first try on this subject and I'm sure more experienced developers out there must have better stuff to show :)

About these ads