Skip to content

Commit

Permalink
added standalone extra
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Apr 30, 2021
1 parent e22c9c3 commit aa83126
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 37 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Pagy is the ultimate pagination gem that outperforms the others in each and ever
- Added the docker development environment to ease contributions
- Big code restyling following ruby 3.0 syntax and cops; the code is simpler, more readable and verbose with yet improved performance.
- All the public helpers accept optional keyword arguments (see the [Changelog](CHANGELOG.md#version-440))
- New [standalone extra](http://ddnexus.github.io/pagy/extras/standalone) to use pagy without any request object, nor Rack environment/gem, nor any defined `params` method, even in the irb/rails console without any app or config.

## Comparison with other gems

Expand Down Expand Up @@ -126,6 +127,7 @@ Use the official extras, or write your own in just a few lines. Extras add speci
- [i18n](http://ddnexus.github.io/pagy/extras/i18n): Use the `I18n` gem instead of the pagy-i18n implementation
- [items](http://ddnexus.github.io/pagy/extras/items): Allow the client to request a custom number of items per page with an optional selector UI
- [overflow](http://ddnexus.github.io/pagy/extras/overflow): Allow for easy handling of overflowing pages
- [standalone](http://ddnexus.github.io/pagy/extras/standalone): Use pagy without any request object, nor Rack environment/gem, nor any defined `params` method, even in the irb/rails console without any app or config.
- [support](http://ddnexus.github.io/pagy/extras/support): Extra support for features like: incremental, auto-incremental and infinite pagination
- [trim](http://ddnexus.github.io/pagy/extras/trim): Remove the `page=1` param from the first page link

Expand Down
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ <h1 id="site-title">{{ site.title | default: site.github.repository_name }}
<a href="{{ site.baseurl }}/extras/navs"><p class="indent1" {% if page.title == 'Navs' %}id="active"{% endif %} >Navs</p></a>
<a href="{{ site.baseurl }}/extras/searchkick"><p class="indent1" {% if page.title == 'Searchkick' %}id="active"{% endif %} >Searchkick</p></a>
<a href="{{ site.baseurl }}/extras/semantic"><p class="indent1" {% if page.title == 'Semantic' %}id="active"{% endif %} >Semantic</p></a>
<a href="{{ site.baseurl }}/extras/standalone"><p class="indent1" {% if page.title == 'Standalone' %}id="active"{% endif %} >Standalone</p></a>
<a href="{{ site.baseurl }}/extras/support"><p class="indent1" {% if page.title == 'Support' %}id="active"{% endif %} >Support</p></a>
<a href="{{ site.baseurl }}/extras/tailwind"><p class="indent1" {% if page.title == 'Tailwind' %}id="active"{% endif %} >Tailwind</p></a>
<a href="{{ site.baseurl }}/extras/trim"><p class="indent1" {% if page.title == 'Trim' %}id="active"{% endif %} >Trim</p></a>
Expand Down
45 changes: 23 additions & 22 deletions docs/extras.md

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions docs/extras/standalone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Standalone
---
# Standalone Extra

This extra allows you to use pagy completely standalone, i.e. without any request object, nor Rack environment/gem, nor any defined `params` method, even in the irb/rails console witout an app.

You may need it in order to paginate a collection outside of a regular rack request or controller, like in an unconventional API module, or in the irb/rails console or for testing/playing with backend and frontend methods.

You trigger the standalone mode by setting an `:url` variable, which will be used directly and verbatim, instead of extracting it from the `request` `Rack::Request` object. You can also pass other params by using the `:params` variable as usual. That will be used to produce the final URLs in the usual way.

This extra will also create a dummy `params` method (if not already defined) in the module where you will include the `Pagy::Backend` (usually a controller).

## Synopsis

See [extras](../extras.md) for general usage info.

In the `pagy.rb` initializer:

```ruby
require 'pagy/extras/standalone'

# optional: set a default url
Pagy::Vars[:url] = 'http://www.example.com/subdir'

# pass a :url variable to work in standalone mode (no need of any request object nor Rack env)
@pagy, @records = pagy(Product.all, url: 'http://www.example.com/subdir', params: {...})
```

In a console, even without any app nor initializer:

```ruby
require 'pagy'
require 'pagy/extras/standalone'
include Pagy::Console
pagy_extras :array, :metadata, ...

### then you can use it like inside an app
pagy, items = pagy_array((1..1000).to_a, page: 3)
pagy_navs(pagy)
=> "<nav class=\"pagy-nav pagination\" role=\"navigation\" aria-label=\"pager\"><span class=\"page prev\"><a href=\"http://www.example.com/subdir?page=2&items=20\" rel=\"prev\" aria-label=\"previous\">&lsaquo;&nbsp;Prev</a></span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=1&items=20\" >1</a></span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=2&items=20\" rel=\"prev\" >2</a></span> <span class=\"page active\">3</span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=4&items=20\" rel=\"next\" >4</a></span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=5&items=20\" >5</a></span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=6&items=20\" >6</a></span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=7&items=20\" >7</a></span> <span class=\"page gap\">&hellip;</span> <span class=\"page\"><a href=\"http://www.example.com/subdir?page=50&items=20\" >50</a></span> <span class=\"page next\"><a href=\"http://www.example.com/subdir?page=4&items=20\" rel=\"next\" aria-label=\"next\">Next&nbsp;&rsaquo;</a></span></nav>"

pagy_metadata(pagy)
=>
{:scaffold_url=>"http://www.example.com/subdir?page=__pagy_page__",
:first_url=>"http://www.example.com/subdir?page=1",
:prev_url=>"http://www.example.com/subdir?page=2",
:page_url=>"http://www.example.com/subdir?page=3",
:next_url=>"http://www.example.com/subdir?page=4",
:last_url=>"http://www.example.com/subdir?page=50",
:count=>1000,
:page=>3,
:items=>20,
:vars=>
{:page=>3,
:items=>20,
:outset=>0,
:size=>[1, 4, 4, 1],
...
```

## Files

- [standalone.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/standalone.rb)

## Variables

| Variable | Description | Default |
|:---------|:-----------------------------------------|:--------|
| `:url` | url string (can be absolute or relative) | `nil` |

You can use the `:params` variable to add params to the final URLs.

## Methods

### Overridden pagy_url_for

The `standalone` extra overrides the `pagy_url_for` method used internally. If it finds a set `:url` variable it assumes there is no `request` object, so it uses the `:url` var verbatim to produce the final URL, only adding the query string, composed by merging the `:page` param to the `:params` variable. If there is no `:url` variable set it works like usual, i.e. it uses the rake `request` object to extract the base_url, path from the request, merging the params returned from the `params` controller method, the `:params` variable and the `:page` param to it.

### Dummy params method

This extra creates a dummy `params` method (if not already defined) in the module where you will include the `Pagy::Backend` (usually a controller). The method is called by pagy to retrive backend variables coming from the request, and expects a hash, so the dummy param method returns an empty hash avoiding an error.

## Pagy::Console module

Include it in your console window to include `Pagy::Backend`, `Pagy::Frontend` and set a dummy default `:url` variable.

### pagy_extras(*extras)

Simple utility method to save some typing in the console. It will require the extras arguments:

```ruby
pagy_extra :array, :bootstrap, :support, :headers, ...
```
19 changes: 12 additions & 7 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,27 @@ You can copy the comprehensive and annotated [pagy.rb](https://github.com/ddnexu
Notice: Older versions run on ruby 1.9+ or jruby 1.7+ till ruby <3.0
### Out of the box assumptions
### Assumptions for Rack environment
Pagy works out of the box assuming that:
Pagy works out of the box in a web app assuming that:
- You are using a `Rack` based framework
- The collection to paginate is an ORM collection (e.g. ActiveRecord scope)
- You are using a `Rack` based framework (Rails, Sinatra, Padrino, etc.)
- The collection to paginate is an ORM collection (e.g. ActiveRecord scope) or other collections supported by some backend extra ([array](extras/array.md), [elasticsearch_rails](extras/elasticsearch_rails.md), [searchkick](extras/searchkick.md), ...)
- The controller where you include `Pagy::Backend` responds to a `params` method
- The view where you include `Pagy::Frontend` responds to a `request` method returning a `Rack::Request` instance.
### Non Rack Environments apps/API
- Require the [standalone extra](extras/standalone.md), and pass a `:url` variable and you can use it without Rack in your app or exotic API, with or without the other extra you might need. You can even use every feature/helper right in the irb/rails console.
- Besides Rack the other assumptions above apply
### Any other scenario assumptions
Pagy can also work in any other scenario assuming that:
- If your framework doesn't have a `params` method you may need to define the `params` method or override the `pagy_get_vars` (which uses the `params` method) in your controller
- If the collection you are paginating doesn't respond to `offset` and `limit` you may need to override the `pagy_get_items` method in your controller (to get the items out of your specific collection) or use a specific extra if available (e.g. `array`, `searchkick`, `elasticsearch_rails`)
- If your framework doesn't have a `request` method you may need to override the `pagy_url_for` (which uses `Rack` and `request`) in your view
- If your framework doesn't have a `params` method you can use the [standalone extra](extras/standalone.md) or you may need to define the `params` method or override the `pagy_get_vars` (which uses the `params` method) in your controller
- If the collection you are paginating doesn't respond to `offset` and `limit` and is not yet supported by psome extra, you may need to override the `pagy_get_items` method in your controller (to get the items out of your specific collection)
- If your framework doesn't have a `request` method you can use the [standalone extra](extras/standalone.md) or you may need to override the `pagy_url_for` (which uses `Rack` and `request`) in your view

**Notice**: the total overriding you may need is usually just a handful of lines at worse, and it doesn't need monkey patching or writing any sub-class or module.
Expand Down
2 changes: 1 addition & 1 deletion lib/pagy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def self.root

# Merge and validate the options, do some simple arithmetic and set the instance variables
def initialize(vars)
@vars = VARS.merge( vars.delete_if{|_,v| v.nil? || v == '' } )
@vars = VARS.merge( vars.delete_if{|k,v| VARS.key?(k) && (v.nil? || v == '') } )
@vars[:fragment] = deprecated_var(:anchor, @vars[:anchor], :fragment, @vars[:fragment]) if @vars[:anchor]

INSTANCE_VARS_MIN.each do |name,min|
Expand Down
71 changes: 71 additions & 0 deletions lib/pagy/extras/standalone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# See the Pagy documentation: https://ddnexus.github.io/pagy/extras/standalone
# frozen_string_literal: true

require 'uri'
class Pagy

# extracted from Rack::Utils and reformatted for rubocop
module QueryUtils
module_function
def escape(str)
URI.encode_www_form_component(str)
end
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
when Hash
value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) }.delete_if(&:empty?).join('&')
when nil
prefix
else
raise ArgumentError, 'value must be a Hash' if prefix.nil?
"#{prefix}=#{escape(value)}"
end
end
end

module UseStandaloneExtra
# without any :url var it works exactly as the regular #pagy_url_for;
# with a defined :url variable it does not use rack/request
def pagy_url_for(pagy, page, deprecated_url=nil, absolute: nil)
absolute = Pagy.deprecated_arg(:url, deprecated_url, :absolute, absolute) if deprecated_url
pagy, page = Pagy.deprecated_order(pagy, page) if page.is_a?(Pagy)
p_vars = pagy.vars
if p_vars[:url]
url_string = p_vars[:url]
params = {}
else
url_string = "#{request.base_url if absolute}#{request.path}"
params = request.GET
end
params = params.merge(p_vars[:params])
params[p_vars[:page_param].to_s] = page
params[p_vars[:items_param].to_s] = p_vars[:items] if defined?(UseItemsExtra)
query_string = "?#{QueryUtils.build_nested_query(pagy_get_params(params))}" unless params.empty?
"#{url_string}#{query_string}#{p_vars[:fragment]}"
end
end
Helpers.prepend UseStandaloneExtra

# defines a dummy #params method if not already defined in the including module
module Backend
def self.included(controller)
controller.define_method(:params){{}} unless controller.method_defined?(:params)
end
end

# include Pagy::Console in irb/rails console for a ready to use pagy environment
module Console
def self.included(main)
main.include(Backend)
main.include(Frontend)
VARS[:url] = 'http://www.example.com/subdir'
end

def pagy_extras(*extras)
extras.each {|extra| require "pagy/extras/#{extra}"}
end
end

end
1 change: 1 addition & 0 deletions pagy.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ lib/pagy/extras/overflow.rb
lib/pagy/extras/searchkick.rb
lib/pagy/extras/semantic.rb
lib/pagy/extras/shared.rb
lib/pagy/extras/standalone.rb
lib/pagy/extras/support.rb
lib/pagy/extras/trim.rb
lib/pagy/extras/uikit.rb
Expand Down
16 changes: 9 additions & 7 deletions tasks/test.rake
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ require 'rake/testtask'
# in isolation in order to avoid affecting also other tests.
test_tasks = {}

%w[ headers
%w[ elasticsearch_rails
headers
i18n
items
items_trim
overflow
support
shared_oj
searchkick
shared_json
shared_oj
standalone
standalone_console
support
trim
items_trim
items
elasticsearch_rails
searchkick
].each do |name|
task_name = :"test_#{name}"
file_path = "test/**/#{name}_test.rb"
Expand Down
28 changes: 28 additions & 0 deletions test/pagy/extras/standalone_console_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require_relative '../../test_helper'
require 'pagy/extras/standalone'

module PagyConsole
include Pagy::Console
# we are not in the console so we need module_function
module_function :pagy_extras
end

describe 'pagy/extras/standalone_console' do

describe 'Pagy::Console' do
it 'defines default :url' do
_(Pagy::VARS[:url]).must_equal 'http://www.example.com/subdir'
end
it 'includes Pagy::Backend and Pagy::Frontend' do
_(PagyConsole <= Pagy::Backend).must_equal true
_(PagyConsole <= Pagy::Frontend).must_equal true
end
it 'requires extras' do
PagyConsole.pagy_extras :array, :navs
_(Pagy::Backend.method_defined?(:pagy_array))
_(Pagy::Frontend.method_defined?(:pagy_nav_js))
end
end
end
76 changes: 76 additions & 0 deletions test/pagy/extras/standalone_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require_relative '../../test_helper'
require 'pagy/extras/standalone'

class EmptyController
include Pagy::Backend
end
class FilledController
def params
{a: 'a', b: 'b'}
end
include Pagy::Backend
end

describe 'pagy/extras/standalone' do
let(:view) { MockView.new }

describe 'defines #params if missing' do
it 'defines a dummy #params' do
_(EmptyController.new.params).must_equal({})
end
it 'does not define a dummy #params' do
_(FilledController.new.params).must_equal({a: 'a', b: 'b'})
end
end

describe '#pagy_url_for' do

it 'renders basic url' do
pagy = Pagy.new count: 1000, page: 3
_(view.pagy_url_for(pagy, 5)).must_equal '/foo?page=5'
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal 'http://example.com:3000/foo?page=5'
pagy = Pagy.new count: 1000, page: 3, url: 'http://www.pagy-standalone.com/subdir'
_(view.pagy_url_for(pagy, 5)).must_equal 'http://www.pagy-standalone.com/subdir?page=5'
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal 'http://www.pagy-standalone.com/subdir?page=5'
pagy = Pagy.new count: 1000, page: 3, url: ''
_(view.pagy_url_for(pagy, 5)).must_equal '?page=5'
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal '?page=5'
end

it 'renders url with params' do
pagy = Pagy.new count: 1000, page: 3, params: {a: 3, b: 4}
_(view.pagy_url_for(pagy, 5)).must_equal '/foo?page=5&a=3&b=4'
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal 'http://example.com:3000/foo?page=5&a=3&b=4'
pagy = Pagy.new count: 1000, page: 3, params: {a: 3, b: 4}, url: 'http://www.pagy-standalone.com/subdir'
_(view.pagy_url_for(pagy, 5)).must_equal "http://www.pagy-standalone.com/subdir?a=3&b=4&page=5"
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal "http://www.pagy-standalone.com/subdir?a=3&b=4&page=5"
pagy = Pagy.new count: 1000, page: 3, params: {a: 3, b: 4}, url: ''
_(view.pagy_url_for(pagy, 5)).must_equal "?a=3&b=4&page=5"
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal "?a=3&b=4&page=5"
end
it 'renders url with fragment' do
pagy = Pagy.new count: 1000, page: 3, fragment: '#fragment'
_(view.pagy_url_for(pagy, 6)).must_equal '/foo?page=6#fragment'
_(view.pagy_url_for(pagy, 6, absolute: true)).must_equal 'http://example.com:3000/foo?page=6#fragment'
pagy = Pagy.new count: 1000, page: 3, fragment: '#fragment', url: 'http://www.pagy-standalone.com/subdir'
_(view.pagy_url_for(pagy, 6)).must_equal 'http://www.pagy-standalone.com/subdir?page=6#fragment'
_(view.pagy_url_for(pagy, 6, absolute: true)).must_equal 'http://www.pagy-standalone.com/subdir?page=6#fragment'
pagy = Pagy.new count: 1000, page: 3, fragment: '#fragment', url: ''
_(view.pagy_url_for(pagy, 6)).must_equal '?page=6#fragment'
_(view.pagy_url_for(pagy, 6, absolute: true)).must_equal '?page=6#fragment'
end
it 'renders url with params and fragment' do
pagy = Pagy.new count: 1000, page: 3, params: {a: 3, b: 4}, fragment: '#fragment'
_(view.pagy_url_for(pagy, 5)).must_equal '/foo?page=5&a=3&b=4#fragment'
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal 'http://example.com:3000/foo?page=5&a=3&b=4#fragment'
pagy = Pagy.new count: 1000, page: 3, params: {a: [1,2,3]}, fragment: '#fragment', url: 'http://www.pagy-standalone.com/subdir'
_(view.pagy_url_for(pagy, 5)).must_equal "http://www.pagy-standalone.com/subdir?a[]=1&a[]=2&a[]=3&page=5#fragment"
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal "http://www.pagy-standalone.com/subdir?a[]=1&a[]=2&a[]=3&page=5#fragment"
pagy = Pagy.new count: 1000, page: 3, params: {a: nil}, fragment: '#fragment', url: ''
_(view.pagy_url_for(pagy, 5)).must_equal "?a&page=5#fragment"
_(view.pagy_url_for(pagy, 5, absolute: true)).must_equal "?a&page=5#fragment"
end
end
end

0 comments on commit aa83126

Please sign in to comment.