Behind-the-scenes of ‘Geared Pagination’ in Rails

Liroy Leshed
Sep 24 · 4 min read

There’s a better, more user-friendly-driven approach to writing pagination for your application. The idea is simple: Load more records every time the user is asking to see more.

So, for example, the first time the user is presented with 15 records. Then when the user clicks on the “next page” button, and asks to see even more: It doesn’t show 15 but instead 30 records. The next time the user asks to see even more, it shows 50 records. And then the next time 100 records.

In this post, we’ll take a look at the Geared Pagination gem which was written exceptionally well by David Heinemeier Hansson, and see how it really works behind-the-scenes.

The first thing it does after you add the gem to your Gemfile, is defining a concern on the controller you’d like to paginate. A concern is simply a module in Ruby that can be mixed into, or added, to any other module and class, and extend it with new methods.

This concern is added automatically to the application through the Rails engine. Pay attention to how ActionController calls send with the include method to include the GearedPagination::Controller in any controller that inherits from ActionController in your application:

class GearedPagination::Engine < ::Rails::Engine
initializer :geared_pagination do |app|
ActiveSupport.on_load :action_controller do
ActionController::Base.send :include, GearedPagination::Controller
end
end
end

Kickoff with GearedPagination::Controller

It all starts with the set_page_and_extract_portion_from method which initiates the flow. It receives a set of records, and you can specify how many records per page you’d like to present to the user:

module GearedPagination  module Controller
extend ActiveSupport::Concern
private
def set_page_and_extract_portion_from(records, per_page: nil)
# ... end endend

Once we have that set, we can use the method above in the controller we’d like to paginate. So for example, let’s say we have a Companies controller, and we have thousands of companies in the database. We would like to use pagination to present them to the user in the browser. We’ll use the method set_page_and_extract_portion_from and pass it a set of companies from the database. It now can be done like so:

class CompaniesController < ApplicationController  def index
set_page_and_extract_portion_from Company.all
end
end

Next, let’s go over the flow of what this method does and how it actually works. It calls current_page_from with the records set, and saves it in the @page instance variable. Then it calls records on @page to return the records:

module GearedPagination  module Controller
# ...
private
def set_page_and_extract_portion_from(records, per_page: nil)
@page = current_page_from(records, per_page: per_page)
@page.records
end
# ... endend

Let’s peak into the current_page_from method. It instantiates the GearedPagination::Recordset which again receives the records, and per-page information, meaning how many records we’d like to present per page to the user. Then it calls the page method on the set (instance of the Recordset) with the current page which it receives from the page parameter. For example, /companies?page=2 :

module GearedPagination  module Controller    # ...
private
def current_page_from(records, per_page: nil)
GearedPagination::Recordset
.new(records, per_page: per_page).page(current_page_param)
end
endend

GearedPagination::Recordset

The Recordset class is responsible for managing the set we’re presenting in one page in the application.

When the new instance of the GearedPagination::Recordset class is created, the constructor (the initialize method) will run first. It saves the records to @records, and creates a new instance of the GearedPagination::Ratios class and saves it to @ratios.

Next, the page method is called with the page number. In turn, it simply creates a new instance of the GearedPagination::Page class, which we’ll take a look shortly.

module GearedPagination
class Recordset
attr_reader :records, :ratios
def initialize(records, per_page: nil)
@records = records
@ratios = GearedPagination::Ratios.new(per_page)
end
def page(number)
GearedPagination::Page.new(number, from: self)
end
end
end

GearedPagination::Page

Let’s peak into the GearedPagination::Page class, which is responsible for managing everything that‘s related to the actual page.

The three most important things in this class are: @number, @recordset, and the @portion.

The constructor runs first when the class is instantiated and sets the three variables. Then it calls @page.records which gets the records from the portion class using the from instance method.

It also has some convience methods like first?, and next_number .

module GearedPagination
class Page
attr_reader :number, :recordset
def initialize(number, from:)
@number, @recordset = number, from
@portion = GearedPagination::Portion.new(page_number: number, per_page: from.ratios)
end
def records
@records ||= @portion.from(recordset.records)
end
def first?
number == 1
end
def next_number
number + 1
end
end
end

GearedPagination::Portion

Peaking into the from method: The Portion class gets the portion of records from the database using ActiveRecord’s limit method.

module GearedPagination
class Portion
attr_reader :page_number, :ratios
def initialize(page_number: 1,
per_page: GearedPagination::Ratios.new)
@page_number, @ratios = page_number, per_page
end
def from(scope)
scope.limit(limit).offset(offset)
end
end
end

GearedPagination::Ratios

Finally, we have the Ratios class which has some defaults in case you don’t feel like deciding on the ratio. It loads 15 records first, then 30, then 50, and finally 100 records from there on.

module GearedPagination
class Ratios
DEFAULTS = [ 15, 30, 50, 100 ]
def initialize(ratios = nil)
@ratios = Array(ratios || DEFAULTS)
end
def [](page_number)
@ratios[page_number - 1] || @ratios.last
end
end
end

I highly encourage you to open the source code, check it out for yourself, and play with it in your own application. Next, I plan to write about the tests for this gem, and expand about the whole concern concept that comes from ActiveSupport::Concern .

Thanks for reading!

Liroy Leshed

Written by

Founder & CEO @ http://squeezerhq.com

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade