Calendar pagination with Pagy
Peviously I wrote about paginating records by date.
Gem pagy also offers a pagination solution out of the box:
Here’s how we can (and can’t) use it.
Initial setup #
First, let’s add a list of events that we can paginate:
# /db/seeds.rb
path = "https://raw.githubusercontent.com/ruby-conferences/ruby-conferences.github.io/master/_data/conferences.yml"
uri = URI.open(path)
yaml = YAML.load_file uri, permitted_classes: [Date]
yaml.each do |event|
Event.create!(
name: event["name"],
location: event["location"],
start_date: event["start_date"]
)
end
rails g scaffold Event name location start_date:datetime
rails db:migarte db:seed
Add calendar pagination #
# terminal
bundle add pagy
Enable pagy calendar plugin:
# config/initializers/pagy.rb
require 'pagy/extras/calendar'
# optionally enable frontend libraries
# require 'pagy/extras/bootstrap' # https://ddnexus.github.io/pagy/docs/extras/bootstrap/
# https://ddnexus.github.io/pagy/docs/extras/tailwind/
Pagy does not know what date
attribute we will use for pagination (created_at
? starts_at
? start_time
?), so we have to define it:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# enable pagy backend helpers globally
include Pagy::Backend
# start and end of calendar (first and last record in the list)
def pagy_calendar_period(collection)
collection.minmax.map(&:start_date)
# between first event and Today
# start_date = collection.min_by(&:start_date).start_date
# end_date = Time.zone.now
# [start_date, end_date]
end
# optionally: end on last event or today
# def end_date(collection)
# last_event_date = collection.max_by(&:start_date).start_date
# return last_event_date if last_event_date > Time.zone.now
# Time.zone.now
# end
# query to paginate within start_date
def pagy_calendar_filter(collection, from, to)
collection.where(start_date: from..to)
end
end
Enable pagy froentend helpers like pagy_nav
:
# app/helpers/application_helper.rb
module ApplicationHelper
include Pagy::Frontend
end
In the controller, wrap your collection into pagy_calendar
.
Uncomment for any pagination granularity that you like:
- year
- year/month
- year/week
- year/day (stupid)
- year/month/day
- ???
def index
collection = Event.all.order(start_date: :asc)
@calendar, @pagy, @events = pagy_calendar(collection,
year: {size: 4},
# year: { size: [1, 1, 1, 1] },
month: {size: 12, format: '%b'},
# month: { size: [0, 12, 12, 0], format: '%b' },
week: { size: 53, format: '%W' },
# week: { size: [0, 53, 53, 0], format: '%W' },
day: {size: 31, format: '%d'},
# day: { size: [0, 31, 31, 0], format: '%d' },
pagy: { items: 10 }, # items per page
active: !params[:skip]
)
end
ℹ️ size
attribute defines how many pagy links to show: [pagination start, before current, after current, pagination end]
. For example, if current selected page is 11
and size: [1, 2, 2, 1]
, the pagination links displayed can be [1, 9-10, 12-13, 100]
.
Display records (events) and pagination in a view:
# app/views/events/index.html.erb
<h1>Events</h1>
<div>
<% if params[:skip] %>
<%= link_to 'Show Calendar', events_path %>
<% else %>
<%= link_to 'Hide Calendar', events_path(skip: true) %>
<br>
<%= link_to 'Today', pagy_calendar_url_at(@calendar, Time.zone.now, fit_time: true) %>
<% end %>
</div>
<% if @calendar %>
<%== pagy_info(@pagy) %>
for
<%= @calendar.showtime %>
<%#== @calendar[:year].label %>
<%#== @calendar[:day]&.label %>
<%#== @calendar[:month].label(format: '%B %Y') %>
<%#== @calendar[:week].label %>
<%== pagy_nav(@calendar[:year]) %>
<%== pagy_nav(@calendar[:month]) %>
<%== pagy_nav(@calendar[:week]) if @calendar[:week] %>
<%== pagy_nav(@calendar[:day]) if @calendar[:day] %>
<% end %>
<%== pagy_nav(@pagy) %>
<hr>
<% if @calendar %>
<%#= link_to "New event", new_event_path(start_date: [@calendar[:day]&.label, @calendar[:month].label(format: '%m-%Y')].compact.join('-')) %>
<%= link_to "New event", new_event_path(start_date: @calendar.showtime) %>
<% else %>
<%= link_to "New event", new_event_path %>
<% end %>
<hr>
<% if @events.any? %>
<% @events.each do |event| %>
<%= render 'event', event: event %>
<% end %>
<% elsif @events.empty? %>
No events found
<% end %>
Add new event to current date #
In the view, add a link_to
add an event for a date.
@calendar.showtime
will always give you the current date/month/year.
# app/views/events/index.html.erb
# for current_date
link_to "New event", new_event_path(start_date: @calendar.showtime)
# for any date
link_to "Add event (Today)", new_event_path(start_date: Date.today)
# other approaches
# if no format defined in controller
link_to "Add event", new_event_path(start_date: @calendar[:day].label)
# if :day format is defined in controller, we have to deduce todays date
link_to "New event", new_event_path(start_date: [@calendar[:day]&.label, @calendar[:month].label(format: '%m-%Y')].compact.join('-'))
Display the selected date in a form:
# app/views/events/_form.html.erb
<% if params[:start_date] %>
<%= form.datetime_field :start_date, value: params[:start_date]&.to_date&.strftime('%Y-%m-%dT%H:%M:%S') || form.object.start_date %>
<% else %>
<%= form.datetime_field :start_date %>
<% end %>
To redirect to the calendar page with this event, we need to define @calendar
in the #create
action the same way we did for #index
.
# app/controllers/events_controller.rb
+ before_action :set_calendar, only: %i[ index create ]
def create
if @event.save
- redirect_to events_path
+ redirect_to helpers.pagy_calendar_url_at(@calendar, @event.start_date)
private
# this will be shared for both #index and #create actions
+ def set_calendar
+ # @events = Event.all
+ collection = Event.all.order(start_date: :asc)
+ @calendar, @pagy, @events = pagy_calendar(collection,
+ year: {size: 4},
+ month: {size: 12, format: '%b'},
+ day: {size: 31, format: '%d'},
+ pagy: { items: 10 }, # items per page
+ active: !params[:skip]
+ )
+ end
Wishes for the future #
If we could have actual year in params, not page index, it would make URLs predictable:
# bad
http://localhost:3000/events?year_page=10&month_page=10&day_page=5
# good
http://localhost:3000/events?year_page=2023&month_page=10&day_page=5
Overall, Pagy Calendar is a great out of the box solution.
Huge respect to ddnexus for his work! 💪
To explore later:
- Time zones
- i18n
Did you like this article? Did it save you some time?