Ruby on Rails: paginate stateful tabs with Pagy

Customized-Pagy-Navigation-Examples

In a recent post on the Imaginary Cloud blog, Tiago talked about Pagy, the new highly performant kid on the block when it comes to pagination in Rails (or really any Rack based Ruby framework).

This follow up is a how to guide, using the built-in compact extra that offers a new kind of UI through the pagy_nav_bootstrap_compact helper, adapting it a little bit to our needs.

I'll use a small sample app with some test data generated with the Faker gem to demonstrate Pagy's abilities. You can see what we'll build here and inspect the code on GitHub.

Besides the default frontend helper pagy_nav, which will generate a very basic pagination, there are two others (compact, bootstrap), so if you want to use a different kind of pagination or go without Bootstrap, that's perfectly fine too.

Installation

First, add Pagy to your Gemfile and run bundle install:

gem 'pagy', '~> 0.8.1'

Add the application's data to the database:

rake db:seed

Create the pagy.rb initializer for the app:

# load the compact UI extra
require 'pagy/extras/compact'

# set the default items per page
Pagy::VARS[:items] = 10

Include the Pagy backend in a controller to make it available just there or inside ApplicationController to make it available globally:

include Pagy::Backend

Now you are set up to paginate you ActiveRecord collections inside controllers which include the Pagy backend, which would be all controllers that inherit from your ApplicationController if you included it there. Let's say you have an index action that shows some space nebulæ which you want to paginate, you could do it this way:

@pagy, @nebulae = pagy(Nebula.all)

Include the frontend in a helper or in ApplicationHelper to make it available globally:

include Pagy::Frontend

To render your paginated nebulæ, you can now use one of Pagys frontend helpers, like so:

<%== pagy_nav_bootstrap_compact @pagy %>

Customizing the pagination

By default, pagy_nav_bootstrap_compact will render something like this:

Pagy Nav Bootstrap Compact

You can adapt the text labels if you require 'pagy/extra/i18n' in an initializer, copy the standard Pagy dictionary to your config/locales and change or translate the labels however you want.

Via the initializer, you can also set options like the number of items per page and much more.

To adapt the styling we can use a bit of SASS:

.pagy-nav-bootstrap-compact
  @extend .justify-content-center
  position: relative
  margin: 60px 0 40px
  .prev,
  .next,
  .pagy-compact-input
    @extend .btn-outline-dark
  .prev,
  .next,
    font-family: FontAwesome
    font-size: 1.25rem
    line-height: 1.3rem
    padding-right: 1rem
    padding-left: 1rem
    &.disabled
      color: $gray-light
  .pagy-compact-input
    color: $body-color !important
    opacity: 1
    background: transparent !important
    input
      @extend .form-control
      display: inline
      font-weight: bold
      width: auto !important
      background: transparent

This will @extend Bootstraps btn-outline-dark style, replace the prev/next buttons text with arrow icons and style the input.

Paginate multiple collections at once

Sometimes it's necessary to paginate multiple collections per controller action.

In the following example I'll render two collections, Stars & Nebulae, on one page inside two Bootstrap tabs.

With Pagy it's easy to paginate and navigate each of them separately.

Inside the controller action where I define the Pagy collections, all that's needed is the addition of a page_param with a symbol of my choice to each collection:

@pagy_stars, @stars = pagy(Star.all, page_param: :page_stars)
@pagy_nebulae, @nebulae = pagy(Nebula.all, page_param: :page_nebulae)

Now Pagy will automatically prepend that param to the navigation links as well as extract them from a URL when rendering a view. Extremely easy and convenient!

Maintaining the state

To also maintain the state currently active tab, we can use Pagy's option to add arbitrary parameters:

@pagy_stars, @stars = pagy(Star.all, page_param: :page_stars, params: { active_tab: 'stars' })
@pagy_nebulae, @nebulae = pagy(Nebula.all, page_param: :page_nebulae, params: { active_tab: 'nebulae' })

This param can be used for Bootstraps tabs .nav-links and .tab-pane by setting their active state with a conditional:

<ul class="nav nav-tabs" role="tablist">
  <li class="nav-item" role="presentation">
    <a class="nav-link <%= 'active' unless params[:active_tab] == 'nebulae' %>" data-toggle="tab" href="#stars">
      Stars
    </a>
  </li>
  <li>
    <a class="nav-link <%= 'active' if params[:active_tab] == 'nebulae' %>" data-toggle="tab" href="#nebulae">
      Nebulae
    </a>
  </li>
</ul>

<div class="tab-content">
  <div class="tab-pane <%= 'active' unless params[:active_tab] == 'nebulae' %>" id="stars">
  …
</div>

We can now not only link to certain pages for each tab, but also maintain the currently open tab even when visitors copy and paste links to our website. Neat!

Conclusion

As I hope this article help to highlight, Pagy is not only very performant (see previous blogpost about Pagy) but also very comfortable to use and easy to customize.

Things like renaming the page parameter are in my opinion even more straightforward to do with Pagy then with will_paginate or Kaminari.

Time will tell if Pagy will become the go-to gem for Rails when it comes to pagination, I certainly think it has to potential to become just that.

At Imaginary Cloud, we simplify complex systems, delivering interfaces that users love. If you’ve enjoyed this article, you will certainly enjoy our newsletter, which may be subscribed below. Take this chance to also check our latest work and, if there is any project that you think we can help with, feel free to reach us. We look forward to hearing from you!