We build Web & Mobile Applications.

< All Articles

Revisited: roll your own pagination links with will_paginate and Rails 3

UPDATE! will_paginate has moved on again since this blog was written so make sure you read all the way to the the end to save yourself getting angry when things don’t work as they should.

With a final release of Rails 3 edging closer every day it seems like a good time to revisit some of my old articles from the last few years and bring them up to date.

Back in the summer of 2008 I wrote about custom link renderers using will_paginate and, as it is still one of the most popular posts on the blog, it’s the one I’ve decided to refresh first. Don’t worry if you haven’t read the original article as I’ll be covering the same things here. So without further ado, let’s get stuck in!

Getting started

Of course before going any further you’ll need to have the Rails 3 release candidate installed. If you haven’t already done this I recommend using rvm to save polluting your existing Rails development environment and as you’d expect Ryan Bates has another of his wonderful RailsCasts to help you set it all up.

You’ll also need the Rails 3 version of will_paginate and as Rails 3 uses bundler to manage gem dependencies you won’t have to install it manually like you used to. Simply add the following to the Gemfile in your application’s root directory and then run the bundle install command to install the gem (along with any others required by your application):

gem "will_paginate", "~> 3.0.pre2"

The standard view helper

As with previous versions the standard view helper is creatively named will_paginate which 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 (I’ve broken it into multiple lines to make it more readable):

<div class="pagination">
  <span class="previous_page disabled">← Previous</span>
  <em>1</em>
  <a rel="next" href="/users?page=2">2</a>
  <a href="/users?page=3">3</a>
  <a href="/users?page=4">4</a>
  <a class="next_page" rel="next" href="/users?page=2">Next →</a>
</div>

As you can see the helper uses the fairly common markup of a <div> containing links to page numbers along with previous and next page links. The main difference to previous versions is the use of <em> elements for the current page instead of a <span>, so you’ll need to update your CSS if you’re intending re-using an existing stylesheet.

The helper accepts a hash of options for basic customisation:

:container
Determines whether or not the container <div> is included in the generated markup (defaults to true).
:class
Allows the CSS class of the container <div> to be specified (defaults to `pagination`).
:id
This new option allows an ID attribute to be specified for the container <div>. If set to true then an ID will be auto-generated (similar to the way form_for does) or set to a string to use your own ID.
:previous_label
:next_label
Customises the previous and next page link labels.
:page_links
Determines whether or not the individual page links are included in the generated markup (defaults to true). If false then only previous and next page links are generated.
:inner_window
:outer_window
Customises the number of links displayed at either side of the current page (:inner_window) and at the start and end of the page links (:outer_window). Defaults to 4 and 1 respectively.
:separator
Specifies the text inserted between the individual HTML elements generated by the helper (defaults to a single space).

You can also get away without passing a collection to the helper if it follows the naming convention of the controller. So in the above example the @users collection is named after the UsersController, so the helper can be used like this:

<%= will_paginate %>

Doing it your way

The standard view helper is fine, but often you’ll need to do your own thing. That’s when you need to use a custom link renderer, and this is where will_paginate has changed the most for Rails 3.

The WillPaginate::LinkRenderer class has been moved into the ViewHelpers module and split into two separate classes:

LinkRendererBase

The main method of interest in the LinkRendererBase class is windowed_page_numbers: it does the work of figuring out which page numbers should be shown using the :inner_window and :outer_window settings and is a replacement for the windowed_links and visible_page_numbers methods in previous versions of will_paginate. Unlike the methods it replaces, windowed_page_numbers doesn’t actually return any HTML, it returns a simple array of page numbers.

The other method to look at is pagination which calls windowed_page_numbers (if the :page_links option has been set) and then adds :previous_page and :next_page symbols to the start and end of the array. It is then up to the renderer to convert this array into HTML for display.

In most cases it is unlikely that you’ll be directly subclassing the LinkRendererBase class: although I’m sure there’s a good reason for a separate base class, it looks to me like an unnecessary abstraction.

LinkRenderer

The LinkRenderer class is a subclass of LinkRendererBase and as this is where the HTML generation takes place it is what you’ll subclass when creating your custom renderer. It provides the to_html method that is called by the will_paginate view helper and is responsible for converting the array of page links returned by the pagination method (from the base class) into HTML markup.

To work with will_paginate, all your renderer has to do is implement two methods: prepare and to_html, so we’ll take a look at them first.

The prepare method is passed the collection being paginated, a hash of options (from the will_paginate helper), and a reference to the template being rendered. By subclassing the LinkRenderer class the preparation of the renderer is taken care of for you, however if you need to do any additional setup in your custom renderer this is the place to put it, keeping in mind this method is called every time the will_paginate view helper is called.

The to_html method maps the page number array returned by the pagination method (inherited from the LinkRendererBase class) to HTML using the page_number method for the page number links and the previous_page and next_page methods (which in turn call the previous_or_next_page method) for the previous and next page links.

As you will see in the examples below the easiest way to create a custom link renderer is by overriding the pagination method to change the the contents of the array it returns. By default the LinkRenderer class handles values in the array like this:

Also note that there is no restriction on the order of values in the array, so you are free to mix integers and symbols however you see fit.

The actual HTML generation takes place in the link and tag methods which are responsible for generating <a> elements for active and <em> elements for inactive links.

These methods replace the page_link_or_span, page_link and page_span methods from previous versions of will_paginate. Another change from previous versions is that the initialize method is no longer used to set the gap marker (the HTML that is used to fill in the gaps in page numbers), instead you can override the protected gap method in your custom renderer to change the markup:

def gap
  tag(:li, "…", :class => "gap")
end

And now for an example

In addition to the methods described above there are a number of additional methods in the LinkRenderer class that can be overridden to generate the markup you require. To see them in action let’s copy from the original article on will_paginate and say we want to generate markup like this for our pagination:

<ul class="pagination">
  <li class="previous_page"><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_page"><a href="/users?page=3">Next →</a></li>
</ul>

Our custom link renderer will look like this (put the code in the lib folder of your application):

class PaginationListLinkRenderer < WillPaginate::ViewHelpers::LinkRenderer

  protected

    def page_number(page)
      unless page == current_page
        tag(:li, link(page, page, :rel => rel_value(page)))
      else
        tag(:li, page, :class => "current")
      end
    end

    def previous_or_next_page(page, text, classname)
      if page
        tag(:li, link(text, page), :class => classname)
      else
        tag(:li, text, :class => classname + ' disabled')
      end
    end

    def html_container(html)
      tag(:ul, html, container_attributes)
    end

end

The first thing you’ll probably notice is that there was no need to override the to_html method. In previous versions if you wanted to change the element used to contain the pagination links from a <div> to something else you had no choice, but the new html_container method means this is no longer the case. The page_number and previous_or_next_page methods also need to be overridden to wrap the page links in <li> tags. Compare this with the code for previous versions of will_paginate and you can see we have a lot less work to do now:

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

Using the custom renderer

As with previous versions of will_paginate, having created a custom renderer you need to tell the view helper 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.

A Rails 3 autoloading gotcha

If you try and use your custom link renderer at this point you’re likely to get the following exception:

uninitialized constant ActionView::CompiledTemplates::PaginationListLinkRenderer

This is because, unlike Rails 2.x, the current release candidate of Rails 3 doesn’t autoload code in the lib directory. There’s a lengthy and at times heated discussion of this over on the Rails bug tracker which has not yet been formally resolved. In the meantime you can restore the old Rails 2.x behaviour by changing the autoload paths in config/application.rb:

config.autoload_paths += %W(#{config.root}/lib)

Restart your server and you should be good to go.

How do I…?

So that covers the basics of creating your own link renderer. If you take a look at the FAQ on my original article you’ll see some people have asked how to achieve different kinds of pagination markup. To finish I’ll update my replies for use with Rails 3.

How can page number links be turned off completely?

Turning off page numbers is a simple case of passing :page_links => false to the will_paginate view helper which means all you get is previous and next page links.

How can first and last page links be added?

To add first and last page links five things need to be done:

  1. Override the container_attributes method to prevent custom options being output as HTML attributes for the pagination container.
  2. Override the pagination method to return an array of symbols and to not call the windowed_page_numbers method.
  3. Add a first_page method that handles the generation of HTML for the first page link.
  4. Add a last_page method that handles the generation of HTML for the last page link.
  5. Set new label options for the first and last page links.

Here’s the code:

class MinimalPaginationLinkRenderer < WillPaginate::ViewHelpers::LinkRenderer

  def container_attributes
    super.except(:first_label, :last_label)
  end

  protected

    def pagination
      [:first_page, :previous_page, :next_page, :last_page]
    end

    def first_page
      previous_or_next_page(current_page == 1 ? nil : 1, @options[:first_label], "first_page")
    end

    def last_page
      previous_or_next_page(current_page == total_pages ? nil : total_pages, @options[:last_label], "last_page")
    end

end

As you can see the renderer re-uses the previous_or_next_page method to render the first and last page links as it handles the generation of the correct markup for when the current page is the first or last page. To use this in your view use:

<%= will_paginate(:renderer => MinimalPaginationLinkRenderer, :first_label => "&#8676; First", :last_label => "Last &#8677;") %>

Alternatively you can put these options in an initializer using the WillPaginate::ViewHelpers.pagination_options hash.

How can the ‘previous’ link be put after the page numbers?

To change the order of links, for example to show the page numbers followed by the previous and next page links, is even more straightforward with the Rails 3 version of will_paginate. All you need to do is override the pagination method to change the order of elements in the array it returns, for example:

class ReorderedPaginationLinkRenderer < WillPaginate::ViewHelpers::LinkRenderer

  protected

    def pagination
      items = @options[:page_links] ? windowed_page_numbers : []
      items.push :previous_page, :next_page
    end

end

The only change here from the method in LinkRendererBase is that the :previous_page symbol is being pushed onto the end of the array instead of being added to the beginning. Similarly if you wanted the previous and next links to appear before the page numbers, just change the second line of the method to items.unshift :previous_page, :next_page. As always, either pass the :renderer option to the view helper or set the global option in an initializer to use this renderer.

FAQ

How can I render Gmail style pagination?

class GmailPaginationLinkRenderer < WillPaginate::ViewHelpers::LinkRenderer

  def container_attributes
    super.except(:first_label, :last_label, :summary_label)
  end

  protected

    def pagination
      items = []
      items << :first_page if current_page > 2
      items << :previous_page if current_page > 1
      items << :summary
      items << :next_page if current_page < total_pages
      items << :last_page if current_page < total_pages - 1
      items
    end

    def first_page
      previous_or_next_page(1, @options[:first_label], "first_page")
    end

    def last_page
      previous_or_next_page(total_pages, @options[:last_label], "last_page")
    end

    def summary
      tag(:span, @options[:summary_label] % [ @collection.offset + 1, @collection.offset + @collection.length, @collection.total_entries ], :class => "summary")
    end

end

To use it you’ll need to pass the extra options to the will_paginate helper:

<%= will_paginate(
  :renderer => GmailPaginationLinkRenderer,
  :first_label => "&#171; Newest",
  :previous_label => "&#8249; Newer",
  :next_label => "Older &#8250;",
  :last_label => "Oldest &#187;",
  :summary_label => "%d - %d of %d"
) %>

There’s a few things going on here:

And finally here’s an example of the HTML output by the renderer:

<div class="pagination">
  <a class="previous_page" rel="prev start" href="/users?page=1">&#8249; Newer</a>
  <span class="summary">31 - 60 of 100</span>
  <a class="next_page" rel="next" href="/users?page=3">Older &#8250;</a>
  <a class="last_page" href="/users?page=4">Oldest &#187;</a>
</div>

API changes

At the time I blogged about this, over a year ago now, the Rails 3 version of will_paginate was still in pre-release form and under development. As such the API has changed, and some of what I wrote no longer applies (I really will get around to yet another will_paginate blog one day, but sadly not today).

The class your custom renderers should inherit from is now WillPaginate::ActionView::LinkRenderer. This new class has been added to separate out Rails specific functionality from the parent WillPaginate::ViewHelpers::LinkRenderer class, so that other framework specific renderers (e.g. for Sinatra) can also be defined. So as an example, when defining a custom renderer class, your code should now look like this:

class PaginationListLinkRenderer < WillPaginate::ActionView::LinkRenderer

The :renderer option is now deprecated and cannot be use to set a default renderer: the reasons for this are explained in this pull request on GitHub. This means that in order to use a custom renderer it is now necessary to pass the :renderer option to every will_paginate call in your views, or of course you could write some kind of helper wrapper that does it for you.

It’s probably also worth noting the various label options are also now deprecated in favour of using the Rails i18n framework.

Updated on 07 February 2019
First published by Rob Anderton on 22 August 2010
© Rob Anderton 2019
"Revisited: roll your own pagination links with will_paginate and Rails 3" by Rob Anderton at TheWebFellas is licensed under a Creative Commons Attribution 4.0 International License.