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