We build Web & Mobile Applications.

< All Articles

Roll your own pagination links with will_paginate

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 standard view helper

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
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).
: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).
:separator
Specifies the text inserted between the individual HTML elements generated by the helper (defaults to a single space).

Doing it your way

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
Generates the individual page number links using a call to visible_page_numbers to determine the page numbers that should be included based on the inner and outer window options.
page_link_or_span
Used by windowed_links to generate the appropriate HTML for a page: this will be a link for all but the current page.
page_link
Generates a list item containing a link to a specific page.
page_span
Generates a list item containing text rather than a link.

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
The constructor initialises the 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">&hellip;</span>. The gap marker is not used in the example renderer above.
prepare
This method is called each time you use the 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!

FAQ

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.

Updated on 07 February 2019
First published by Rob Anderton on 03 August 2008
© Rob Anderton 2019
"Roll your own pagination links with will_paginate" by Rob Anderton at TheWebFellas is licensed under a Creative Commons Attribution 4.0 International License.