Paged Scopes, Part II: Routing and Controller Methods

Posted 30 June 2009

Last week I described the basics of my Paged Scopes rubygem for ActiveRecord. In this article I’ll describe some routing and controller methods that make Paged Scope pagination very easy to use in your Rails controllers.

(It should be noted that the use of these methods is optional. If you don’t like them, you don’t have to use them! Design your routes and paginate your collections in your controllers however you see fit.)

Page Routing

The most common way to represent a paginated collection in an URL is to tack on the page number as a query parameter: http://www.example.com/articles?page=3, for example.

I’m not a fan of this approach at all. For starters, it’s a bit ugly. More importantly, it won’t work with standard Rails page caching, which ignores query parameters.

I prefer to think of pagination as just another scoping of the collection. Just as we have paths like /users/9/articles, I prefer a paged collection to have paths like /pages/2/articles (or /users/9/pages/2/articles, for that matter).

To this end, the Paged Scopes gem adds a :paged option to the Rails resources mapper. We’ll use this option to define the routes for our articles:

ActionController::Routing::Routes.draw do |map|
  map.resources :articles, :paged => true
end

Checking our routes using rake routes:

     articles GET    /articles(.:format)                {:controller=>"articles", :action=>"index"}
              POST   /articles(.:format)                {:controller=>"articles", :action=>"create"}
  new_article GET    /articles/new(.:format)            {:controller=>"articles", :action=>"new"}
 edit_article GET    /articles/:id/edit(.:format)       {:controller=>"articles", :action=>"edit"}
      article GET    /articles/:id(.:format)            {:controller=>"articles", :action=>"show"}
              PUT    /articles/:id(.:format)            {:controller=>"articles", :action=>"update"}
              DELETE /articles/:id(.:format)            {:controller=>"articles", :action=>"destroy"}
page_articles GET    /pages/:page_id/articles(.:format) {:controller=>"articles", :action=>"index"}

Just your standard set of resource routes, with one extra – the paged articles index route, last in the list. Specifying the :paged option in the mapping yields this extra route for use in our index actions. (Everything else remains the same.)

Want a bit more flexibility? We can pass :as or :name options to the paged option if needed:

map.resources :articles, :paged => { :as => :pagina }
map.resources :users, :paged => { :name => :group }

Which would produce these routes:

page_articles GET /pagina/:page_id/articles(.:format) {:controller=>"articles", :action=>"index"}
  group_users GET /groups/:group_id/users(.:format)   {:controller=>"users", :action=>"index"}

(This is likely only to be useful in rare situations. One example would be paginating more than one collection in a single view.)

Controller Methods

OK, so we have our pages represented in our article index route. Let’s turn to the articles controller next.

I believe there is diverging practice on this, but in controllers I always prefer to load the collection and object in before filters, typically along the lines of:

class ArticlesController < ApplicationController
  before_filter :get_articles
  before_filter :get_article, :only => [ :show, :edit, :update, :destroy ]
  before_filter :new_article, :only => [ :new, :create ]

  # actions here ...

  protected
  
  def get_articles
    @articles = @user.articles.scoped(:order => "created_at DESC") # or whatever
  end
  
  def get_article
    @article = @articles.find_from_param(params[:id])
  end
  
  def new_article
    @article = @articles.new(params[:article])
  end
end

It’s a very consistent way to write RESTful controllers. The @articles collection is always created, which is OK, since it’s just a scope or an association and no records are actually loaded. For the member actions, the collection instance is either loaded from the collection or built from it, depending on whether the action is creating a new record (new, create) or modifying an existing once (show, edit, update, destroy).

Using this pattern, paginating the collection fits naturally as another before filter once the collection is set. To this end, Paged Scopes provides a tailored paginate class method to do just that:

class ArticlesController < ApplicationController
  before_filter :get_articles
  before_filter :get_article, :only => [ :show, :edit, :update, :destroy ]
  before_filter :new_article, :only => [ :new, :create ]

  paginate :articles, :per_page => 3, :path => :page_articles_path

  ...

This paginate method basically adds another before_filter which loads the current page from the collection. As arguments, it takes an optional collection name and an options hash. If omitted, the collection name is inferred from the controller name. (Hence, in the above example, we could have omitted the :artices arguments and @articles would then be inferred from the ArticlesController name. Hurrah for naming conventions!)

You can pass a few options to the paginate method:

  • A :per_page option sets the page size on the collection if you specify it. (This option can be omitted if per_page has already been set on the collection.)
  • A :path option will set the path proc for the paginator to be the controller method you specify. The method should take a page and return the path. In the above example we’ve set it to a named route (page_articles_path), but it could equally well be a method you’ve defined later in the controller. (This could be useful if you want to use a polymorphic path, for example.)
  • A :name option is available if you want to refer to your pages by a different class name (unlikely).

Any other options will be passed through to the filter definition. So you can use filter options, such as :if, :only and :except, just as you would for any other filter.

Aside from setting the options you specify, the main job of the paginate filter is to set the page as an instance variable. Controller actions will then have a @page variable available to be used for pagination. The page number is determined from three locations in order of priority.

  1. If an object of the collection is present (an @article, in our example), the page containing that object is loaded (unless the object is a new record).
  2. Failing that, the request params are examine for a :page_id. If present, that page number is loaded. (This fits with the paged resource routes described earlier.)
  3. Failing that, the first page is loaded by default.

Loading the page for a member action (show, edit, update) might not seem useful at first. Its utility becomes apparent when we’re redirecting though:

def update
  if @article.save
    flash[:notice] = "Success!"
    redirect_to page_articles_path(@page)
  else
    ...
  end
end

The page is used to redirect to the index at the page containing the edited object. Very polite to users! (Views can also link back to the paged index in a similar manner.)

Next Time …

There you have it – a couple of methods that make paging your resources a no-brainer. But I’ve saved one of the best bits for last! In the final installment, I’ll describe the paginator, a super-convenient object for rendering windowed pagination links in your views.

Feed-icon-14x14 Comments