Unobtrusive Pageless Pagination in Rails

At a glance
This entry was written on March 19, 2007.
The entry prior to this is entitled The trunk for the branches.
The entry following this is entitled Widgetize your world.
There are 0 comments on this post.
This entry has been tagged as AJAX, Recommended, Work, examples, javascript, mediatemple, pageless, plugins, rails.
Archives are also available.

Inspired a lot by a keen dislike of pagination and more than a little by Unspace’s starting point, I started tinkering with this idea of pageless pagination for a current project on my plate at work.

Basically, rather than using the usual previous/next links to navigate through a large dataset, the design scheme loads more information into a list as you scroll down a page using a bit of Javascript wizardry to determine where you happen to be on a page and a little more javascript/AJAX on the backend to supply the data as you need it.

The problem, of course, is that you’re dealing with a lot of javascript-based “ifs” there.

If the user has javascript available. If the user has javascript enabled (for instance, I always turn JS off before visiting espn.com). And so on.

So, step one would be to make sure that non-JS users get their old-school, click-for-next-page pagination while giving the 98-percent out there with JS enabled the slick all-you-can-eat scroll.

Also, working from Unspace’s original example code, I didn’t like that it was so specifically tied-in with that particular batch of results. In the project I’m working on, I have several different listing modules and pages that all have similar, but not identical, markup. I didn’t want to rely on a hard-coded javascript file to determine which page element got filled in.

I also didn’t want to have five different JS files for five different pages … enter RJS (See ActionView#base, look under JavascriptGenerator toward the bottom).

The end result is a perfectly useable pageless pagination scheme with a baked-in fallback for users without javascript. You can view the demo at dummied.org/pageless.

Just scroll down and watch every entry I’ve written here fill in as you go. Then turn off javascript and you’re back to old-school paged pagination mode once again.

The key differences between the Unspace approach and this one include loading the initial data set using the base HTML call/page instead of using AJAX to fill a blank div. Because I haven’t had the time to unhook it, the page does load the second batch of results on page load and appends them, however.Apparently, I fixed this and didn’t even realize it. Since I’m starting with the results (and navigation) on the page from the very beginning, I also added a bit of JS to hide the two navigation bars; if you disable JS, they should pop right in there accordingly.

On the Rails end of things, the controller’s pretty simple thanks to respond_to

 def pageless
  if params[:page]
   @items = Post.find(:all, :page => {:size => 5, :current => params[:page].to_i})
  else
   @items = Post.find(:all, :page => {:size => 5, :current => 1})
  end
  respond_to do |format|
   format.html { render :layout => 'pageless' } # pageless.rhtml
   format.js { render :action => 'filler', :layout => false } #filler.rjs
  end
 end

I’m using the paginating_find plugin to handle the actual pagination of results here, but you could probably get something very similar using Rails’ baked-in pagination helpers.

Essentially, what we’re doing here is loading posts five at a time, with an offset determined by the :page param (?page=X). If it’s an HTML request, we render the default view for the action (pageless.rhtml). If it’s an AJAX/JS request, we render the filler action, which has only one template: filler.rjs. In an ideal world, format.js would automatically render a template called pageless.rjs, but Rails apparently isn’t set up that way (it renders pageless.rhtml instead). Hence, the specified and renamed view name.

pageless.rhtml treats the page as a normal paginated page, with navigation at the top and bottom and an id’d element wrapping a partial in the middle. There is also this chunk of code:

<%= hidden_field_tag :items_index, "" %>
<%= hidden_field_tag :total_pages, @items.page_count %>
<%= hidden_field_tag :page_id, Time.now.to_i %>
<div style="display:none;" id="more_loading"><strong>More items are being loaded...</strong><br />If you are using the scroll bar, release your mouse to see more.</div>
<div style="display:none;" id="page_end"></div>
<div style="display:none;" id="no_results"></strong></div>
<div style="position:absolute;top:0px;left:0px;visibility:hidden;" id="spacer">&nsbp;</div>

That chunk is used by Unspace’s original JS to determine which page to load and which error message to display. I figured if it wasn’t broke, why fix it.

Meanwhile, filler.rjs looks like this:

page.insert_html :bottom, 'items_list', {:partial => 'posts/pagelessitem', :collection => @items}

As you can see, all we’re doing is rendering a partial (the same one we used for pageless.rhtml) and appending the results to the bottom of our items_list element.

The great thing about using RJS to handle the filling instead of the hard-coded javascript file is that we gain flexibility. I can have a different filler.rjs for different controllers or different .rjs files for different actions within the same controller. As long as I have an id-able element to fill and a partial to render, I’m able to pageless paginate any dataset I want without touching the javascript file.

You can grab the revised Unspace javascript here. There’s still a little cookie code left in there for now until I have time to determine if I can take it out. The whole house of cards requires Prototype 1.5+, by the way (to handle the hiding of the navigation bars using the CSS selector).

Also, all the relevant files (minus Prototype and paginating_find) are available in a zipped format.

I also plan on packing this up into plugin form at some point, if only for our internal use (although I could be convinced to release it into the wild if there any sort of demand for it).

If anything looks a little funky or out-of-whack it might be because I had to add this directly to SimpleLog, the Rails blog engine that runs this site, since I couldn’t get a second app running on my Media Temple RoR container (any suggestions?).

If you find any problems or errors or have any questions, let me know.

Post a comment