Skip to content

Commit

Permalink
added countless_minimal feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Aug 20, 2021
1 parent ed6bc0b commit 5103ee9
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 40 deletions.
6 changes: 3 additions & 3 deletions docs/api/countless.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ The `pagy_info` and all the `*_combo_nav_js` helpers that use the total `count`

Instead of basing all the internal calculations on the `:count` variable (passed with the constructor), this class uses the number of actually retrieved items to deduce the pagination variables.

The retrieved items number is passed in a second step with the `finalize` method, and it allows to determine if there is a `next` page, or if the current page is the `last` page, or if the current request should raise a `Pagy::OverflowError` exception.

The trick is retrieving `items + 1`, and using the resulting number to calculate the variables, while eventually removing the extra item from the result. (see the [countless.rb extra](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/countless.rb))
The retrieved items number can be passed in a second step to the `finalize` method, wich allows to determine if there is a `next` page, or if the current page is the `last` page, or if the current request should raise a `Pagy::OverflowError` exception.
Retrieving these variables may be useful to supply a UI as complete as possible, when used with classic helpers, and can be skipped when it's not needed (like for navless pagination, infinite-scroll, etc.). See the [countless.rb extra](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/countless.rb) for more details.

## Methods

Expand Down
51 changes: 41 additions & 10 deletions docs/extras/countless.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ title: Countless
---
# Countless Extra

This extra uses the `Pagy::Countless` subclass in order to avoid to execute an otherwise needed count query. It is especially useful when used with large DB tables, where [Caching the count](../how-to.md#caching-the-count) may not be an option.

Its usage is practically the same as the regular `Pagy::Backend` module (see the [backend doc](../api/backend.md)).

The pagination resulting from this extra has some limitation as documented in the [Pagy::Countless Caveats doc](../api/countless.md#caveats).
This extra uses the `Pagy::Countless` subclass in order to save one count query per request. It is especially useful when used with large DB tables, where [Caching the count](../how-to.md#caching-the-count) may not be an option, or when there is no need to have a classic UI. Please read also the [Pagy::Countless doc](../api/countless.md) for a full understanding of its features and limitations.

## Synopsis

Expand All @@ -17,13 +13,51 @@ In the `pagy.rb` initializer:

```ruby
require 'pagy/extras/countless'
# optionally enable the minimal mode by default
# Pagy::VARS[:countless_minimal] = true
```

In a controller:

```ruby
@pagy, @records = pagy_countless(some_scope, ...)
# default mode (eager loading)
@pagy, @records = pagy_countless(some_scope, ...)

# OR
# enable minimal mode for this instance (lazy loading)
@pagy, @records = pagy_countless(some_scope, countless_minimal: true, ...)
```

## Modes
This extra can be used in two different modes by enabling or not the `:countless_minimal` variable.

### Default mode

This is the preferred automatic way to save one query per request, while keep using the classic pagination UI helpers.

By default this extra will try to finalize the `pagy` object with all the available variables in a countless pagination. It will do so by retrieving `items + 1`, and using the resulting number to calculate the variables, while eventually removing the extra item from the result.

That means:

- The `pagy` object will know whether the current page is the last one or there will be a next page so you can use it right away with any supported helper
- The returned paginated collection (`@records`) will be an `Array` instead of a scope (so the records are already eager-loaded from the DB)

### Minimal mode

This is the preferred mode used to implement navless and automatic incremental/infinite-scroll pagination, where there is no need to use any UI.

If you enable the `:countless_minimal` variable, then:

- The returned `pagy` object will contain just a handful of variables and will miss the finalization, so you cannnot use it with any helpers
- The returned paginated collection (`@records`) will be a regular scope (i.e. no record has been load yet) so an eventual fragment caching can work as expected
- You will need to check the size of the paginated collection (`@records`) in order to know if it is the last page or not. You can tell if it by checking `@records.size < @pagy.vars[:items]`. Notice that IF the last page has exactly the `@pagy.vars[:items]` in it you will not be able to know it. In infinite scroll that would just try to load the next page returning 0 items, so it will be perfectly acceptable anyway.

## Variables

| Variable | Description | Default |
|:---------------------|:----------------------------------|:--------|
| `:countless_minimal` | enable the countless minimal mode | `false` |

## Files

Expand All @@ -35,7 +69,7 @@ All the methods in this module are prefixed with the `"pagy_countless"` string,

### pagy_countless(collection, vars=nil)

This method is the same as the generic `pagy` method. (see the [pagy doc](../api/backend.md#pagycollection-varsnil))
This method is the same as the generic `pagy` method (see the [pagy doc](../api/backend.md#pagycollection-varsnil)), however its returned objects will depend on the value of the `:countless_minimal` variable (see [Modes](#modes))

### pagy_countless_get_vars(_collection, vars)

Expand All @@ -44,6 +78,3 @@ This sub-method is similar to the `pagy_get_vars` sub-method, but it is called o
### pagy_countless_get_items(collection, pagy)

This sub-method is similar to the `pagy_get_items` sub-method, but it is called only by the `pagy_countless` method. (see the [pagy_get_items doc](../api/backend.md#pagy_get_itemscollection-pagy)).

**Notice**: This method calls `to_a` on the collection in order to `pop` the eventual extra item from the result, so it returns an `Array`. That's different than the regular `pagy_get_items` method which doesn't need to call `to_a` on the collection.

1 change: 1 addition & 0 deletions lib/config/pagy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
# Countless extra: Paginate without any count, saving one query per rendering
# See https://ddnexus.github.io/pagy/extras/countless
# require 'pagy/extras/countless'
# Pagy::VARS[:countless_minimal] = false # default (eager loading)

# Elasticsearch Rails extra: Paginate `ElasticsearchRails::Results` objects
# See https://ddnexus.github.io/pagy/extras/elasticsearch_rails
Expand Down
6 changes: 5 additions & 1 deletion lib/pagy/extras/countless.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

class Pagy

VARS[:countless_minimal] = false

module Backend
private # the whole module is private so no problem with including it in a controller

Expand All @@ -17,13 +19,15 @@ def pagy_countless(collection, vars={})
# Sub-method called only by #pagy_countless: here for easy customization of variables by overriding
def pagy_countless_get_vars(_collection, vars)
pagy_set_items_from_params(vars) if defined?(UseItemsExtra)
vars[:page] ||= params[ vars[:page_param] || VARS[:page_param] ]
vars[:page] ||= params[ vars[:page_param] || VARS[:page_param] ]
vars
end

# Sub-method called only by #pagy_countless: here for easy customization of record-extraction by overriding
def pagy_countless_get_items(collection, pagy)
# This should work with ActiveRecord, Sequel, Mongoid...
return collection.offset(pagy.offset).limit(pagy.items) if pagy.vars[:countless_minimal]

items = collection.offset(pagy.offset).limit(pagy.items + 1).to_a
items_size = items.size
items.pop if items_size == pagy.items + 1
Expand Down
53 changes: 27 additions & 26 deletions test/pagy/extras/metadata_test.rb.rematch
Original file line number Diff line number Diff line change
@@ -1,30 +1,4 @@
---
"[1] pagy/extras/metadata::#pagy_metadata#test_0004_returns only specific metadata":
:scaffold_url: "/foo?page=__pagy_page__"
:page: 3
:count: 1000
:prev: 2
:next: 4
:pages: 50
"[1] pagy/extras/metadata::#pagy_metadata#test_0001_defines all metadata":
- :scaffold_url
- :first_url
- :prev_url
- :page_url
- :next_url
- :last_url
- :count
- :page
- :items
- :vars
- :pages
- :last
- :from
- :to
- :prev
- :next
- :series
- :sequels
"[1] pagy/extras/metadata::#pagy_metadata#test_0002_returns the full pagy metadata":
:scaffold_url: "/foo?page=__pagy_page__"
:first_url: "/foo?page=1"
Expand All @@ -50,6 +24,7 @@
:link_extra: ''
:i18n_key: pagy.item_name
:cycle: false
:countless_minimal: false
:steps: false
:metadata:
- :scaffold_url
Expand Down Expand Up @@ -98,3 +73,29 @@
- 7
- :gap
- 50
"[1] pagy/extras/metadata::#pagy_metadata#test_0001_defines all metadata":
- :scaffold_url
- :first_url
- :prev_url
- :page_url
- :next_url
- :last_url
- :count
- :page
- :items
- :vars
- :pages
- :last
- :from
- :to
- :prev
- :next
- :series
- :sequels
"[1] pagy/extras/metadata::#pagy_metadata#test_0004_returns only specific metadata":
:scaffold_url: "/foo?page=__pagy_page__"
:page: 3
:count: 1000
:prev: 2
:next: 4
:pages: 50

0 comments on commit 5103ee9

Please sign in to comment.