We build Web & Mobile Applications.
UPDATE! If you’re using Rails 3 then don’t go a step further: I’ve written an updated version of this article that covers the latest and greatest version of Rails and will_paginate.
Mislav’s will_paginate plugin (and Gem) has become the de facto standard for pagination in Rails, replacing the often derided classic pagination from the dark days before Rails 2.0. If you haven’t used will_paginate before then Ryan Bates’ RailsCast is a good introduction, although be warned that it is just over a year old and there have been a number of changes to the plugin in that time.
The plugin includes a helper, unsurprisingly named will_paginate
that makes it easy to add pagination links to your views. Here’s an example of its use:
# In the controller
@users = User.paginate(:page => params[:page])
# In the view
<%= will_paginate(@users) %>
This will produce the following HTML markup:
<div class="pagination"><span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="/users?page=2" rel="next">2</a> <a href="/users?page=3">3</a> <a href="/users?page=2" class="next_page" rel="next">Next »</a></div>
As you can see the helper uses the fairly common markup of a <div>
containing links or spans for the page numbers along with previous and next page links. The plugin includes a number of CSS examples that allow the markup to be styled like digg or Flickr (take a look here for more) and the helper accepts a hash of options that allow basic customisation.
:container
<div>
is included in the generated markup (defaults to true).:class
<div>
to be specified (defaults to pagination
).:previous_label
:next_label
:page_links
:inner_window
:outer_window
:inner_window
) and at the start and end of the page links (:outer_window
).:separator
The standard helper provides customisation through CSS styling and the options listed above, but what do you do if the UI designer on your project insists that the pagination markup must look like this?
<ul class="pagination">
<li class="previous"><a href="/users?page=1">« Previous</a></li>
<li><a href="/users?page=1">1</a></li>
<li class="current">2</li>
<li><a href="/users?page=3">3</a></li>
<li class="next"><a href="/users?page=3">Next »</a></li>
</ul>
You have a couple of options: either you beat the designer with a stick until they agree that the default markup is acceptable or, if like us you prefer a non-violent approach to web development, you can take advantage of will_paginate’s support for custom link renderers.
Here’s an example that will generate the above markup:
class PaginationListLinkRenderer < WillPaginate::LinkRenderer
def to_html
links = @options[:page_links] ? windowed_links : []
links.unshift(page_link_or_span(@collection.previous_page, 'previous', @options[:previous_label]))
links.push(page_link_or_span(@collection.next_page, 'next', @options[:next_label]))
html = links.join(@options[:separator])
@options[:container] ? @template.content_tag(:ul, html, html_attributes) : html
end
protected
def windowed_links
visible_page_numbers.map { |n| page_link_or_span(n, (n == current_page ? 'current' : nil)) }
end
def page_link_or_span(page, span_class, text = nil)
text ||= page.to_s
if page && page != current_page
page_link(page, text, :class => span_class)
else
page_span(page, text, :class => span_class)
end
end
def page_link(page, text, attributes = {})
@template.content_tag(:li, @template.link_to(text, url_for(page)), attributes)
end
def page_span(page, text, attributes = {})
@template.content_tag(:li, text, attributes)
end
end
As you can see the link renderer is a subclass of WillPaginate::LinkRenderer
the main method of which is to_html
: this is the method that is used by the view helper to generate pagination markup. In this example I’ve chosen to preserve support for all of the standard options (:container
, :previous_label
, etc.) however you do not have to do so in your own link renderers if you don’t want to. I’ve also chosen to override the four protected methods that are called upon by to_html
to actually perform the HTML generation:
windowed_links
visible_page_numbers
to determine the page numbers that should be included based on the inner and outer window options.page_link_or_span
windowed_links
to generate the appropriate HTML for a page: this will be a link for all but the current page.page_link
page_span
Of course you don’t have to override these methods if you don’t want to: you could choose to do all of your rendering in your custom to_html
method or you could choose to define your own protected methods for performing specific tasks, the choice is yours. In this example I chose to use the existing method names for two reasons: it makes it easier for somebody else to maintain the code as it follows the same naming as the standard link renderer, and I couldn’t think of any better names!
The WillPaginate::LinkRenderer
also has initialize
and prepare
methods that you can extend in your own renderer, although in most cases you won’t need to.
initialize
gap_marker
attribute used by the windowed_links
method: this is the HTML that is used to fill in the gaps in page numbers (caused by the inner and outer window settings) and defaults to <span class="gap">…</span>
. The gap marker is not used in the example renderer above.prepare
will_paginate
helper in your view and is passed the collection being paginated, the options hash and view template.If you need to do one-time initialisation when your renderer is created then override the initialize
method, and if you need to perform initialisation each time the renderer is called override prepare
.
Having created a custom renderer you need to tell will_paginate to use it. There are two ways to do this, the first is to pass the :renderer
option like this:
<%= will_paginate(@users, :renderer => PaginationListLinkRenderer) %>
The :renderer
option accepts a class name string, a class or an instance of a link renderer. If you want your custom link renderer to be the default for all pagination you don’t have to add a :renderer
option to all of your will_paginate
calls, instead you can specify the default in a Rails initializer like this:
WillPaginate::ViewHelpers.pagination_options[:renderer] = 'PaginationListLinkRenderer'
Your view can then contain <%= will_paginate(@users) %>
and your link renderer will be used automatically.
Custom link renderers are an excellent, if underused, feature of will_paginate that give you the freedom to markup your pagination links in whatever creative way you can come up with. Go forth and paginate!
Where should the
PaginationListLinkRenderer
class be placed in the application?
I usually put the link renderer in my lib
folder, for example in RAILS_ROOT/lib/pagination_list_link_renderer.rb
. I then create a plugins initializer in RAILS_ROOT/config/initializers/plugins.rb
that contains the WillPaginate configuration:
WillPaginate::ViewHelpers.pagination_options[:renderer] = 'PaginationListLinkRenderer'
Rails will then autoload the class because it is in the lib
folder.
How can page number links be turned off completely?
Page number links can be turned off without needing to do any custom coding: simply set the page_links
option to false in an initializer:
WillPaginate::ViewHelpers.pagination_options[:page_links] = false
How can first and last page links be added?
You’ll need to write your own custom link renderer, and do something like this in the to_html
method:
links.unshift(page_link_or_span(1, 'first', 'First'))
links.push(page_link_or_span(@collection.total_pages, 'last', 'Last'))
How can the ‘previous’ link be put after the page numbers?
You can create your own subclass of the default link renderer and override the to_html
method like this:
class CustomLinkRenderer < WillPaginate::LinkRenderer
def to_html
links = @options[:page_links] ? windowed_links : []
# previous/next buttons
links.push(page_link_or_span(@collection.previous_page, 'prev_page', @options[:previous_label]))
links.push(page_link_or_span(@collection.next_page, 'next_page', @options[:next_label]))
html = links.join(@options[:separator])
html = html.html_safe if html.respond_to?(:html_safe)
@options[:container] ? @template.content_tag(:div, html, html_attributes) : html
end
end
The only change here is that the previous link is being pushed onto the end of the links
array rather than being added to the start of the array.