In this post I describe how I add pagination to a blog using the Pagy gem.
Adding records to test the pagination functionality
Before adding the pagination functionality, I want to add a number of random blog posts so I can test out the implementation.
To add blog posts, I could use a library like Faker, a gem that populates models with fake data for development and testing.
But in my case, I don't need anything sophisticated, so I will just create random posts
in the database directly using the seeds.rb
file.
One thing to remember when seeding data is that running db:seeds
should be
idempotent, that is, it should always create the same data in the database, no
matter how many times the command is run.
With this in mind, instead of creating records with Article.create
, which
would add additional records every time I run db:seeds
again, I make sure that
records are created with Article.find_or_create_by
instead, which will create
records only if they don't exist.
# db/seeds.rb
100.times do |n|
Article.find_or_create_by(
title: "Article #{n}",
body: "This is my #{n}th article")
end
Adding the Pagy gem
There are several Ruby gems that handle pagination. Pagy is commonly used because it's faster, lighter, and simpler to use than other alternatives.
To add the library to my application I use the bundle add
command like so:
$ bundle add pagy
This will add the gem to the Gemfile
and run bundle install
.
With the gem installed, I can then open ApplicationController
in my text
editor, and include Pagy::Backend
, as explained in the Pagy instructions:
# app/controller/application_controller.rb
class ApplicationController < ActionController::Base
include Pagy::Backend
end
This will make Pagy available to all the controllers in the application,
because they all inherit from ApplicationController
.
In a similar way, I include Pagy::Frontend
to the application helper file so
Pagy helpers are made available:
# app/helpers/application_helper.rb
module ApplicationHelper
include Pagy::Frontend
end
The next step is to wrap article collections inside controller actions with Pagy.
Using Pagy inside actions
If I look at the articles controller, I can see that the index
action gets all
the articles from the database and assigns them to the @articles
collection.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
To enable the pagination, all I have to do is take this collection and pass it
to the pagy()
method. The method will return a Pagy
object loaded with
several methods used for the pagination itself, as well as the original article
collection.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
@pagy, @articles = pagy(@articles)
end
end
When this is done, I go to the articles' index
page view template and add the pagination links like so:
# app/views/articles/index.html.erb
<%== pagy_nav @pagy %>
Note the double equal syntax <%== ... %>
in the erb
tag. This version of the tag will render the html
links generated by pagy as raw html, without sanitizing them first. If we had used
the single equal version of the tag, the output of pagy_nav
would be sanitized, and
displayed on the page with escaped characters.
Since this html
is generated by Pagy, it's safe to display the unsanitized links.
With these changes, our pagination should work correctly, which we can verify if we look at the page.
Styling the pagination links
Our pagination links have default classes applied by Pagy and look OK for initial setup and testing, but they would need some more robust styling for production.
An advantage of Pagy is that it integrates with various CSS framework. Since I mosty use Tailwind CSS I will use this integration to style my links.
All I have to do is go to the Tailwind CSS integration page for Pagy and copy the example styles in that page.
I will paste these styles at the bottom of the app/assets/stylesheets/application.tailwind.css
file
and restart the server to see the changes.
/* app/assets/stylesheets/application.tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
/* existing code */
}
/* CSS classes provided by Pagy */
.pagy-nav,
.pagy-nav-js {
@apply flex space-x-2;
}
.pagy-nav .page a,
.pagy-nav .page.active,
.pagy-nav .page.prev.disabled,
.pagy-nav .page.next.disabled,
/* ... */
By adding the Tailwind styles I end up with the pagination navigation links looking much better, and of course I can always edit these styles to match the look of the application.
Displaying the pagination links conditionally
Right now, the pagination links will always show up at the bottom of the page, even if we only have one single blog article. It would be nice to remove the pagination links if the number of articles in the collection is less than the number of articles per page set up by Pagy.
It's easy enough to add this functionality by adding an if
condition and
display the links only when needed:
# app/views/articles/index.html.erb
<% if @pagy.counts > @pagy.items %>
<%== pagy_nav @pagy %>
<% end %>
With this code we take advantage of the methods provided by Pagy on the @pagy
object.
and will essentially see the pagination links only if there are
more than one page of results.
Rescuing errors
To keep track of our position in the pagination, Pagy adds parameters to the URL, similar to this:
http://10.0.0.10:3000/?page=2
If we try to access a pagination number that's too high our collection,
like /?page=2000
for example, Pagy will throw a Pagy::OverflowError
error,
like so:
Pagy::OverflowError in ArticlesController#index
expected :page in 1..6; got 2000
We can rescue this error and redirect the user to the first page of the blog in this way:
# app/controllers/articles_controller.rb
def index
@articles = Article.all
@pagy, @articles = pagy(@articles)
rescue Pagy::OverflowError
redirect_to articles_path(page: 1)
end
In the redirect we set the page
parameter to 1
, so the URL will reflect that
and will show the pages from the beginning.
Another way of achieving the same thing is to use retry
to re-run the index
action with parameters modified.
# app/controllers/articles_controller.rb
def index
@articles = Article.all
@pagy, @articles = pagy(@articles)
rescue Pagy::OverflowError
params[:page] = 1
retry
end
The code above will rescue the error by first modifying params[:page]
and then
re-running the index
method from the beginning.
Conclusion
In this article I have explained how I add pagination to a collection of articles on a blog by using the popular Pagy Ruby library.
Photo by Pixabay