How To
This page provides practical tips and examples to help you to use Pagy efficiently.
Check the Choose Right Guide
- Fixed
- Pass the
:limitoption to the paginator to set the number of items to serve with each page. - Requestable
- Pass the
:limitoption combined with the:client_max_limitoption to the paginator, allowing the client to request a variable:limitup to the specified:client_max_limit. - Interactive
- Use the limit_tag_js helper to provide a UI selector to the user.
Pagy provides series_nav and series_nav_js helpers for displaying a pagination bar.
You can customize the number and position of page links in the navigation bar using:
- The :slots and :compact options.
- Overriding the
seriesmethod for full control over the pagination bar
:page
Pagy retrieves the page from the 'page' request params hash. To force a specific page number, pass it directly to the pagy method. For example:
@pagy, @records = pagy(:offset, collection, page: 3) # force page #3
You can customize the aria-label attributes of any *nav* helper by providing a :aria_label string.
Pass the :aria_label option to the helper.
You can also replace the pagy.aria_label.nav strings in the dictionary, as well as the pagy.aria_label.previous and the pagy.aria_label.next.
See ARIA.
By default, Pagy retrieves the page from the request params hash and generates URLs using the "page" key, e.g., ?page=3.
- Set
page_key: 'custom_page'to customize URL generation, e.g.,?custom_page=3. - Set the
:limit_keyto customize thelimitparam the same way.
See URL Options
Enable jsonapi: true, optionally providing :page_key and :limit_key:
# JSON:API nested query string: E.g.: ?page[number]=2&page[size]=100
@pagy, @records = pagy(:offset, collection, jsonapi: true, page_key: 'number', limit_key: 'size')
See the :querify Option
See the URL Options
Pass the :anchor_string option to the helper. It's especially useful for adding data-turbo-* or data-* Stimulus attributes.
Pagy includes different formats of stylesheets for customization, as well as styled nav tags for :bootstrap and :bulma.
You can also override the specific helper method.
The input_nav_js and limit_tag_js use inline style attributes. You can override these rules in your stylesheet files using the [style] attribute selector and !important. Below is an example of overriding the width of an input element:
.pagy input[style] {
width: 5rem !important; /* just an useless example */
}
- Identify the method file's path in the gem
libdir (e.g., 'pagy/...'). - Note the name of the module where it is defined (e.g.,
Pagy::...).
Copy and paste the original method in the Pagy Initializer
require 'pagy/...' # path to the overridden method file
module MyOverridingModule # wrap it with your arbitrarily named module
def any_method # Edit or define your method with the identical name
# Custom logic here...
super
# Custom logic here...
end
end
# prepend your module to the overridden module
Pagy::AnyModule.prepend MyOverridingModule
If you need assistance, ask in the Q&A discussions.
Simply pass it as the collection: pagy(:offset, my_array, **options)
Pagy works seamlessly with ActiveRecord collections, but certain collections may require specific handling:
For better performance of grouped counts, you may want to use the :count_over option
Do it in two steps:
@pagy, records = pagy(:offset, Post.all)
@decorated_records = records.decorate # or YourDecorator.method(records) whatever works
If the default pagy doesn't get the right count:
# pass the right count to pagy (that will directly use it skipping its own `collection.count(:all)`)
@pagy, @records = pagy(:offset, custom_scope, count: custom_count) # Example implementation
Ransack's result method returns an ActiveRecord collection that is ready for pagination:
q = Person.ransack(params[:q])
@pagy, @people = pagy(:offset, q.result)
Explore the following options:
- :keyset paginator
- headers_hash helper
:client_max_limitpaginator option:jsonapi optionpaginator option
You can send selected @pagy instance data to the client as JSON using the data_hash helper, including pagination metadata in your JSON response.
See these paginators:
Use the :calendar paginator for pagination filtering by calendar time units (e.g., year, quarter, month, week, day).
When you need to paginate multiple collections in a single request, you need to explicitly differentiate the pagination objects. Here are some common methods to achieve this:
By default, Pagy generates links using the same path as the request path. To generate links pointing to a different controller or path, explicitly pass the desired :path. For example:
def index
@pagy_foos, @foos = pagy(:offset, Foo.all, path: '/foos')
@pagy_bars, @bars = pagy(:offset, Bar.all, path: '/bars')
end
<%== @pagy_foos.series_nav %>
<%== @pagy_bars.series_nav %>
<!-- Pagination links of `/foos?page=2` instead of `/dashboard?page=2` -->
<!-- Pagination links of `/bars?page=2` etc. -->
If you're using hotwire (turbo-rails being the Rails implementation), another way of maintaining independent contexts is using separate turbo frames actions. Just wrap each independent context in a turbo_frame_tag and ensure a matching turbo_frame_tag is returned:
<-- movies/index.html.erb -->
<-- movies#bad_movies -->
<%= turbo_frame_tag "bad_movies", src: bad_movies_path do %>
<%= render "movies_table", locals: {movies: @movies}%>
<%== @pagy.series_nav %>
<% end %>
<-- movies#good_movies -->
<%= turbo_frame_tag "good_movies", src: good_movies_path do %>
<%= render "movies_table", locals: {movies: @movies}%>
<%== @pagy.series_nav %>
<% end %>
def good
@pagy, @movies = pagy(:offset, Movie.good, limit: 5)
end
def bad
@pagy, @movies = pagy(:offset, Movie.bad, limit: 5)
end
Consider Benito Serna's implementation of turbo-frames (on Rails) using search forms with the Ransack gem along with a corresponding demo app for a similar implementation of the above logic.
By default, pagy creates flat URLs for its links. If you need to handle multiple pagy instance in the same request, you can nest the :page and -if you use it- the :limit params by passing the :root_key option to the paginator:
def index
@pagy_stars, @stars = pagy(:offset, Star.all, root_key: 'stars')
@pagy_nebulae, @nebulae = pagy(:offset, Nebula.all, root_key: 'nebulae')
end
You can also paginate multiple model in the same request by simply using different :page_key for each instance:
def index
@pagy_stars, @stars = pagy(:offset, Star.all, page_key: 'pagy_stars')
@pagy_nebulae, @nebulae = pagy(:offset, Nebula.all, page_key: 'pagy_nebulae')
end
You may want to limit the availability of your records either for speeding up the DB queries (especially useful with OFFSET paginators with big tables), or simply to avoid exposing all your data to scrapers.
The best way to ensure it, is creating a limited collection using an ActiveRecord Virtual Table:
max_records = 10_000
collection = Product.where(...).limit(max_records) # Add the max_records limit to your collection
limited = collection.from(collection, :products) # Create a limited collection using the :products Virtual Table
@pagy, @records = pagy(:offset, limited, **options) # Paginate the limited collection
When your collection is already paginated and contains count and pagination metadata, you don't need any pagy* controller method.
For example this is a Tmdb API search result object, but you can apply the same principle to any other type of collection metadata:
#<Tmdb::Result page=1, total_pages=23, total_results=446, results=[#<Tmdb::Movie ..>,#<Tmdb::Movie...>,...]...>
As you can see, it contains the pagination metadata that you can use to set up the pagination with pagy:
# get the paginated collection
tobj = Tmdb::Search.movie("Harry Potter", page: params[:page])
# use its count and page to initialize the @pagy object
@pagy = Pagy::Offset.new(count: tobj.total_results, page: tobj.page, request: Pagy::Request.new(request))
# set the paginated collection records
@movies = tobj.results
Unlike other gems, Pagy does not decide for you that the nav of a single page of results must not be rendered. You may want it rendered... or maybe you don't. If you don't:
<%== @pagy.series_nav if @pagy.last > 1 %>
- Consider the paginators:
- Consider the helpers:
- When possible
Pagy outputs safe HTML, however being an agnostic pagination gem it does not use the specific html_safe rails helper for its output. That is noted by the Brakeman gem, that will raise a UnescapedOutputs warning.
Avoid the warning by adding it to the brakeman.ignore file. More details here and here.
Pagy::OptionError
It is a subclass of ArgumentError that offers information to rescue invalid options. For example: with rescue Pagy::OptionError => e you can get access to a few readers:
e.pagythe pagy objecte.optionthe offending option symbol (e.g.:page)e.valuethe value of the offending option (e.g.-3)
Pagy::RangeError
With the OFFSET pagination technique, it may happen that the users/clients paginate after the end of the collection (when one or a few records got deleted) and a user went to a stale page.
By default, Pagy doesn't raise any exceptions for requesting an out-of-range page. Instead, it does not retrieve any records and serves the navs as usual, so the user can visit a different page.
Sometimes you may want to take a different action, so you can set the option raise_range_error: true, rescue it and do whatever fits your app better. For example:
rescue_from Pagy::RangeError, with: :redirect_to_last_page
private
def redirect_to_last_page(exception)
redirect_to url_for(page: exception.pagy.last), notice: "Page ##{params[:page]} is out-of-range. Showing page #{exception.pagy.last} instead."
end
- Pagy has 100% test coverage.
- You only need to test pagy if you have overridden methods.
Warning!
The pagy nav helpers are not only a lot faster than templates, but accept dynamic arguments and comply with ARIA and I18n standards.
Using your own templates is possible, but it's likely just reinventing a slower wheel.
If you really need to use your own templates, you absolutely can. Notice, that since you are not using any helper, you should require the following files that provide internal method to use in the template:
require "pagy/toolbox/helpers/support/series"
require "pagy/toolbox/helpers/support/a_lambda"
Here is a static example that doesn't use any other helper nor dictionary file for the sake of simplicity, however, feel free to add your dynamic options and use any helper and dictionary entries as you need:
<%# IMPORTANT: replace '<%=' with '<%==' if you run this in rails %>
<%# The a variable below is set to a lambda that generates the a tag %>
<%# Usage: anchor_tag = a_lambda.(page_number, text, classes: nil, aria_label: nil) %>
<% a_lambda = pagy.send(:a_lambda) %>
<nav class="pagy nav" aria-label="Pages">
<%# Previous page link %>
<% if pagy.previous %>
<%= a_lambda.(pagy.previous, '<', aria_label: 'Previous') %>
<% else %>
<a role="link" aria-disabled="true" aria-label="Previous"><</a>
<% end %>
<%# Page links (series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]) %>
<% pagy.send(:series).each do |item| %>
<% if item.is_a?(Integer) %>
<%= a_lambda.(item) %>
<% elsif item.is_a?(String) %>
<a role="link" aria-disabled="true" aria-current="page"><%= item %></a>
<% elsif item == :gap %>
<a role="separator" aria-disabled="true">…</a>
<% end %>
<% end %>
<%# Next page link %>
<% if pagy.next %>
<%= a_lambda.(pagy.next, '>', aria_label: 'Next') %>
<% else %>
<a role="link" aria-disabled="true" aria-label="Next"><</a>
<% end %>
</nav>
You can use it as usual: just remember to pass the :pagy local set to the @pagy object:
<%== render file: 'nav.html.erb', locals: {pagy: @pagy} %>
You may want to look at the actual output interactively by running:
pagy demo
...and point your browser to http://127.0.0.1:8000/template
For non-rack environments that don't respond to the request method, you should pass the :request option to the paginator.
pagy outside controllers or views
The pagy method needs to set a few options that depend on the availability of the self.request method in the class/module where you included it.
For example, if you call the pagy method for a model (that included the Pagy::Method), it would almost certainly not have the request method available.
The simplest way to make it work is as follows:
include Pagy::Method
def self.paginated(view, my_arg1, my_arg2, **)
collection = ...
view.instance_eval { pagy(:offset, collection, **) }
end
<% pagy, records = YourModel.paginated(self, my_arg1, my_arg2, **options) %>
You may need to POST a very complex search form and paginate the results. Pagy produces nav tags with GET links, so here is a simple way of handling it.
You can start the process with your regular POST request and cache the filtering data on the server. Then, using the regular GET pagination cycle, pass only the cache key as a param (which avoids sending the actual filters back and forth).
Here is a conceptual example using the session:
require 'digest'
def filtered_action
pagy_options = {}
if params[:filter_key] # retrieve already cached filters
filters = session[params[:filter_key]]
else # store new filters
filters = params[:filters] # your filter hash
key = Digest::SHA1.hexdigest(filters.sort.to_json)
session[key] = filters
pagy_options[:querify] = ->(query_hash) { query_hash.merge!(filter_key: key) }
end
collection = Product.where(**filters)
@pagy, @records = pagy(:offset, collection, **pagy_options)
end
Notice: ensure a server-side storage
If you use the session for caching, configure it to use ActiveRecord, Redis, or any server-side storage
Feel free to ask for further help via Pagy Support.