Skip to content

Commit

Permalink
added metadata extra
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Aug 20, 2019
1 parent 261e6e6 commit 43349a8
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 7 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -106,6 +106,8 @@ Use the official extras, or write your own in just a few lines. Extras add speci
- [array](http://ddnexus.github.io/pagy/extras/array): Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding
- [countless](http://ddnexus.github.io/pagy/extras/countless): Paginate without the need of any count, saving one query per rendering
- [elasticsearch_rails](http://ddnexus.github.io/pagy/extras/elasticsearch_rails): Paginate `ElasticsearchRails` response objects
- [headers](http://ddnexus.github.io/pagy/extras/headers): Add RFC-8288 compliant http response headers (and other helpers) useful for API pagination
- [metadata](http://ddnexus.github.io/pagy/extras/metadata): Provides the pagination metadata to Javascrtipt frameworks like Vue.js, react.js, etc.
- [searchkick](http://ddnexus.github.io/pagy/extras/searchkick): Paginate `Searchkick::Results` objects

### Frontend Extras
Expand All @@ -120,7 +122,6 @@ Use the official extras, or write your own in just a few lines. Extras add speci

### Feature Extras

- [headers](http://ddnexus.github.io/pagy/extras/headers): Add RFC-8288 compliant http response headers (and other helpers) useful for API pagination
- [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
Expand Down
9 changes: 8 additions & 1 deletion Rakefile
Expand Up @@ -12,7 +12,8 @@ require "rake/testtask"
Rake::TestTask.new(:test_main) do |t|
t.libs += %w[test lib]
t.test_files = FileList.new.include("test/**/*_test.rb")
.exclude('test/**/i18n_test.rb',
.exclude('test/**/headers_test.rb',
'test/**/i18n_test.rb',
'test/**/items_test.rb',
'test/**/overflow_test.rb',
'test/**/trim_test.rb',
Expand All @@ -23,6 +24,11 @@ Rake::TestTask.new(:test_main) do |t|
'test/**/shared_combo_test.rb')
end

Rake::TestTask.new(:test_extra_headers) do |t|
t.libs += %w[test lib]
t.test_files = FileList['test/**/headers_test.rb']
end

Rake::TestTask.new(:test_extra_i18n) do |t|
t.libs += %w[test lib]
t.test_files = FileList['test/**/i18n_test.rb']
Expand Down Expand Up @@ -66,6 +72,7 @@ end

task :test => [ :test_main,
:test_extra_items,
:test_extra_headers,
:test_extra_i18n,
:test_extra_overflow,
:test_extra_trim,
Expand Down
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Expand Up @@ -110,6 +110,7 @@ <h1 id="site-title">{{ site.title | default: site.github.repository_name }}
<a href="{{ site.baseurl }}/extras/items"><p class="indent1" {% if page.title == 'Items' %}id="active"{% endif %} >Items</p></a>
<a href="{{ site.baseurl }}/extras/overflow"><p class="indent1" {% if page.title == 'Overflow' %}id="active"{% endif %} >Overflow</p></a>
<a href="{{ site.baseurl }}/extras/materialize"><p class="indent1" {% if page.title == 'Materialize' %}id="active"{% endif %} >Materialize</p></a>
<a href="{{ site.baseurl }}/extras/metadata"><p class="indent1" {% if page.title == 'Metadata' %}id="active"{% endif %} >Metadata</p></a>
<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>
Expand Down
2 changes: 2 additions & 0 deletions docs/assets/css/style.scss
Expand Up @@ -6,6 +6,8 @@
body {
background-color: #f7f7f7;
color: #51585F;
padding-top: 30px;
padding-bottom: 30px;
}

a:hover, a:focus {
Expand Down
3 changes: 2 additions & 1 deletion docs/extras.md
Expand Up @@ -17,13 +17,14 @@ Pagy comes with a few optional extensions/extras:
| `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) |
| `materialize` | Add nav, nav_js and combo_nav_js helpers for the Materialize CSS [pagination component](https://materializecss.com/pagination.html) | [materialize.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/materialize.rb), [documentation](extras/materialize.md) |
| `metadata` | Provides the pagination metadata to Javascrtipt frameworks like Vue.js, react.js, etc. | [metadata.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/metadata.rb), [documentation](extras/metadata.md) |
| `navs` | Add nav_js and combo_nav_js javascript unstyled helpers | [navs.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/navs.rb), [documentation](extras/navs.md) |
| `overflow` | Allow for easy handling of overflowing pages | [overflow.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/overflow.rb), [documentation](extras/overflow.md) |
| `searchkick` | Paginate arrays efficiently avoiding expensive array-wrapping and without overriding | [searchkick.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/searchkick.rb), [documentation](extras/searchkick.md) |
| `semantic` | Add nav, nav_js and combo_nav_js helpers for the Semantic UI CSS [pagination component](https://semantic-ui.com/collections/menu.html) | [semantic.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/semantic.rb), [documentation](extras/semantic.md) |
| `support` | Extra support for features like: incremental, infinite, auto-scroll pagination | [support.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/support.rb), [documentation](extras/support.md) |
| `trim` | Remove the `page=1` param from links | [trim.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/trim.rb), [documentation](extras/trim.md) |
| `uikit` | Add nav, nav_js and combo_nav_js helpers for the UIkit [pagination component](https://getuikit.com/docs/pagination) | [uikit.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/uikit.rb), [documentation](extras/uikit.md) |
| `uikit` | Add nav, nav_js and combo_nav_js helpers for the UIkit [pagination component](https://getuikit.com/docs/pagination) | [uikit.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/uikit.rb), [documentation](extras/uikit.md) |

## Synopsis

Expand Down
4 changes: 3 additions & 1 deletion docs/extras/headers.md
Expand Up @@ -127,8 +127,10 @@ This method generates a hash of [RFC-8288](https://tools.ietf.org/html/rfc8288)

### pagy_headers_hash(pagy)

This method generates a hash structure of the headers, useful if you want to include some meta-data within your json. For example:
This method generates a hash structure of the headers, useful only if you want to include the headers as metadata within your JSON. For example:

```ruby
render json: records.as_json.merge!(meta: {pagination: pagy_headers_hash(pagy)})
```

**Notice**: If you need a more complete set of metadata (e.g. if you use some javascript frontend) see the [metadata extra](metadata.md).
66 changes: 66 additions & 0 deletions docs/extras/metadata.md
@@ -0,0 +1,66 @@
---
title: Metadata
---
# Metadata Extra

If your app uses ruby as pure backend and some javascript frameworks as the frontend (e.g. Vue.js, react.js, ...), then you may want to generate the whole pagination UI directly in javascript either with your own code or using some available component.

This extra makes that easy and efficient by adding a single method to the backend.

## Synopsis

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

In the `pagy.rb` initializer:

```ruby
require 'pagy/extras/metadata'
```

In your controller action:

```ruby
pagy, records = pagy(Product.all)
render json: { data: records,
pagy: pagy_metadata(pagy) }
```

## Files

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

## Variables

| Variable | Description | Default |
|:------------|:-----------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `:metadata` | Array of names used to control the returned metadata | `[ :scaffold_url, :first_url, :prev_url, :page_url, :next_url, :last_url, :count, :page, :items, :vars, :pages, :last, :from, :to, :prev, :next, :series ]` (plus `:sequels` if defined) |

As usual, depending on the scope of the customization, you can set the `:metadata` variable globally or for a single pagy instance.

**IMPORTANT**: Don't rely on the broad default! You should explicitly set the `:metadata` variable with only the names that you will actually use in the frontend, for obvious performance reasons.

### :scaffold_url key

This is a special url string that you can use as the scaffold to build real page urls in your frontend (instead of producing them on the backend).

It is a pagination url/path (complete with all the params) containing the `__pagy_page__` placeholder in place of the page number (e.g. `'/foo?page=__pagy_page__&bar=baz'`)

You can generate all the actual links on the frontend by simply replacing the placeholder with the actual page number you want to link to.

In javascript you can do something like:

```js
page_url = scaffold_url.replace(/__pagy_page__/, page_number)
```

This is particularly useful when you want to build some dynamic pagination UI (e.g. similar to what the `pagy_*combo_js` generates), but right in your frontend app, saving backend resources with obvious performance benefits.

**Notice**: for simple cases you might directly use the other few `:*_url` metadata instead of the `:scaffold_url`.

## Methods

This extra adds a single method to the `Pagy::Backend` (available in your controllers).

### pagy_metadata(pagy, url=false)

This method returns a hash with the keys/values defined by the `:metadata` variable. When true, the `url` boolean argument will cause all the `:*_url` metadata to be absolute instead of relative.
4 changes: 2 additions & 2 deletions docs/extras/semantic.md
@@ -1,5 +1,5 @@
---
title: Semantic UI
title: Semantic
---
# Semantic UI Extra

Expand Down Expand Up @@ -46,4 +46,4 @@ See more details in the [javascript navs](navs.md#javascript-navs) documentation

This method is the same as the `pagy_combo_nav_js`, but customized for the Semantic UI framework.

See more details in the [compact_navs_js](navs.md#javascript-combo-navs) documentation.
See more details in the [compact_navs_js](navs.md#javascript-combo-navs) documentation.
8 changes: 8 additions & 0 deletions lib/config/pagy.rb
Expand Up @@ -89,6 +89,14 @@
# require 'pagy/extras/overflow'
# Pagy::VARS[:overflow] = :empty_page # default (other options: :last_page and :exception)

# Metadata extra: Provides the pagination metadata to Javascrtipt frameworks like Vue.js, react.js, etc.
# See https://ddnexus.github.io/pagy/extras/metadata
# you must require the shared internal extra (BEFORE the metadata extra) ONLY if you need also the :sequels
# require 'pagy/extras/shared'
# require 'pagy/extras/metadata'
# For performance reason, you should explicitly set ONLY the metadata you use in the frontend
# Pagy::VARS[:metadata] = [:scaffold_url, :count, :page, :prev, :next, :last] # example

# Trim extra: Remove the page=1 param from links
# See https://ddnexus.github.io/pagy/extras/trim
# require 'pagy/extras/trim'
Expand Down
37 changes: 37 additions & 0 deletions lib/pagy/extras/metadata.rb
@@ -0,0 +1,37 @@
# See the Pagy documentation: https://ddnexus.github.io/pagy/extras/metadata
# encoding: utf-8
# frozen_string_literal: true

class Pagy
# Add a specialized backend method for pagination metadata
module Backend ; private

METADATA = [ :scaffold_url, :first_url, :prev_url, :page_url, :next_url, :last_url,
:count, :page, :items, :vars, :pages, :last, :from, :to, :prev, :next, :series ]
METADATA << :sequels if VARS.key?(:steps) # :steps gets defined along with the #sequels method

VARS[:metadata] = METADATA.dup

include Helpers

def pagy_metadata(pagy, url=false)
names = pagy.vars[:metadata]
(unknown = names - METADATA).empty? or raise(VariableError.new(pagy), "unknown metadata #{unknown.inspect}")
scaffold_url = pagy_url_for(Frontend::MARK, pagy, url)
metadata = {}
names.each do |key|
metadata[key] = case key
when :scaffold_url ; scaffold_url
when :first_url ; scaffold_url.sub(Frontend::MARK, 1.to_s)
when :prev_url ; scaffold_url.sub(Frontend::MARK, pagy.prev.to_s)
when :page_url ; scaffold_url.sub(Frontend::MARK, pagy.page.to_s)
when :next_url ; scaffold_url.sub(Frontend::MARK, pagy.next.to_s)
when :last_url ; scaffold_url.sub(Frontend::MARK, pagy.last.to_s)
else pagy.send(key)
end
end
metadata
end

end
end
2 changes: 1 addition & 1 deletion test/pagy/extras/headers_test.rb
Expand Up @@ -3,7 +3,7 @@

require_relative '../../test_helper'
require 'pagy/extras/headers'
require 'pagy/countless'
require 'pagy/extras/countless'

describe Pagy::Backend do

Expand Down
43 changes: 43 additions & 0 deletions test/pagy/extras/metadata_test.rb
@@ -0,0 +1,43 @@
# encoding: utf-8
# frozen_string_literal: true

require_relative '../../test_helper'
require 'pagy/extras/shared' # include :sequels in VARS[:metadata]
require 'pagy/extras/metadata'
require 'pagy/countless'

describe Pagy::Backend do

describe "#pagy_metadata" do

before do
@controller = MockController.new
@collection = MockCollection.new
end

it 'defines all metadata' do
Pagy::VARS[:metadata].must_equal [:scaffold_url, :first_url, :prev_url, :page_url, :next_url, :last_url, :count, :page, :items, :vars, :pages, :last, :from, :to, :prev, :next, :series, :sequels]
end

it 'returns the full pagy metadata' do
pagy, _records = @controller.send(:pagy, @collection)
@controller.send(:pagy_metadata, pagy).must_equal(
{:scaffold_url=>"/foo?page=__pagy_page__", :first_url=>"/foo?page=1", :prev_url=>"/foo?page=2", :page_url=>"/foo?page=3", :next_url=>"/foo?page=4", :last_url=>"/foo?page=50", :count=>1000, :page=>3, :items=>20, :vars=>{:page=>3, :items=>20, :outset=>0, :size=>[1, 4, 4, 1], :page_param=>:page, :params=>{}, :anchor=>"", :link_extra=>"", :i18n_key=>"pagy.item_name", :cycle=>false, :steps=>false, :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], :count=>1000}, :pages=>50, :last=>50, :from=>41, :to=>60, :prev=>2, :next=>4, :series=>[1, 2, "3", 4, 5, 6, 7, :gap, 50], :sequels=>{"0"=>[1, 2, "3", 4, 5, 6, 7, :gap, 50]}}
)
end

it 'checks for unknown metadata' do
pagy, _records = @controller.send(:pagy, @collection, metadata: [:page, :unknown_key])
proc { @controller.send(:pagy_metadata, pagy)}.must_raise Pagy::VariableError
end

it 'returns only specific metadata' do
pagy, _records = @controller.send(:pagy, @collection, metadata: [:scaffold_url, :page, :count, :prev, :next, :pages])
@controller.send(:pagy_metadata, pagy).must_equal(
{:scaffold_url=>"/foo?page=__pagy_page__", :page=>3, :count=>1000, :prev=>2, :next=>4, :pages=>50}
)
end

end

end

0 comments on commit 43349a8

Please sign in to comment.