Skip to content

Commit

Permalink
WIP: Better Calendar paginator and combo_nav_js 2
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Jan 22, 2025
1 parent e4879b5 commit 5143268
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 410 deletions.
10 changes: 5 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
The docs are written in [markdown format](https://en.wikipedia.org/wiki/Markdown) and [retype](https://retype.com/) converts it
into styled HTML.

The [retype configuration](https://retype.com/configuration/project/) is located in
the [`retype.yml` file](https://github.com/ddnexus/pagy/blob/master/retype.yml).
The [retype configuration](https://retype.com/configuration/project/) is located in the [
`retype.yml` file](https://github.com/ddnexus/pagy/blob/master/retype.yml).

The [publish-docs workflow](https://github.com/ddnexus/pagy/blob/master/.github/workflows/publish-docs.yml) builds and
publishes the documentation in the [`docs-site` branch](https://github.com/ddnexus/pagy/tree/docs-site) when its markdown changes
are pushed to the `master` branch.
The [publish-docs workflow](https://github.com/ddnexus/pagy/blob/master/.github/workflows/publish-docs.yml) builds and publishes
the documentation in the [`docs-site` branch](https://github.com/ddnexus/pagy/tree/docs-site) when its markdown changes are pushed
to the `master` branch.
2 changes: 1 addition & 1 deletion docs/api/calendar/units.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ already filtered by the `:period` so there are no records outside it.

### Time conversions

This classes can use the recommended `ActiveSupport::TimeWithZone` class or the ruby `Time` class for all their time calculations.
This classes can use the `ActiveSupport::TimeWithZone` class for all their time calculations.

Since they are meant to be used in the UI, they use the user/server local time in order to make sense for the UI. For that reason
their input (the `:period` variable) and output (the `from` and `to` accessors) are always local time.
Expand Down
14 changes: 14 additions & 0 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,3 +892,17 @@ pagy demo
!!!

You may want to read also the [Pagy::Frontend API documentation](api/frontend.md) for complete control over your templates.

## Use Pagy with a non-rack app

For non-rack environments that don't respond to the request method, pass the `:request` variable to the paginator with your request[:url_prefix] (i.e. everything that comes before the `?` in the complete url), and your request[:query_params] hash to be merged with the pagy params and form the complete url


```ruby
@pagy, @records = pagy_offset(collection, request: { url_prefix: 'https://my.domain.com/path/script',
query_params: { a: 'a', b: 'b'} })
```

That would compose urls of the pages like: `https://my.domain.com/path/script?a=a&b=b&page=3`.

Pagy rely also on the `params` method inside the app, which should be a hash of the params from the request. Define an alias or a method if your environment doesn't respond to it.
28 changes: 11 additions & 17 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,12 @@ if that makes sense for Pagy:
- If it makes sense, you should add the equivalent Pagy statement and remove the legacy statement(s).
- If it doesn't make sense, then just remove the legacy statement.

!!!primary Don't stress if you miss
Don't worry about missing something in this step: if anything won't work as before the next steps will fix it.
!!!primary Don't stress if you miss Don't worry about missing something in this step: if anything won't work as before the next
steps will fix it.
!!!

#### Preparation

- Download the pagy initializer: you will edit it during the process.
[!file](/gem/config/pagy.rb)
- Replace the legacy gem with `gem "pagy"` in the `Gemfile` and `bundle`, or install and require the gem if you don't use bundler.
- Ensure that the legacy gem will not get loaded anymore (or it could mask some old statement still in place and not converted)
- Add the `include Pagy::Backend` statement to the application controller.
Expand All @@ -62,13 +60,9 @@ Kaminari.configure do |config|
#config.left = 0
#config.right = 0
end

Pagy::DEFAULT[:limit] = 10
require 'pagy/extras/size' # Provide legacy support of old navbars like the above
Pagy::DEFAULT[:size] = [5, 4, 4, 5] # Array parsed by the extra above
```

Remove all the legacy settings of the old gem(s) and uncomment and edit the new settings in the `pagy.rb` initializer _(see
Remove all the legacy settings of the old gem(s) and uncomment and edit the new settings in the `pagy.rb` initializer _(see
[How to configure pagy](/quick-start.md#configure))_.

#### Cleanup the Models
Expand All @@ -80,30 +74,29 @@ kinds of statements scattered around in your models. You should remove them all
makes sense to Pagy, which of course _is absolutely not_ in the models.

For example, you may want to search for keywords like `per_page`, `per` and such, which are actually configuration settings. They
should either go into the `pagy.rb` initializer if they are global to the app, or into the specific `pagy` call in the controller
if they are specific to an action.
should be added to the specific pagintor call in the controller (e.g. `pagy_offset`).

If the app uses the `page` scope in some of its methods or scopes in some model, that should be removed (including removing the
argument used to pass the page number to the method/scope), leaving the rest of the scope in place. Search where the app uses the
already paginated scope in the controllers, and use the scope in a regular `pagy` statement. For example:
already paginated scope in the controllers, and use the scope in a paginator statement. For example:

```ruby Controller
#@records = Product.paginated_scope(params[:page])
@pagy, @records = pagy(Product.non_paginated_scope)
@pagy, @records = pagy_offset(Product.non_paginated_scope)
```

#### Search and replace in the Controllers

In the controllers, the occurrence of statements from legacy pagination should have a one-to-one relationship with the Pagy
pagination, so you should be able to go through each of them and convert them quite easily.

Search for keywords like `page` and `paginate` statements and use the `pagy` method instead. For example:
Search for keywords like `page` and `paginate` statements and use the `pagy_offset` paginator instead. For example:

```ruby Controller
#@records = Product.some_scope.page(params[:page])
#@records = Product.paginate(:page => params[:page])

@pagy, @records = pagy(Product.some_scope)
@pagy, @records = pagy_offset(Product.some_scope)

#@records = Product.some_scope.page(params[:page]).per(15)
#@records = Product.some_scope.page(params[:page]).per_page(15)
Expand Down Expand Up @@ -136,13 +129,14 @@ until there will be no exception.
## Fine tuning

If the app is working and displays the pagination, it's time to adjust Pagy as you need, but if the old pagination was using
custom elements (e.g. custom params, urls, links, html elements, etc.) it will likely not work without some possibly easy adjustment.
custom elements (e.g. custom params, urls, links, html elements, etc.) it will likely not work without some possibly easy
adjustment.

Please take a look at the topics in the [how-to](how-to.md) documentation: that should cover most of your custom needs.

### CSS

The css styling that you may have applied to the pagination elements may need some minor change. However if the app uses the
The css styling that you may have applied to the pagination elements may need some minor change. However, if the app uses the
pagination from bootstrap (or some other framework), the same CSSs should work seamlessly with the pagy nav helpers.

### I18n
Expand Down
19 changes: 15 additions & 4 deletions docs/prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Use pagy normally
Your app does not use a `Rack` based framework.
!!!

Use the [standalone extra](extras/standalone.md)
Read how to [Use Pagy with non-rack apps](how-to.md#use-pagy-with-a-non-rack-app).

+++ Irb

Expand All @@ -41,8 +41,19 @@ Use the [Pagy::Console](api/console.md)

## Supported collections

Out of the box pagy
supports `ActiveRecord::Relation`, [array](extras/array.md), [elasticsearch_rails](extras/elasticsearch_rails.md), [searchkick](extras/searchkick.md)
and [meilisearch](extras/meilisearch.md) collections.
Out of the box pagy supports:

### Offset pagination with:

- `ActiveRecord::Relation`
- [array](extras/array.md)
- [elasticsearch_rails](extras/elasticsearch_rails.md)
- [searchkick](extras/searchkick.md)
- [meilisearch](extras/meilisearch.md)

### Keyset and Keynav pagination with:

- `ActiveRecord::Relation`
- `Sequel::Dataset`

In order to paginate other collections, search for "paginate" in the search field above.
37 changes: 13 additions & 24 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,46 @@ icon: alert-24
!!!danger Don't Paginate Unordered PostgreSQL Collections!

```rb
@pagy, @records = pagy(unordered)
@pagy, @records = pagy_offset(unordered)

# behind the scenes, pagy selects the page of records with:
unordered.offset(pagy.offset).limit(pagy.limit)
```

!!!warning From the [PostgreSQL Documentation](https://www.postgresql.org/docs/16/queries-limit.html#:~:text=When%20using%20LIMIT,ORDER%20BY)
!!! warning

When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you
From the [PostgreSQL Documentation](https://www.postgresql.org/docs/16/queries-limit.html#:~:text=When%20using%20LIMIT,ORDER%20BY)

When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise, you
will get an unpredictable subset of the query's rows.

!!!

!!! success Ensure the PostgreSQL collection is ordered!

```rb
# results will be predictable with #order
ordered = unordered.order(:id)
@pagy, @records = pagy(ordered)
@pagy, @records = pagy_offset(ordered)
```

!!!

==- Invalid HTML

!!!danger Don't rely on ARIA default with multiple nav elements!

Pagy sets the `aria-label` attribute of its `nav` elements with the translated and pluralized `pagy.aria_label.nav` that finds in
the locale files. That would be (always) `"Page"/"Pages"` for the `en` locale.

Since the `nav` or `role="navigation"` elements of a HTML document are considered `landmark roles`, they
should be uniquely aria-identified in the page.
Since the `nav` or `role="navigation"` elements of a HTML document are considered `landmark roles`, they should be uniquely
aria-identified in the page.
!!!

!!!success Pass your own `aria_label` to each nav!

```erb
<%# Explicitly set the aria_label string %>
<%# Explicitly set the aria_label string %>
<%== pagy_nav(@pagy, aria_label: 'Search result pages') %>
```

Expand All @@ -69,27 +73,12 @@ pagy demo
# ...and point your browser at http://0.0.0.0:8000
```

!!!primary
In the specific `bootstrap` example you could override the default bootstrap `"pagination"` class by adding other classes with:
!!!primary In the specific `bootstrap` example you could override the default bootstrap `"pagination"` class by adding other
classes with:

```ruby
@pagy, @records = pagy_bootstrap_nav(collection, classes: 'pagination my-class')
```

!!!

==- Slow Last Page

There has been a single report of a slow last page using very big DB tables. It's a pure DB problem and it's not caused by
pagy or by any other ruby code ([#696](https://github.com/ddnexus/pagy/pull/696),
[#704](https://github.com/ddnexus/pagy/pull/704)), but a simple pagy override may avoid it:

```rb
## override pagy_get_items
def pagy_get_items(collection, pagy)
limit = pagy.last == pagy.page ? pagy.in : pagy.limit
collection.offset(pagy.offset).limit(limit)
end
```
!!!
===
28 changes: 21 additions & 7 deletions gem/config/pagy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,44 @@

# Pagy initializer file (9.3.3)

# IMPORTANT:
# Customizing the static and frozen Pagy::DEFAULT is not supported since version 10.0.0.
# Pass the variables to the constructor, or pass your own DEFAULT hash.
##### Pagy::DEFAULT #######################

# Customizing the static and frozen Pagy::DEFAULT is NOT SUPPORTED since version 10.0.0.
# Pass the variables to the constructor, or pass your own PAGY_DEFAULT hash.
# For example:
#
# PAGY_DEFAULT = { ... }
# pagy_offset(collection, **PAGY_DEFAULT, ...)

# Extras

# Size extra: Enable the Array type for the `:size` variable (e.g. `size: [1,4,4,1]`)
# See https://ddnexus.github.io/pagy/docs/extras/size
# require 'pagy/extras/size' # must be required before the other extras
##### Extras #######################

# The extras are almost all converted to autoloaded mixins, or integrated in the core code at zero-cost.
# You can use the methods that you need, and they will just work without the need of any explicit `require`.
#
# The only extras that are left (for different compelling reasons) are listed below:
# gearbox, i18n and size. They must be required in the initializer as usual.


# Gearbox extra: Automatically change the limit per page depending on the page number
# (e.g. `gearbox_limit: [15, 30, 60, 100]`
# See https://ddnexus.github.io/pagy/docs/extras/gearbox
# require 'pagy/extras/gearbox'


# I18n extra: uses the standard i18n gem which is ~18x slower using ~10x more memory
# than the default pagy internal i18n (see below)
# See https://ddnexus.github.io/pagy/docs/extras/i18n
# require 'pagy/extras/i18n'


# Size extra: Enable the Array type for the `:size` variable (e.g. `size: [1,4,4,1]`)
# See https://ddnexus.github.io/pagy/docs/extras/size
# require 'pagy/extras/size' # must be required before the other extras


##### Pagy::I18n configuration #######################

# Pagy internal I18n: ~18x faster using ~10x less memory than the i18n gem
# See https://ddnexus.github.io/pagy/docs/api/i18n
# Notice: No need to configure anything in this section if your app uses only "en"
Expand Down
25 changes: 16 additions & 9 deletions gem/lib/pagy/backend/paginators/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ def pagy_calendar(collection, conf)
pagy, results = send(conf[:pagy][:backend] || :pagy_offset, collection, **conf[:pagy])
[calendar, pagy, results]
end
end

# This method must be implemented by the application
def pagy_calendar_period(*)
raise NoMethodError, 'the pagy_calendar_period method must be implemented by the application ' \
'(see https://ddnexus.github.io/pagy/docs/extras/calendar/#pagy-calendar-period-collection)'
end
module CalendarOverride
def pagy_anchor(pagy, anchor_string: nil, **vars)
return super unless (counts = pagy.vars[:counts]) # Skip unless pagy_calendar_counts is defined

# This method must be implemented by the application
def pagy_calendar_filter(*)
raise NoMethodError, 'the pagy_calendar_filter method must be implemented by the application ' \
'(see https://ddnexus.github.io/pagy/docs/extras/calendar/#pagy-calendar-filter-collection-from-to)'
left, right = %(<a#{%( #{anchor_string}) if anchor_string} href="#{pagy_page_url(pagy, PAGE_TOKEN, **vars)}")
.split(PAGE_TOKEN, 2)
# Lambda used by all the helpers
lambda do |page, text = pagy.label(page: page), classes: nil, aria_label: nil|
count = counts[page - 1]
classes = classes ? "#{classes} empty-page" : 'empty-page' if count.zero?
info_key = count.zero? ? 'pagy.info.no_items' : 'pagy.info.single_page'
title = %( title="#{pagy_t(info_key, item_name: pagy_t('pagy.item_name', count:), count:)}")
%(#{left}#{page}#{right}#{title}#{
%( class="#{classes}") if classes}#{%( aria-label="#{aria_label}") if aria_label}>#{text}</a>)
end
end
end
Frontend.prepend CalendarOverride
end
20 changes: 4 additions & 16 deletions gem/lib/pagy/frontend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,11 @@ module Frontend
# Return a performance optimized lambda to generate the HTML anchor element (a tag)
# Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
def pagy_anchor(pagy, anchor_string: nil, **vars)
# Skip if pagy_calendar_counts is defined
if (counts = pagy.vars[:calendar_counts])
count_info = lambda do |page, classes|
count = counts[page - 1]
info_key = count.zero? ? 'pagy.info.no_items' : 'pagy.info.single_page'
classes = classes ? "#{classes} empty-page" : 'empty-page' if count.zero?
title = %( title="#{pagy_t(info_key, item_name: pagy_t('pagy.item_name', count:), count:)}")
[classes, title]
end
end
left, right =
%(<a#{%( #{anchor_string}) if anchor_string} href="#{pagy_page_url(pagy, PAGE_TOKEN, **vars)}").split(PAGE_TOKEN, 2)
# lambda used by all the helpers
left, right = %(<a#{%( #{anchor_string}) if anchor_string} href="#{pagy_page_url(pagy, PAGE_TOKEN, **vars)}")
.split(PAGE_TOKEN, 2)
# Lambda used by all the helpers
lambda do |page, text = pagy.label(page: page), classes: nil, aria_label: nil|
classes, title = count_info.(page, classes) if count_info
%(#{left}#{page}#{right}#{title}#{%( class="#{classes}") if classes}#{
%( aria-label="#{aria_label}") if aria_label}>#{text}</a>)
%(#{left}#{page}#{right}#{%( class="#{classes}") if classes}#{%( aria-label="#{aria_label}") if aria_label}>#{text}</a>)
end
end

Expand Down
2 changes: 1 addition & 1 deletion gem/lib/pagy/modules/url.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def escape(str)

# Return the URL for the page, relying on the params method and Rack by default.
# It supports all rack-based frameworks (Sinatra, Padrino, Rails, ...).
# For non-rack environments that don't respond to the request method, pass the :request variable to the constructor
# For non-rack environments that don't respond to the request method, pass the :request variable to the paginator
# with your request[:url_prefix] (i.e. everything that comes before the ? in the complete url),
# and your request[:query_params] hash to be merged with the pagy params and form the complete url
def pagy_page_url(pagy, page, absolute: false, fragment: nil, **)
Expand Down
2 changes: 1 addition & 1 deletion gem/lib/pagy/offset/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def init(conf, period, params)
conf[unit][:page] = @params[:"#{unit}_#{@page_sym}"] # requested page
# :nocov:
# simplecov doesn't need to fail block_given?
conf[unit][:calendar_counts] = yield(unit, conf[unit][:period]) if block_given?
conf[unit][:counts] = yield(unit, conf[unit][:period]) if block_given?
# :nocov:
calendar[unit] = object = Calendar.send(:create, unit, **conf[unit])
end
Expand Down
Loading

0 comments on commit 5143268

Please sign in to comment.