#51 will_paginate (revised)
- Download:
- source codeProject Files in Zip (85.8 KB)
- mp4Full Size H.264 Video (18.2 MB)
- m4vSmaller H.264 Video (9.89 MB)
- webmFull Size VP8 Video (9.21 MB)
- ogvFull Size Theora Video (25.5 MB)
Below is a screenshot from an application that displays a long list of products. To reduce the size of the page we want to add pagination to this page.
There are two gems to consider if you want to use pagination in a Rails application: will_paginate and Kaminari. At the end of this episode we’ll explain some of the differences between these two gems and give some tips to help you choose which one to use, but for now we’ll concentrate on will_paginate.
Adding will_paginate to Our Application
The will_paginate gem has been around for a few years and it had issues working with Rails 3 for a while. The new 3.0 release of will_paginate works well under Rails 3.0 and 3.1 so we can use it with our application. To install it we add a reference to it in the Gemfile
, making sure that we specify that at least version 3.0 is used, and then run bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.0' gem 'rack', '1.3.3' # temporarily to hide warning gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', " ~> 3.1.0" gem 'coffee-rails', "~> 3.1.0" gem 'uglifier' end gem 'jquery-rails' gem 'will_paginate', '> 3.0'
Now that we have will_paginate installed we can use it in our application. We want to add pagination to the ProductsController
’s index
action. With previous versions of will_paginate we did this by calling the paginate
method on the collection we wanted to paginate, like this:
class ProductsController < ApplicationController def index @products = Product.order("name").paginate end # Other actions omitted. end
This method is still available in Rails 3.0 but we’ll use the new page
method instead. We pass it the page number we want to display, in this case the page
parameter from the query string.
def index @products = Product.order("name").page(params[:page]) end
We add pagination to the view by calling will_paginate
, passing it the paginated collection of products.
<h1>Products</h1> <%= will_paginate @products %> <% for product in @products %> <div class="product"> <h2><%= link_to product.name, product %></h2> <div class="details"> <%= number_to_currency(product.price) %> | Released <%= product.released_at.strftime("%B %e, %Y") %> </div> </div> <% end %> <p><%= link_to "New Product", new_product_path %></p>
When we reload the page now we’ll have instant pagination.
Changing The Number of Items in The List
The list of items on each page is still fairly long as the default is to show 30 items per page. We can change this with the per_page
method. We’ll show five items per page so that we can see the effect more easily.
def index @products = Product.order("name").page(params[:page]).per_page(5) end
When we reload the page now only five items are shown.
Customizing The Pagination
Next we’ll look at customizing the way the pagination looks. The default look isn’t very pretty, but we can do a lot to improve it with CSS. The documentation has some nice examples of different pagination styles and we’ll choose one that looks like the pagination on digg.com. We’ll add its CSS to our application’s stylesheet.
.pagination { background: white; cursor: default; height: 22px; a, span, em { padding: 0.2em 0.5em; display: block; float: left; margin-right: 1px; } .disabled { color: #999999; border: 1px solid #dddddd; } .current { font-style: normal; font-weight: bold; background: #2e6ab1; color: white; border: 1px solid #2e6ab1; } a { text-decoration: none; color: #105cb6; border: 1px solid #9aafe5; &:hover, &:focus { color: #000033; border-color: #000033; } } .page_info { background: #2e6ab1; color: white; padding: 0.4em 0.6em; width: 22em; margin-bottom: 0.3em; text-align: center; b { color: #000033; background: #2e6ab1 + 60; padding: 0.1em 0.25em; } } }
All of the styles are nested under a pagination
class as will_paginate uses that class in the div
it wraps the pagination HTML in. When we reload our products page again we’ll see the improved pagination.
There’s more that we can do with CSS that we might think, such as hiding the “previous” and “next” links when we’re displaying the first or last page. We do this by modifying the disabled
class in the stylesheet so that disabled items are hidden.
.pagination { .disabled { display: none; } /* Rest of stylesheet omitted. */ }
If we reload the page now while we’re on the first page of products the “Previous” link will be hidden.
Rendering Custom Pagination
If we wanted to customize the pagination further there are various options that we can pass to the will_paginate
method. If, say, we wanted to change the “previous” label to a less than sign and the “next” label to a greater than sign we can do so like this:
<%= will_paginate @products, previous_label: h("<"), next_label: h(">") %>
Note that that values we supply for the labels aren’t HTML-escaped by default so we need to use the h
method here to make them safe. When we reload the page again and we’ll see the changes.
The RDoc documentation contains more information on the options we can pass in and there is enough flexibility in them to let us customize the way the pagination works in almost any way we could want. There is even a :renderer
option that lets us specify a class that can handle all of the rendering logic.
Sometimes, though, it can be easier to code the pagination directly into the view instead of using the will_paginate
method. Let’s say that we just want to have “previous” and “next” links with some information in between. It’s easy to create a link on the page, but how do we get the correct page parameters for the previous and next pages? One way to do this is to use params.merge
. Will_paginate provides methods on our collection called previous_page
and next_page
which will return the value for the previous or next page or nil
if there is none so we can use these methods to set the page
parameter for the links.
Between the links we want to show a “page x of y” message and we can do so by using of the current_page
and total_page
methods. When we put it all together our pagination code looks like this:
<% if @products.previous_page %> <%= link_to "< Previous", params.merge(:page => @products.previous_page ) %> <% end %> Page <%= @products.current_page %> of <%= @products.total_pages %> <% if @products.next_page %> <%= link_to "Next >", params.merge(:page => @products.next_page ) %> <% end %>
When we reload the page now we’ll see our custom pagination. We could now apply some CSS to make it prettier but the point is that this is all custom made without the use of the will_paginate
method and it works just how we want it to.
Comparing will_paginate with Kaminari
As you can see will_paginate
is a great gem for handling pagination. We haven’t covered all of its features here so you’re encouraged to check the README on the will_paginate’s home page and the wiki for more information.
How does will_paginate compare with Kaminari? We covered Kaminari back in episode 254 [watch, read] and functionally both gems are very similar. The differences appear when you begin to customize the pagination. Kaminari uses several partials to render the pagination and you override these to customize the way it looks and behaves. By comparison will_paginate handles customization by setting options on Ruby classes so choosing which gem to use depends on which technique you prefer. One thing to note is that rendering many partials has an effect on performance so Kaminari can be a bit slower than will_paginate but not so much slower that you’d notice for most applications.
A more significant factor in deciding which to use is that some Ruby gems depend on either will_paginate or Kaminari. ActiveAdmin, for example, depends on Kaminari. The two pagination gems don’t play well together, so if one of your application’s gems depends on either will_paginate or Kaminari then you’ll need to use that gem for all of your application’s pagination. These cases aside, you can’t go wrong with either pagination gem.