Pagination in Rails from scratch (no gems)
Tags: rails, activerecord
Published: 2021-11-22 13:11:38

Pagination usually requires having to answer two main questions:

- **What will the selected page contain?**
   - How many items?
   - Which items?
- **How is the page related to the collection?**
   - What is the total number of pages for this collection?
   - Is there a page after? And before?

Whilst the first will return the right subset of items back to the user, the latter will provide the frontend with the information needed to build the pagination controls. There are different types of controls you can build depending on your goals and UX preferences. In this example I will stick with a classic, something like this:

![pagination controls](https://blogpublicbucket.s3.eu-central-1.amazonaws.com/pagination_controls.jpeg)

If you're having doubts about which controls to go with, I recommend [this article](https://gist.github.com/mislav/622561). 

So without further ado, let us write some code:

## 1. Creating a pagination mixin

### Why a mixin?

When you're building an application you will probably need pagination in more than one endpoint. To avoid repeating the same implementation in different controllers we can make use of mixins. In practice, this means creating a `Pagination` module that can be included in different classes across your application.

### The `Pagination` module

Following the pattern of most of the pagination gems, this `Pagination` module will consist of one public method: a `paginate` method that receives a collection and params. In our case, that collection will be an ActiveRecord collection. The params is a hash that will hold some of the current page attributes needed to paginate the collection. For instance, `params` will need to include `page` (the current page) and `per_page` (items that each page will retrieve from the collection).

```ruby
module Pagination
  def paginate(collection:, params: {})
    pagination = Services::Pagination.new(collection, params)

    [
	  pagination.metadata,
	  pagination.results
    ]
  end
end
```

This module will be responsible for returning two sets of information: the page items (`pagination.results`) and the metadata that will support the pagination controls (`pagination.metadata`). 

How do we get this data? We do not know yet, let's hold it for a while. What we know for now is that we will have to return this information to the controller. These are the answers to the questions we have raised before, in the intro of this article. 

Now we can include this module in whatever class that needs pagination. In our case, that will be the controller:

```ruby
class PostsController < ApplicationController
  include Pagination
  
  POSTS_PER_PAGE = 6

  def index
    @pagination, @posts = paginate(collection: Post.all, params: page_params)
  end

  private

  def page_params
    params.permit(:page).merge(per_page: POSTS_PER_PAGE)
  end
end
```

The collection will be all the posts, and the params will hold the `page` selected by the user and what we have defined to be the limit of each page (the `POSTS_PER_PAGE`  constant).

Note that since the pagination module returns an array, you can 'unpack' that array and assign its values to multiple variables in one line. The first variable will hold the first value of the array and the second variable will hold the second value. You can learn more about this technique [here](https://www.geeksforgeeks.org/parallel-assignment-in-ruby/).

## 2. Writing the pagination logic

Ok, so let us understand what is behind the magic of our module. The module calls a pagination service that will be responsible for getting the page items based on two values: items per page and the number that sets the start of each page. For an ActiveRecord collection that can be achieved with two methods only: `limit` and `offset`. As long as you know the values that these two methods receive - that is all you need to paginate. 

```ruby
module Services
  class Pagination
    attr_reader :collection, :params

    def initialize(collection, params = {})
      @collection = collection
      @params = params.merge(count: collection.size)
    end

    def metadata
      @metadata ||= ViewModel::Pagination.new(params)
    end

    def results
      collection
        .limit(metadata.per_page)
        .offset(metadata.offset)
    end
  end
end
```

But how can we calculate these values? 

`limit` **limits the returned results to a maximum number**. In our case, all pages should have 6 items each (the last page can have less, depending on the total size of the collection). So 6 is our `limit`. Remember this was defined as constant in the controller - `POSTS_PER_PAGE` - and passed as a param to our pagination service. 

But what about `offset`? The offset will help you define the boundaries of your page. **It skips over a number of posts when returning results**.  If your offset is 12 it will skip the first 12 posts and return all resulting records after that. When combined with `limit` it allows us to see pages beyond the first page. So if the `offset` is 12 and `limit` is 6, we will return 6 results after skipping the first 12. That will be page 3. 

But these values are calculated in a separate class that will be solely responsible for building all the metadata needed for the pagination and for the frontend. This leads us to the final part of our backend:

### Preparing the pagination metadata

```ruby
module ViewModel
  class Pagination
    DEFAULT = { page: 1, per_page: 6 }.freeze

    attr_reader :page, :count, :per_page

    def initialize(params = {})
      @page     = (params[:page] || DEFAULT[:page]).to_i
      @count    = params[:count]
      @per_page = params[:per_page] || DEFAULT[:per_page]
    end

    def offset
      return 0 if page == 1

      per_page * (page.to_i - 1)
    end

    def next_page
      page + 1 unless last_page?
    end

    def next_page?
      page < total_pages
    end

    def previous_page
      page - 1 unless first_page?
    end

    def previous_page?
      page > 1
    end

    def last_page?
      page == total_pages
    end

    def first_page?
      page == 1
    end

    def total_pages
      (count / per_page.to_f).ceil
    end
  end
end
```

Besides providing our pagination service with the `limit` and `offset` values that we saw before, this class will work as a frontend helper as most of the methods in this class will hold the information needed to build the pagination controls. 

Going back to our module, remember that the first element of the returned array will be an instance of this class. It will later be assigned to an instance variable `@pagination` in our `PostsController` and made available for the frontend views to consult. 

## 3. Building the frontend component

At the bottom of your collection view, in this case `posts/index.html.erb` you can now add the pagination controls. It might be a good idea to isolate them in a component:

```erb
<%# posts/index.html.erb %>

<%= render partial: 'shared/pagination', locals: { pagination: @pagination } %>
```

```erb
<%# _pagination.html.erb %>

<div>
  <% if  pagination.previous_page? %>
    <%= link_to 'First', posts_path(page: 1) %>
    <%= link_to '< Previous', posts_path(page: pagination.previous_page) %>
  <% else %>
    <p>First</p>
    <p>&#60; Previous</p>
  <% end %>
  <p><%= "Page #{pagination.page} of #{pagination.total_pages}" %></p>
  <% if pagination.next_page? %>
    <%= link_to 'Next >', posts_path(page: pagination.next_page) %>
    <%= link_to 'Last', posts_path(page: pagination.total_pages) %>
  <% else %>
    <p>Next &#62;</p>
    <p>Last </p>
  <% end %>
</div>

```

So there you go, you can now access the `ViewModel::Pagination` methods like `previous_page`, `next_page`, or `total_pages` and direct the links to the right pages. Now go ahead and add the CSS styling as you please. 

Have fun paginating! 📖