:keynav_js  JavaScript Setup Required!
:keynav_js is a fast KEYSET paginator that supports the UI. It's a pagy exclusive technique.
The Keynav pagination adds the numeric variables (@page, @last, @previous, @next, @in) to its instances, supporting their usage with the UI. It does so by transparently exchanging data with the client, that stores the state of the pagination.
If something goes wrong on the client side, it falls back to the :countless paginator seamlessly.
Check it out with bundle exec pagy keynav
@pagy, @records = pagy(:keynav_js, collection, **options)
@pagyis the pagination instance. It provides thereaders and the helpers to use in your code.@recordsis the eager-loadedArrayof the page records.
keyset: {...}- Set it only to force the
keysethash of column/order pairs. (It is set automatically from the set order) tuple_comparison: true- Enable the tuple comparison e.g.
(brand, id) > (:brand, :id). It works only with the same direction order, hence, it's ignored for mixed order. Check how your DB supports it (yourkeysetshould include onlyNOT NULLcolumns). pre_serialize: serializeSet it to a
lambdathat receives thekeyset_attributeshash. Modify this hash directly to customize the serialization of specific values (e.g., to preserveTimeobject precision). The lambda's return value is ignored.serialize = lambda do |attributes| # Convert it to a string matching the stored value/format in SQLite DB attributes[:created_at] = attributes[:created_at].strftime('%F %T.%6N') endlimit: 10- Specifies the number of items per page (default:
20) client_max_limit: 1_000Set the maximum
:limitthat the client is allowed torequest. Higher requested:limits are silently capped.IMPORTANT If falsey, the client cannot request any
:limit.page: force_page- Set it only to force the current
:page. (It is set automatically from the request param). request: request || hashPagy tries to find the
Rake::Requestatself.request. Set it only when it's not directly available in your code (e.g., Hanami, standalone app, test,...). For example:hash_request = { base_url: 'http://www.example.com', path: '/path', params: { 'param1' => 1234 }, # The string-keyed params hash from the request cookie: 'xyz' } # The 'pagy' cookie, only for keynavjsonapi: true- Enables JSON:API-compliant URLs with nested query string (e.g.,
?page[number]=2&page[size]=100). root_key: 'my_root'- Set it to enable nested URLs with nested query string
?my_root[page]=2&my_root[limit]=100)). Use it to handle multiple pagination objects in the same request. page_key: 'my_page'- Set it to change the key string used for the
:pagein URLs (default'page'). limit_key: 'my_limit'- Set it to change the key string used for the
:limitin URLs (default'limit').
last- The last page.
pages- The number of pages.
previous- The previous page
next- The next page
page- The current page
limit- The items per page
in- The actual items in the page
records- The fetched records for the current page.
options- The hash of options of the object
Let's take a new look at the diagram of the keyset pagination explained in the Keyset documentation:
│ first page (10) >│ second page (10) >│ last page (9) >│
beginning of set >[· · · · · · · · · X]· · · · · · · · · Y]· · · · · · · · ·]< end of set
â–² â–²
cutoff-X cutoff-Y
Let's suppose that we navigate till page #3 (i.e., the last page), and we click on the link for page #2. We have stored the cutoff-X, so we can pull the 10 records after cutoff-X again as we did the first time... but are we sure that we would get the same results?
Let's suppose that the database just changed: 1 record was inserted before cutoff-X, and 2 records were deleted after cutoff-X...
│ page #1 (11) >│ page #2 (8) >│ page #3 (9) >│
beginning of set >[· · · · · · · · · · X]· · · · · · · Y]· · · · · · · · ·]< end of set
â–² â–²
cutoff-X cutoff-Y
At this point pulling 10 records from the cutoff-X would get also the first 2 records from page 3, if you navigate on page 3, you will pull the same 2 records again also for page #3.
Indeed, not only the results have changed, but the cutoffs appear to have also shifted their absolute position in the set. In reality, the cutoffs have the same value as before, so they maintained their relative position in the set. However, now there is a different number of records falling into the same pages, which is totally consistent with the changes, but possibly unexpected. That is... if you have the mindset of OFFSET pagination, where the pages are split by number of records (absolute position) and not by their position relative to the records in the set.
The main goal of pagination is to split the results into manageable chunks and ensure it is as fast and accurate as possible, so the variation in the page size seems not relevant to that. However, should it be relevant to you, you can always use the classic OFFSET pagination and accept its slowness and inaccuracy.
Pagy keynav doesn't use the LIMIT to pull the records of already visited pages. Instead, it replaces the LIMIT with the same filter used for the beginning of the page, but it just compounds it with the negated filter of the ending of the page.
For example, the filtering of the page could be logically described like:
- Page #1 `WHERE NOT AFTER CUTOFF-X` <- only ending filter
- Page #2 `WHERE AFTER CUTOFF-X AND NOT AFTER CUTOFF-Y` <- combined beginning + ending filter
- Page #3 `WHERE AFTER CUTOFF-Y LIMIT 10` <- regular beginning filter (no cutoff for last page)
Implementing page-rebalancing
When the number of records on a visited page has drastically changed, it would be helpful to mitigate the surprise effect on the user by:
- Automatically compacting the empty (or almost empty) visited pages.
- Automatically splitting the excessively grown visited pages.