diff --git a/Rakefile b/Rakefile
index 74b1471c7..df15f3135 100644
--- a/Rakefile
+++ b/Rakefile
@@ -10,7 +10,7 @@ require "rubocop/rake_task"
Rake::TestTask.new(:test_common) do |t|
t.libs << "test"
t.libs << "lib"
- t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb')
+ t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb', 'test/**/out_of_range_test.rb')
end
Rake::TestTask.new(:test_extra_i18n) do |t|
@@ -25,7 +25,13 @@ Rake::TestTask.new(:test_extra_items) do |t|
t.test_files = FileList['test/**/items_test.rb']
end
-task :test => [:test_common, :test_extra_items, :test_extra_i18n]
+Rake::TestTask.new(:test_extra_out_of_range) do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList['test/**/out_of_range_test.rb']
+end
+
+task :test => [:test_common, :test_extra_items, :test_extra_i18n, :test_extra_out_of_range ]
RuboCop::RakeTask.new(:rubocop) do |t|
t.options = `git ls-files -z`.split("\x0") # limit rubocop to the files in the repo
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 0d6d2fac6..ab7c44fce 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -35,10 +35,11 @@
Extras
Array
Bootstrap
- Bulma
+ Bulma
Compact
I18n
Items
+ Out Of Range
Responsive
Migration Tips
> Chat Support on Gitter <
diff --git a/docs/extras.md b/docs/extras.md
index df6082e03..f78ca572a 100644
--- a/docs/extras.md
+++ b/docs/extras.md
@@ -5,15 +5,16 @@ title: Extras
Pagy comes with a few optional extensions/extras:
-| Extra | Description | Links |
-| ------------ | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
-| `array` | Paginate arrays efficiently avoiding expensive array-wrapping and without overriding | [array.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/array.rb), [documentation](extras/array.md) |
-| `bootstrap` | Nav helper and templates for Bootstrap pagination | [bootstrap.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bootstrap.rb), [documentation](extras/bootstrap.md) |
-| `bulma` | Nav helper and templates for [Bulma](https://bulma.io) pagination component | [bulma.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bulma.rb), [documentation](extras/bulma.md) |
-| `compact` | An alternative UI that combines the pagination with the nav info in a single compact element | [compact.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/compact.rb), [documentation](extras/compact.md) |
-| `i18n` | Use the `I18n` gem instead of the pagy implementation | [i18n.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/i81n.rb), [documentation](extras/i18n.md) |
-| `items` | Allow the client to request a custom number of items per page with a ready to use selector UI | [items.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/items.rb), [documentation](extras/items.md) |
-| `responsive` | On resize, the number of page links will adapt in real-time to the available window or container width | [responsive.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/responsive.rb), [documentation](extras/responsive.md) |
+| Extra | Description | Links |
+| -------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
+| `array` | Paginate arrays efficiently avoiding expensive array-wrapping and without overriding | [array.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/array.rb), [documentation](extras/array.md) |
+| `bootstrap` | Nav helper and templates for Bootstrap pagination | [bootstrap.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bootstrap.rb), [documentation](extras/bootstrap.md) |
+| `bulma` | Nav helper and templates for [Bulma](https://bulma.io) pagination component | [bulma.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bulma.rb), [documentation](extras/bulma.md) |
+| `compact` | An alternative UI that combines the pagination with the nav info in a single compact element | [compact.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/compact.rb), [documentation](extras/compact.md) |
+| `i18n` | Use the `I18n` gem instead of the pagy implementation | [i18n.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/i81n.rb), [documentation](extras/i18n.md) |
+| `items` | Allow the client to request a custom number of items per page with a ready to use selector UI | [items.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/items.rb), [documentation](extras/items.md) |
+| `out_of_range` | Allow for easy handling of out of range pages | [out_of_range.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/out_of_range.rb), [documentation](extras/out_of_range.md) |
+| `responsive` | On resize, the number of page links will adapt in real-time to the available window or container width | [responsive.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/responsive.rb), [documentation](extras/responsive.md) |
## Synopsys
diff --git a/docs/extras/out_of_range.md b/docs/extras/out_of_range.md
new file mode 100644
index 000000000..613dee4bc
--- /dev/null
+++ b/docs/extras/out_of_range.md
@@ -0,0 +1,96 @@
+---
+title: Out Of Range
+---
+# Out Of Range Extra
+
+This extra allows for easy handling of out of range pages. It internally resques from the `Pagy::OutOfRangeError` offering a few different ready to use modes, quite useful for UIs and/or APIs.
+
+## Synopsys
+
+See [extras](../extras.md) for general usage info.
+
+In the Pagy initializer:
+
+```ruby
+require 'pagy/extras/out_of_range'
+
+# default :last_page (other options :empty_page and :exception )
+Pagy::VARS[:out_of_range_mode] = :last_page
+```
+
+## Files
+
+This extra is composed of the [out_of_range.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/out_of_range.rb) file.
+
+## Variables
+
+| Variable | Description | Default |
+| -------------------- | ------------------------------------------ | ------------ |
+| `:out_of_range_mode` | `:last_page`, `empty_page` or `:exception` | `:last_page` |
+
+As usual, depending on the scope of the customization, you have a couple of options to set the variables:
+
+As a global default:
+
+```ruby
+Pagy::VARS[:out_of_range_mode] = :empty_page
+```
+
+For a single instance (overriding the global default):
+
+```ruby
+pagy(scope, out_of_range_mode: :empty_page)
+Pagy.new(count:100, out_of_range_mode: :empty_page)
+```
+
+## Modes
+
+These are the modes accepted by the `:out_of_range_mode` variable:
+
+### :last_page
+
+This is the default mode. It is useful in apps with an UI, in order to avoid to redirect to the last page.
+
+Regardless the out of range page requested, Pagy will set the page to the last page and paginate exactly as if the last page has been requested. For example:
+
+```ruby
+# no exception passing an out of range page (Default mode :last_page)
+pagy = Pagy.newpag(count: 100, page: 100)
+
+pagy.out_of_range? #=> true
+pagy.vars[:page] #=> 100 (requested page)
+pagy.page #=> 5 (curentlast page)
+pagy.last == pagy.page #=> true
+```
+
+### :empty_page
+
+This mode will paginate the actual requested page, which - being out of range - is empty. It is useful with APIs, where the client expects an empty set of results in order to stop requesting further pages. For example:
+
+```ruby
+pagy = Pagy.new(count: 100, page: 100, out_of_range_mode: :empty_page)
+
+pagy.out_of_range? #=> true
+pagy.vars[:page] #=> 100 (requested page)
+pagy.page #=> 100 (actual empty page)
+pagy.last == pagy.page #=> false
+pagy.last #=> 5
+pagy.last == pagy.prev #=> true (the prev page is the last page relative to out of range page)
+pagy.next #=> nil
+pagy.offset #=> 0
+pagy.items #=> 0
+pagy.from #=> 0
+pagy.to #=> 0
+
+pagy.series #=> [1, 2, 3, 4, 5] (no string, so no current page highlighted in the UI)
+```
+
+### :exception
+
+This mode raises the `Pagy::OutOfRangeError` as usual, so you can rescue from and do what is needed. It is useful when you need to use your own custom mode even in presence of this extra (which would not raise any error).
+
+## Methods
+
+### out_of_range?
+
+Use this method in order to know if the requested page is out of range. The original requested page is available as `pagy.vars[:page]` (useful when used with the `:last_page` mode, in case you want to give some feedback about the rescue to the user/client).
diff --git a/docs/how-to.md b/docs/how-to.md
index dfe9563ae..e47d51912 100644
--- a/docs/how-to.md
+++ b/docs/how-to.md
@@ -384,12 +384,19 @@ You can do so by setting the `:item_path` variable to the path to lookup in the
## Handling Pagy::OutOfRangeError exception
Pass an out of range `:page` number and Pagy will raise a `Pagy::OutOfRangeError` exception.
-This often happens because users paginate over the end of the record set or records go deleted and a user went to a stale page.
-A few options for handling this are:
+
+This often happens because users/clients paginate over the end of the record set or records go deleted and a user went to a stale page.
+
+You can rescue the exception manually or use the [out_of_range extra](extras/out_of_range.md):
+
+### Manual Rescue
+
+A few options for manually handling the error in apps are:
- Do nothing and let the page render a 500
- Rescue and render a 404
- Rescue and redirect to the last know page
+
```ruby
# in a controller
rescue_from Pagy::OutOfRangeError, with: :redirect_to_last_page
@@ -400,6 +407,7 @@ A few options for handling this are:
redirect_to url_for(page: e.pagy.last), notice: "Page ##{params[:page]} is out of range. Showing page #{e.pagy.last} instead."
end
```
+
- Rescue and render a page without results, this can be useful for api responses where clients iterate until they see an empty page
```ruby
results = begin
@@ -408,3 +416,6 @@ A few options for handling this are:
[]
end
```
+
+### Use the "out_of_range" Extra
+
diff --git a/lib/pagy/extras/initializer_example.rb b/lib/pagy/extras/initializer_example.rb
index 32b36ab75..d808ecfbf 100644
--- a/lib/pagy/extras/initializer_example.rb
+++ b/lib/pagy/extras/initializer_example.rb
@@ -27,6 +27,10 @@
# Pagy::VARS[:items_param] = :items # default
# Pagy::VARS[:max_items] = 100 # default
+# Out Of Range: Allow for easy handling of out of range pages
+# See https://ddnexus.github.io/pagy/extras/out_of_range
+# Pagy::VARS[:out_of_range_mode] = :last_page # default (other options :empty_page and :exception )
+
# Responsive: On resize, the number of page links will adapt in real-time to the available window or container width
# See https://ddnexus.github.io/pagy/extras/responsive
# require 'pagy/extras/responsive'
diff --git a/lib/pagy/extras/out_of_range.rb b/lib/pagy/extras/out_of_range.rb
new file mode 100644
index 000000000..ce3f8087a
--- /dev/null
+++ b/lib/pagy/extras/out_of_range.rb
@@ -0,0 +1,29 @@
+class Pagy
+
+ VARS[:out_of_range_mode] = :last_page
+
+ def out_of_range?; @out_of_range end
+
+ alias :create :initialize
+
+ def initialize(vars)
+ create(vars)
+ rescue OutOfRangeError => e
+ raise e if @vars[:out_of_range_mode] == :exception
+ @out_of_range = true
+ if @vars[:out_of_range_mode] == :last_page
+ @page = @last # set as last page
+ elsif @vars[:out_of_range_mode] == :empty_page
+ @offset = @items = @from = @to = 0 # vars relative to the actual page
+ @prev = @last # the prev is the last page
+ define_singleton_method(:series) do |size=@vars[:size]|
+ @page = @last # series for last page
+ super(size).tap do |s| # call original series
+ s[s.index(@page.to_s)] = @page # string to integer (i.e. no current page)
+ @page = @vars[:page] # restore the actual page
+ end
+ end
+ end
+ end
+
+end
diff --git a/test/pagy/extras/out_of_range_test.rb b/test/pagy/extras/out_of_range_test.rb
new file mode 100644
index 000000000..ad8a3826b
--- /dev/null
+++ b/test/pagy/extras/out_of_range_test.rb
@@ -0,0 +1,63 @@
+require 'pagy'
+require_relative '../../test_helper'
+require 'pagy/extras/out_of_range'
+
+SingleCov.covered!
+
+describe Pagy do
+
+ let(:vars) {{ page: 100, count: 103, items: 10, size: [3, 2, 2, 3] }}
+ let(:pagy) {Pagy.new(vars)}
+
+ describe "variables" do
+
+ it 'has vars defaults' do
+ Pagy::VARS[:out_of_range_mode].must_equal :last_page
+ end
+
+ end
+
+ describe "#out_of_range?" do
+
+ it 'must be out_of_range?' do
+ pagy.must_be :out_of_range?
+ Pagy.new(vars.merge(page:2)).wont_be :out_of_range?
+ end
+
+ end
+
+
+ describe "#initialize" do
+
+ it 'initializes with out of range page' do
+ pagy.must_be_instance_of Pagy
+ pagy.page.must_equal pagy.last
+ pagy.vars[:page].must_equal 100
+ end
+
+ it 'raises OutOfRangeError in :exception mode' do
+ proc { Pagy.new(vars.merge(out_of_range_mode: :exception)) }.must_raise Pagy::OutOfRangeError
+ end
+
+ it 'works in :empty_page mode' do
+ pagy = Pagy.new(vars.merge(out_of_range_mode: :empty_page))
+ pagy.page.must_equal 100
+ pagy.offset.must_equal 0
+ pagy.items.must_equal 0
+ pagy.from.must_equal 0
+ pagy.to.must_equal 0
+ pagy.prev.must_equal pagy.last
+ end
+
+ end
+
+ describe "#series singleton for :empty_page mode" do
+ it 'computes series for last page' do
+ pagy = Pagy.new(vars.merge(out_of_range_mode: :empty_page))
+ series = pagy.series
+ series.must_equal [1, 2, 3, :gap, 9, 10, 11]
+ pagy.page.must_equal 100
+ end
+ end
+
+end