pagy_infinite_scroll 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pagy_infinite_scroll/index.js +13 -0
- data/app/assets/javascripts/pagy_infinite_scroll/infinite_scroll_controller.js +151 -0
- data/app/assets/javascripts/pagy_infinite_scroll_controller.js +177 -0
- data/lib/generators/pagy_infinite_scroll/install/install_generator.rb +99 -0
- data/lib/generators/pagy_infinite_scroll/install/templates/initializer.rb +25 -0
- data/lib/pagy_infinite_scroll/configuration.rb +21 -0
- data/lib/pagy_infinite_scroll/controller_helper.rb +84 -0
- data/lib/pagy_infinite_scroll/engine.rb +53 -0
- data/lib/pagy_infinite_scroll/version.rb +5 -0
- data/lib/pagy_infinite_scroll/view_helper.rb +111 -0
- data/lib/pagy_infinite_scroll.rb +24 -0
- data/sig/pagy_infinite_scroll.rbs +4 -0
- metadata +132 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4030148cd309dc18ac97b40df152d44d8b6ba1895be2998c4d5f1a31e3855171
|
|
4
|
+
data.tar.gz: 341f3089353a36671eca179db57c402f7b028649cf839b6fb30a4437046aa2e9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: efb49c31d7b1e5b665089e2332b4e37ea112faf763c9bbdacf95efe11968aaa08e3b5904f9a057ee97e2572bb15bc1bd70ee7547f0546b8e698d54851093f61d
|
|
7
|
+
data.tar.gz: 0ce1a7f8f8bd6bbbd1d4cda4f5fbc53def5b850c6e8adc683e46af8934e7544dca9b6855bd9616ccdb86fcef84c21213ca5322f0446fc28cec74d80f68a5a31a
|
data/.rspec
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hassan Haroon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# PagyInfiniteScroll
|
|
2
|
+
|
|
3
|
+
A Rails gem that adds infinite scroll functionality using Pagy and Stimulus. Load initial records and automatically fetch more as users scroll down - perfect for optimizing pages with large datasets.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 Easy integration with existing Rails apps
|
|
8
|
+
- 📦 Built on Pagy (fast, lightweight pagination)
|
|
9
|
+
- ⚡ Stimulus controller for smooth infinite scrolling
|
|
10
|
+
- 🎨 Customizable HTML rendering
|
|
11
|
+
- 🔧 Configurable scroll threshold and items per page
|
|
12
|
+
- 💾 Preserves URL parameters during scroll
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'pagy_infinite_scroll', github: 'hassanharoon86/pagy_infinite_scroll'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
rails generate pagy_infinite_scroll:install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The generator will automatically detect your JavaScript setup and configure accordingly:
|
|
30
|
+
|
|
31
|
+
### For importmap-rails (auto-configured ✅)
|
|
32
|
+
The generator will:
|
|
33
|
+
- Add pin to `config/importmap.rb`
|
|
34
|
+
- Add import to `app/javascript/application.js`
|
|
35
|
+
- Auto-register controller with Stimulus
|
|
36
|
+
- **No manual steps needed!**
|
|
37
|
+
|
|
38
|
+
### For jsbundling-rails (esbuild/webpack/rollup)
|
|
39
|
+
The generator will:
|
|
40
|
+
- Create `config/initializers/pagy_infinite_scroll.rb`
|
|
41
|
+
- Copy controller to `app/javascript/controllers/pagy_infinite_scroll_controller.js`
|
|
42
|
+
- Display registration instructions
|
|
43
|
+
- Run `yarn build` after setup
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Controller Setup
|
|
48
|
+
|
|
49
|
+
Use the gem's helper methods:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class ProductsController < ApplicationController
|
|
53
|
+
def index
|
|
54
|
+
# Use pagy_infinite_scroll instead of regular pagy
|
|
55
|
+
@pagy, @products = pagy_infinite_scroll(Product.all, limit: 50)
|
|
56
|
+
|
|
57
|
+
respond_to do |format|
|
|
58
|
+
format.html
|
|
59
|
+
format.json do
|
|
60
|
+
# Use the JSON helper to format response
|
|
61
|
+
render json: pagy_infinite_scroll_json(@pagy, @products) { |product|
|
|
62
|
+
{
|
|
63
|
+
id: product.id,
|
|
64
|
+
title: product.title,
|
|
65
|
+
price: product.price
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. View Setup
|
|
75
|
+
|
|
76
|
+
Add the infinite scroll container to your view:
|
|
77
|
+
|
|
78
|
+
```erb
|
|
79
|
+
<div data-controller="pagy-infinite-scroll"
|
|
80
|
+
data-pagy-infinite-scroll-url-value="<%= products_path(format: :json) %>"
|
|
81
|
+
data-pagy-infinite-scroll-page-value="1"
|
|
82
|
+
data-pagy-infinite-scroll-has-more-value="<%= @pagy.next.present? %>"
|
|
83
|
+
style="max-height: 600px; overflow-y: auto;">
|
|
84
|
+
|
|
85
|
+
<!-- Container for items -->
|
|
86
|
+
<div data-pagy-infinite-scroll-target="itemsContainer">
|
|
87
|
+
<% @products.each do |product| %>
|
|
88
|
+
<div class="product-card">
|
|
89
|
+
<h3><%= product.title %></h3>
|
|
90
|
+
<p><%= product.price %></p>
|
|
91
|
+
</div>
|
|
92
|
+
<% end %>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Loading indicator -->
|
|
96
|
+
<div data-pagy-infinite-scroll-target="loadingIndicator" class="hidden">
|
|
97
|
+
<p>Loading more...</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## ⚠️ Important: Custom HTML Rendering
|
|
103
|
+
|
|
104
|
+
**The gem provides the core infinite scroll functionality, but you need to tell it how to render YOUR specific HTML.**
|
|
105
|
+
|
|
106
|
+
### Why?
|
|
107
|
+
|
|
108
|
+
The gem cannot know:
|
|
109
|
+
- Your specific HTML structure (forms, checkboxes, badges, etc.)
|
|
110
|
+
- Your CSS classes and styling
|
|
111
|
+
- Your form field names
|
|
112
|
+
- Your Stimulus action targets
|
|
113
|
+
|
|
114
|
+
### Solution: Extend the Controller
|
|
115
|
+
|
|
116
|
+
**For jsbundling-rails apps:**
|
|
117
|
+
|
|
118
|
+
Create a custom Stimulus controller that extends the gem's base controller:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// app/javascript/controllers/products_scroll_controller.js
|
|
122
|
+
import PagyInfiniteScrollController from "./pagy_infinite_scroll_controller"
|
|
123
|
+
|
|
124
|
+
export default class extends PagyInfiniteScrollController {
|
|
125
|
+
// Override this method to customize HTML for loaded items
|
|
126
|
+
createItemHTML(record) {
|
|
127
|
+
return `
|
|
128
|
+
<div class="product-card">
|
|
129
|
+
<h3>${record.title}</h3>
|
|
130
|
+
<p class="price">$${record.price}</p>
|
|
131
|
+
<span class="badge">${record.category}</span>
|
|
132
|
+
</div>
|
|
133
|
+
`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Register it in `app/javascript/controllers/index.js`:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
import ProductsScrollController from "./products_scroll_controller"
|
|
142
|
+
application.register("products-scroll", ProductsScrollController)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**For importmap-rails apps:**
|
|
146
|
+
|
|
147
|
+
The controller is available globally as `window.PagyInfiniteScrollController`:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
// app/javascript/controllers/products_scroll_controller.js
|
|
151
|
+
class ProductsScrollController extends window.PagyInfiniteScrollController {
|
|
152
|
+
createItemHTML(record) {
|
|
153
|
+
return `
|
|
154
|
+
<div class="product-card">
|
|
155
|
+
<h3>${record.title}</h3>
|
|
156
|
+
<p class="price">$${record.price}</p>
|
|
157
|
+
<span class="badge">${record.category}</span>
|
|
158
|
+
</div>
|
|
159
|
+
`
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Register with Stimulus
|
|
164
|
+
Stimulus.register("products-scroll", ProductsScrollController)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use in your view:
|
|
168
|
+
|
|
169
|
+
```erb
|
|
170
|
+
<div data-controller="products-scroll"
|
|
171
|
+
data-products-scroll-url-value="<%= products_path(format: :json) %>"
|
|
172
|
+
data-products-scroll-page-value="1"
|
|
173
|
+
data-products-scroll-has-more-value="<%= @pagy.next.present? %>">
|
|
174
|
+
...
|
|
175
|
+
</div>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Real-World Example: Form with Checkboxes
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// app/javascript/controllers/distribution_preferences_scroll_controller.js
|
|
182
|
+
import PagyInfiniteScrollController from "./pagy_infinite_scroll_controller"
|
|
183
|
+
|
|
184
|
+
export default class extends PagyInfiniteScrollController {
|
|
185
|
+
createItemHTML(record) {
|
|
186
|
+
const isChecked = record.selected ? 'checked' : ''
|
|
187
|
+
|
|
188
|
+
return `
|
|
189
|
+
<div class="flex items-center gap-4 p-4">
|
|
190
|
+
<input type="checkbox"
|
|
191
|
+
name="product_ids[]"
|
|
192
|
+
value="${record.id}"
|
|
193
|
+
id="product_${record.id}"
|
|
194
|
+
${isChecked}
|
|
195
|
+
class="form-checkbox">
|
|
196
|
+
|
|
197
|
+
<label for="product_${record.id}" class="flex-1">
|
|
198
|
+
<div class="font-medium">${record.title}</div>
|
|
199
|
+
${record.vendor_name ? `
|
|
200
|
+
<span class="badge">${record.vendor_name}</span>
|
|
201
|
+
` : ''}
|
|
202
|
+
<span class="text-gray-500">
|
|
203
|
+
${record.variants_count} variants
|
|
204
|
+
</span>
|
|
205
|
+
</label>
|
|
206
|
+
</div>
|
|
207
|
+
`
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Key Points:**
|
|
213
|
+
- Only ~60 lines of custom JavaScript needed
|
|
214
|
+
- The gem handles all the scroll detection, AJAX, and state management
|
|
215
|
+
- You only define the HTML structure for your specific use case
|
|
216
|
+
- This keeps your code DRY and maintainable
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
Edit `config/initializers/pagy_infinite_scroll.rb`:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
PagyInfiniteScroll.configure do |config|
|
|
224
|
+
config.items_per_page = 50 # Items per page (default: 25)
|
|
225
|
+
config.scroll_threshold = 100 # Pixels from bottom to trigger load (default: 100)
|
|
226
|
+
config.loading_indicator = true # Show loading indicator (default: true)
|
|
227
|
+
config.preserve_state = true # Preserve URL params (default: true)
|
|
228
|
+
config.debounce_delay = 500 # Debounce for search in ms (default: 500)
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Data Attributes Reference
|
|
233
|
+
|
|
234
|
+
### Container Attributes
|
|
235
|
+
- `data-pagy-infinite-scroll-url-value` - JSON endpoint URL (required)
|
|
236
|
+
- `data-pagy-infinite-scroll-page-value` - Current page number (default: 1)
|
|
237
|
+
- `data-pagy-infinite-scroll-has-more-value` - Has more pages? (required, true/false)
|
|
238
|
+
- `data-pagy-infinite-scroll-threshold-value` - Scroll threshold in pixels (optional)
|
|
239
|
+
|
|
240
|
+
### Targets
|
|
241
|
+
- `data-pagy-infinite-scroll-target="itemsContainer"` - Where to append items
|
|
242
|
+
- `data-pagy-infinite-scroll-target="loadingIndicator"` - Loading indicator element
|
|
243
|
+
|
|
244
|
+
## Events
|
|
245
|
+
|
|
246
|
+
The controller dispatches custom events you can listen to:
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
// In your custom controller
|
|
250
|
+
connect() {
|
|
251
|
+
super.connect()
|
|
252
|
+
|
|
253
|
+
this.element.addEventListener('pagy-infinite-scroll:loaded', (event) => {
|
|
254
|
+
console.log('Loaded page:', event.detail.page)
|
|
255
|
+
console.log('Has more:', event.detail.hasMore)
|
|
256
|
+
console.log('Count:', event.detail.count)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
this.element.addEventListener('pagy-infinite-scroll:error', (event) => {
|
|
260
|
+
console.error('Error:', event.detail.error)
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Troubleshooting
|
|
266
|
+
|
|
267
|
+
### Items not loading on scroll
|
|
268
|
+
|
|
269
|
+
1. **Check browser console** for JavaScript errors
|
|
270
|
+
2. **Verify JSON endpoint** returns correct format:
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"records": [...],
|
|
274
|
+
"pagy": {
|
|
275
|
+
"page": 1,
|
|
276
|
+
"pages": 10,
|
|
277
|
+
"next": 2,
|
|
278
|
+
"count": 500
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
3. **Check Network tab** to see if AJAX requests are being made
|
|
283
|
+
4. **Verify controller is connected** - should see console log on page load
|
|
284
|
+
|
|
285
|
+
### Controller not found error
|
|
286
|
+
|
|
287
|
+
Run `yarn build` (or `npm run build`) to rebuild JavaScript bundle.
|
|
288
|
+
|
|
289
|
+
### Helpers not available in controller
|
|
290
|
+
|
|
291
|
+
Restart your Rails server after installing the gem.
|
|
292
|
+
|
|
293
|
+
### Items render as JSON instead of HTML
|
|
294
|
+
|
|
295
|
+
You need to create a custom controller that extends the base controller and overrides the `createItemHTML()` method (see "Custom HTML Rendering" section above).
|
|
296
|
+
|
|
297
|
+
## JavaScript Setup: importmap vs jsbundling
|
|
298
|
+
|
|
299
|
+
This gem works with **both** importmap-rails and jsbundling-rails:
|
|
300
|
+
|
|
301
|
+
### importmap-rails ✅
|
|
302
|
+
- **Setup**: Automatic via generator
|
|
303
|
+
- **Controller**: Standalone file, auto-registers with Stimulus
|
|
304
|
+
- **Extending**: Use `window.PagyInfiniteScrollController` as base class
|
|
305
|
+
- **Advantages**: Zero build step, simpler setup
|
|
306
|
+
- **File location**: Served from gem's assets
|
|
307
|
+
|
|
308
|
+
### jsbundling-rails (esbuild/webpack/rollup) ✅
|
|
309
|
+
- **Setup**: Manual file copy (bundlers can't access gem paths)
|
|
310
|
+
- **Controller**: Import from `./pagy_infinite_scroll_controller`
|
|
311
|
+
- **Extending**: Use ES6 `import` and `extends`
|
|
312
|
+
- **Advantages**: Full ES6 module support, tree-shaking
|
|
313
|
+
- **File location**: `app/javascript/controllers/`
|
|
314
|
+
|
|
315
|
+
Both setups support the same features and API!
|
|
316
|
+
|
|
317
|
+
## Requirements
|
|
318
|
+
|
|
319
|
+
- Rails 7.0+
|
|
320
|
+
- Pagy gem (add `gem 'pagy'` to your Gemfile)
|
|
321
|
+
- Stimulus (Hotwire)
|
|
322
|
+
- Either importmap-rails OR jsbundling-rails (esbuild, webpack, or rollup)
|
|
323
|
+
|
|
324
|
+
## Contributing
|
|
325
|
+
|
|
326
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hassanharoon86/pagy_infinite_scroll/issues
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Pagy Infinite Scroll - Main Entry Point
|
|
2
|
+
// This file provides the controller for manual registration
|
|
3
|
+
|
|
4
|
+
import PagyInfiniteScrollController from "./infinite_scroll_controller"
|
|
5
|
+
|
|
6
|
+
// Export for manual registration in host app
|
|
7
|
+
export { PagyInfiniteScrollController }
|
|
8
|
+
export default PagyInfiniteScrollController
|
|
9
|
+
|
|
10
|
+
// Auto-register if Stimulus application is available globally
|
|
11
|
+
if (typeof window !== 'undefined' && window.Stimulus) {
|
|
12
|
+
window.Stimulus.register("pagy-infinite-scroll", PagyInfiniteScrollController)
|
|
13
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Infinite Scroll Controller for Pagy
|
|
4
|
+
// Can be used standalone or extended
|
|
5
|
+
export default class PagyInfiniteScrollController extends Controller {
|
|
6
|
+
static targets = ['itemsContainer', 'loadingIndicator']
|
|
7
|
+
static values = {
|
|
8
|
+
url: String,
|
|
9
|
+
page: Number,
|
|
10
|
+
loading: Boolean,
|
|
11
|
+
hasMore: Boolean,
|
|
12
|
+
threshold: { type: Number, default: 100 },
|
|
13
|
+
preserveState: { type: Boolean, default: true }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
console.log('[PagyInfiniteScroll] Controller connected')
|
|
18
|
+
this.pageValue = this.pageValue || 1
|
|
19
|
+
this.loadingValue = false
|
|
20
|
+
this.boundHandleScroll = this.handleScroll.bind(this)
|
|
21
|
+
this.element.addEventListener('scroll', this.boundHandleScroll)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnect() {
|
|
25
|
+
this.element.removeEventListener('scroll', this.boundHandleScroll)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleScroll() {
|
|
29
|
+
const scrollThreshold = this.hasThresholdValue ? this.thresholdValue : 100
|
|
30
|
+
const nearBottom = this.element.scrollHeight - this.element.scrollTop - this.element.clientHeight < scrollThreshold
|
|
31
|
+
|
|
32
|
+
if (nearBottom && !this.loadingValue && this.hasMoreValue) {
|
|
33
|
+
this.loadMore()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async loadMore() {
|
|
38
|
+
if (this.loadingValue || !this.hasMoreValue) return
|
|
39
|
+
|
|
40
|
+
console.log('[PagyInfiniteScroll] Loading more items...')
|
|
41
|
+
this.loadingValue = true
|
|
42
|
+
this.showLoading()
|
|
43
|
+
|
|
44
|
+
const nextPage = this.pageValue + 1
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(this.urlValue, window.location.origin)
|
|
48
|
+
url.searchParams.set('page', nextPage)
|
|
49
|
+
|
|
50
|
+
// Preserve current URL parameters
|
|
51
|
+
const currentParams = new URLSearchParams(window.location.search)
|
|
52
|
+
currentParams.forEach((value, key) => {
|
|
53
|
+
if (key !== 'page') {
|
|
54
|
+
url.searchParams.set(key, value)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
headers: {
|
|
60
|
+
'Accept': 'application/json',
|
|
61
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = await response.json()
|
|
70
|
+
|
|
71
|
+
// Dispatch event with data for custom handling
|
|
72
|
+
const event = this.dispatch('beforeAppend', {
|
|
73
|
+
detail: { data },
|
|
74
|
+
cancelable: true
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!event.defaultPrevented) {
|
|
78
|
+
this.appendItems(data)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Update pagination state
|
|
82
|
+
this.pageValue = data.pagy.page
|
|
83
|
+
this.hasMoreValue = data.pagy.next !== null
|
|
84
|
+
|
|
85
|
+
console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
|
|
86
|
+
|
|
87
|
+
// Dispatch success event
|
|
88
|
+
this.dispatch('loaded', {
|
|
89
|
+
detail: {
|
|
90
|
+
page: this.pageValue,
|
|
91
|
+
hasMore: this.hasMoreValue,
|
|
92
|
+
count: data.records.length
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[PagyInfiniteScroll] Error loading more items:', error)
|
|
98
|
+
this.dispatch('error', { detail: { error } })
|
|
99
|
+
} finally {
|
|
100
|
+
this.loadingValue = false
|
|
101
|
+
this.hideLoading()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
appendItems(data) {
|
|
106
|
+
if (!this.hasItemsContainerTarget) {
|
|
107
|
+
console.warn('[PagyInfiniteScroll] No items container target found')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const records = data.records || data.items || data.products || []
|
|
112
|
+
|
|
113
|
+
records.forEach(record => {
|
|
114
|
+
const html = this.createItemHTML(record)
|
|
115
|
+
this.itemsContainerTarget.insertAdjacentHTML('beforeend', html)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
createItemHTML(record) {
|
|
120
|
+
// This method should be overridden by the application
|
|
121
|
+
// or use a custom event listener
|
|
122
|
+
console.warn('[PagyInfiniteScroll] createItemHTML should be overridden')
|
|
123
|
+
return `<div>${JSON.stringify(record)}</div>`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
showLoading() {
|
|
127
|
+
if (this.hasLoadingIndicatorTarget) {
|
|
128
|
+
this.loadingIndicatorTarget.classList.remove('hidden')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hideLoading() {
|
|
133
|
+
if (this.hasLoadingIndicatorTarget) {
|
|
134
|
+
this.loadingIndicatorTarget.classList.add('hidden')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Public API methods
|
|
139
|
+
reset() {
|
|
140
|
+
this.pageValue = 1
|
|
141
|
+
this.hasMoreValue = true
|
|
142
|
+
if (this.hasItemsContainerTarget) {
|
|
143
|
+
this.itemsContainerTarget.innerHTML = ''
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
reload() {
|
|
148
|
+
this.reset()
|
|
149
|
+
this.loadMore()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Pagy Infinite Scroll Controller - Importmap Compatible Version
|
|
2
|
+
// This is a standalone file that works with both importmap-rails and jsbundling-rails
|
|
3
|
+
//
|
|
4
|
+
// For importmap-rails: This file will be pinned and auto-loaded
|
|
5
|
+
// For jsbundling-rails: Import from the modular version in app/javascript/controllers/
|
|
6
|
+
|
|
7
|
+
// IIFE to avoid polluting global scope while still working with importmap
|
|
8
|
+
(() => {
|
|
9
|
+
// Check if Stimulus is available
|
|
10
|
+
if (typeof Stimulus === 'undefined') {
|
|
11
|
+
console.error('[PagyInfiniteScroll] Stimulus is not loaded. Make sure @hotwired/stimulus is loaded before this controller.')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Define the controller class
|
|
16
|
+
class PagyInfiniteScrollController extends Stimulus.Controller {
|
|
17
|
+
static targets = ['itemsContainer', 'loadingIndicator']
|
|
18
|
+
static values = {
|
|
19
|
+
url: String,
|
|
20
|
+
page: Number,
|
|
21
|
+
loading: Boolean,
|
|
22
|
+
hasMore: Boolean,
|
|
23
|
+
threshold: { type: Number, default: 100 },
|
|
24
|
+
preserveState: { type: Boolean, default: true }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
connect() {
|
|
28
|
+
console.log('[PagyInfiniteScroll] Controller connected')
|
|
29
|
+
this.pageValue = this.pageValue || 1
|
|
30
|
+
this.loadingValue = false
|
|
31
|
+
this.boundHandleScroll = this.handleScroll.bind(this)
|
|
32
|
+
this.element.addEventListener('scroll', this.boundHandleScroll)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnect() {
|
|
36
|
+
this.element.removeEventListener('scroll', this.boundHandleScroll)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleScroll() {
|
|
40
|
+
const scrollThreshold = this.hasThresholdValue ? this.thresholdValue : 100
|
|
41
|
+
const nearBottom = this.element.scrollHeight - this.element.scrollTop - this.element.clientHeight < scrollThreshold
|
|
42
|
+
|
|
43
|
+
if (nearBottom && !this.loadingValue && this.hasMoreValue) {
|
|
44
|
+
this.loadMore()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async loadMore() {
|
|
49
|
+
if (this.loadingValue || !this.hasMoreValue) return
|
|
50
|
+
|
|
51
|
+
console.log('[PagyInfiniteScroll] Loading more items...')
|
|
52
|
+
this.loadingValue = true
|
|
53
|
+
this.showLoading()
|
|
54
|
+
|
|
55
|
+
const nextPage = this.pageValue + 1
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(this.urlValue, window.location.origin)
|
|
59
|
+
url.searchParams.set('page', nextPage)
|
|
60
|
+
|
|
61
|
+
// Preserve current URL parameters
|
|
62
|
+
const currentParams = new URLSearchParams(window.location.search)
|
|
63
|
+
currentParams.forEach((value, key) => {
|
|
64
|
+
if (key !== 'page') {
|
|
65
|
+
url.searchParams.set(key, value)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
headers: {
|
|
71
|
+
'Accept': 'application/json',
|
|
72
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = await response.json()
|
|
81
|
+
|
|
82
|
+
// Dispatch event with data for custom handling
|
|
83
|
+
const event = this.dispatch('beforeAppend', {
|
|
84
|
+
detail: { data },
|
|
85
|
+
cancelable: true
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!event.defaultPrevented) {
|
|
89
|
+
this.appendItems(data)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Update pagination state
|
|
93
|
+
this.pageValue = data.pagy.page
|
|
94
|
+
this.hasMoreValue = data.pagy.next !== null
|
|
95
|
+
|
|
96
|
+
console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
|
|
97
|
+
|
|
98
|
+
// Dispatch success event
|
|
99
|
+
this.dispatch('loaded', {
|
|
100
|
+
detail: {
|
|
101
|
+
page: this.pageValue,
|
|
102
|
+
hasMore: this.hasMoreValue,
|
|
103
|
+
count: data.records.length
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('[PagyInfiniteScroll] Error loading more items:', error)
|
|
109
|
+
this.dispatch('error', { detail: { error } })
|
|
110
|
+
} finally {
|
|
111
|
+
this.loadingValue = false
|
|
112
|
+
this.hideLoading()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
appendItems(data) {
|
|
117
|
+
if (!this.hasItemsContainerTarget) {
|
|
118
|
+
console.warn('[PagyInfiniteScroll] No items container target found')
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const records = data.records || data.items || data.products || []
|
|
123
|
+
|
|
124
|
+
records.forEach(record => {
|
|
125
|
+
const html = this.createItemHTML(record)
|
|
126
|
+
this.itemsContainerTarget.insertAdjacentHTML('beforeend', html)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
createItemHTML(record) {
|
|
131
|
+
// This method should be overridden by extending this controller
|
|
132
|
+
// or by listening to the 'beforeAppend' event
|
|
133
|
+
console.warn('[PagyInfiniteScroll] createItemHTML should be overridden in a custom controller')
|
|
134
|
+
return `<div class="pagy-item">${JSON.stringify(record)}</div>`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
showLoading() {
|
|
138
|
+
if (this.hasLoadingIndicatorTarget) {
|
|
139
|
+
this.loadingIndicatorTarget.classList.remove('hidden')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hideLoading() {
|
|
144
|
+
if (this.hasLoadingIndicatorTarget) {
|
|
145
|
+
this.loadingIndicatorTarget.classList.add('hidden')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Public API methods
|
|
150
|
+
reset() {
|
|
151
|
+
this.pageValue = 1
|
|
152
|
+
this.hasMoreValue = true
|
|
153
|
+
if (this.hasItemsContainerTarget) {
|
|
154
|
+
this.itemsContainerTarget.innerHTML = ''
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
reload() {
|
|
159
|
+
this.reset()
|
|
160
|
+
this.loadMore()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Register the controller with Stimulus
|
|
165
|
+
if (window.Stimulus) {
|
|
166
|
+
window.Stimulus.register('pagy-infinite-scroll', PagyInfiniteScrollController)
|
|
167
|
+
console.log('[PagyInfiniteScroll] Controller registered with Stimulus')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Also export for manual registration (jsbundling compatibility)
|
|
171
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
172
|
+
module.exports = PagyInfiniteScrollController
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Make available globally for importmap apps that want to extend it
|
|
176
|
+
window.PagyInfiniteScrollController = PagyInfiniteScrollController
|
|
177
|
+
})()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/base'
|
|
4
|
+
|
|
5
|
+
module PagyInfiniteScroll
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Install Pagy Infinite Scroll into your application"
|
|
11
|
+
|
|
12
|
+
def create_initializer
|
|
13
|
+
template "initializer.rb", "config/initializers/pagy_infinite_scroll.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_javascript_import
|
|
17
|
+
if using_importmap?
|
|
18
|
+
add_importmap_pin
|
|
19
|
+
elsif using_jsbundling?
|
|
20
|
+
add_jsbundling_import
|
|
21
|
+
else
|
|
22
|
+
say "Please manually import the Stimulus controller in your JavaScript setup", :yellow
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show_post_install_message
|
|
27
|
+
say "\n"
|
|
28
|
+
say "Pagy Infinite Scroll installed successfully!", :green
|
|
29
|
+
say "\n"
|
|
30
|
+
say "Next steps:", :yellow
|
|
31
|
+
say " 1. Make sure you have Pagy gem installed: gem 'pagy'"
|
|
32
|
+
say "\n"
|
|
33
|
+
|
|
34
|
+
if using_importmap?
|
|
35
|
+
say " 2. Importmap setup (already configured!):", :green
|
|
36
|
+
say " - Pin added to config/importmap.rb"
|
|
37
|
+
say " - Import added to app/javascript/application.js"
|
|
38
|
+
say " - Controller auto-registers with Stimulus on page load"
|
|
39
|
+
say " - No manual registration needed!"
|
|
40
|
+
elsif using_jsbundling?
|
|
41
|
+
say " 2. jsbundling setup:", :yellow
|
|
42
|
+
say " Copy the controller to your app:"
|
|
43
|
+
say " cp $(bundle show pagy_infinite_scroll)/app/assets/javascripts/pagy_infinite_scroll/infinite_scroll_controller.js app/javascript/controllers/pagy_infinite_scroll_controller.js"
|
|
44
|
+
say "\n"
|
|
45
|
+
say " Then register in app/javascript/controllers/index.js:"
|
|
46
|
+
say " import PagyInfiniteScrollController from './pagy_infinite_scroll_controller'"
|
|
47
|
+
say " application.register('pagy-infinite-scroll', PagyInfiniteScrollController)"
|
|
48
|
+
say "\n"
|
|
49
|
+
say " Finally run: yarn build (or npm run build)"
|
|
50
|
+
else
|
|
51
|
+
say " 2. Manual setup required", :yellow
|
|
52
|
+
say " See documentation for your JavaScript setup"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
say "\n"
|
|
56
|
+
say " 3. Use in your controllers: pagy_infinite_scroll(collection)"
|
|
57
|
+
say " 4. See full documentation: https://github.com/hassanharoon86/pagy_infinite_scroll"
|
|
58
|
+
say "\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def using_importmap?
|
|
64
|
+
File.exist?("config/importmap.rb")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def using_jsbundling?
|
|
68
|
+
File.exist?("package.json") && (
|
|
69
|
+
File.exist?("app/javascript/application.js") ||
|
|
70
|
+
File.exist?("app/javascript/packs/application.js")
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_importmap_pin
|
|
75
|
+
# Pin the standalone importmap-compatible controller
|
|
76
|
+
append_to_file "config/importmap.rb" do
|
|
77
|
+
"\n# Pagy Infinite Scroll - Standalone controller (auto-registers with Stimulus)\npin 'pagy_infinite_scroll_controller', to: 'pagy_infinite_scroll_controller.js'\n"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add import to application.js if it exists
|
|
81
|
+
if File.exist?("app/javascript/application.js")
|
|
82
|
+
append_to_file "app/javascript/application.js" do
|
|
83
|
+
"\n// Pagy Infinite Scroll - Auto-registers with Stimulus\nimport 'pagy_infinite_scroll_controller'\n"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
say "\n"
|
|
88
|
+
say "Importmap configuration added!", :green
|
|
89
|
+
say "The controller will auto-register with Stimulus when loaded.", :green
|
|
90
|
+
say "\n"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_jsbundling_import
|
|
94
|
+
say "For jsbundling, you'll need to manually register the controller", :yellow
|
|
95
|
+
say "See the post-install message above for instructions", :yellow
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Pagy Infinite Scroll Configuration
|
|
2
|
+
#
|
|
3
|
+
# Configure the default settings for infinite scroll pagination
|
|
4
|
+
|
|
5
|
+
PagyInfiniteScroll.configure do |config|
|
|
6
|
+
# Number of items to load per page
|
|
7
|
+
# Default: 50
|
|
8
|
+
config.items_per_page = 50
|
|
9
|
+
|
|
10
|
+
# Distance from bottom of container in pixels to trigger loading more items
|
|
11
|
+
# Default: 100
|
|
12
|
+
config.scroll_threshold = 100
|
|
13
|
+
|
|
14
|
+
# Show loading indicator while fetching new items
|
|
15
|
+
# Default: true
|
|
16
|
+
config.loading_indicator = true
|
|
17
|
+
|
|
18
|
+
# Preserve form state and URL parameters during infinite scroll
|
|
19
|
+
# Default: true
|
|
20
|
+
config.preserve_state = true
|
|
21
|
+
|
|
22
|
+
# Debounce delay for search in milliseconds
|
|
23
|
+
# Default: 500
|
|
24
|
+
config.debounce_delay = 500
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PagyInfiniteScroll
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :items_per_page,
|
|
6
|
+
:scroll_threshold,
|
|
7
|
+
:loading_indicator,
|
|
8
|
+
:auto_initialize,
|
|
9
|
+
:preserve_state,
|
|
10
|
+
:debounce_delay
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@items_per_page = 50
|
|
14
|
+
@scroll_threshold = 100 # pixels from bottom
|
|
15
|
+
@loading_indicator = true
|
|
16
|
+
@auto_initialize = true
|
|
17
|
+
@preserve_state = true
|
|
18
|
+
@debounce_delay = 500 # milliseconds
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module PagyInfiniteScroll
|
|
6
|
+
module ControllerHelper
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
begin
|
|
11
|
+
require 'pagy'
|
|
12
|
+
include Pagy::Backend
|
|
13
|
+
rescue LoadError
|
|
14
|
+
raise LoadError, "Pagy gem is required. Add 'gem \"pagy\"' to your Gemfile and run 'bundle install'"
|
|
15
|
+
rescue NameError
|
|
16
|
+
raise NameError, "Pagy::Backend not found. Make sure Pagy is properly installed."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Pagy with infinite scroll support
|
|
21
|
+
#
|
|
22
|
+
# @param collection [ActiveRecord::Relation] The collection to paginate
|
|
23
|
+
# @param options [Hash] Options for pagy and infinite scroll
|
|
24
|
+
# @return [Array<Pagy, Array>] The pagy object and paginated collection
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# @pagy, @products = pagy_infinite_scroll(Product.all)
|
|
28
|
+
#
|
|
29
|
+
# @example With custom options
|
|
30
|
+
# @pagy, @products = pagy_infinite_scroll(Product.all, limit: 25)
|
|
31
|
+
#
|
|
32
|
+
def pagy_infinite_scroll(collection, **options)
|
|
33
|
+
options[:limit] ||= PagyInfiniteScroll.config.items_per_page
|
|
34
|
+
pagy, records = pagy(collection, **options)
|
|
35
|
+
|
|
36
|
+
[pagy, records]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Respond with infinite scroll format
|
|
40
|
+
#
|
|
41
|
+
# @param pagy [Pagy] The pagy object
|
|
42
|
+
# @param records [Array] The paginated records
|
|
43
|
+
# @param serializer [Proc, nil] Optional custom serializer for records
|
|
44
|
+
# @yield [record] Block to serialize each record
|
|
45
|
+
# @return [Hash] JSON response with records and pagination data
|
|
46
|
+
#
|
|
47
|
+
# @example Basic usage
|
|
48
|
+
# respond_to do |format|
|
|
49
|
+
# format.html
|
|
50
|
+
# format.json do
|
|
51
|
+
# render json: pagy_infinite_scroll_json(@pagy, @products) { |product|
|
|
52
|
+
# {
|
|
53
|
+
# id: product.id,
|
|
54
|
+
# title: product.title,
|
|
55
|
+
# price: product.price
|
|
56
|
+
# }
|
|
57
|
+
# }
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
def pagy_infinite_scroll_json(pagy, records, &serializer)
|
|
62
|
+
{
|
|
63
|
+
records: serialize_records(records, &serializer),
|
|
64
|
+
pagy: {
|
|
65
|
+
page: pagy.page,
|
|
66
|
+
pages: pagy.pages,
|
|
67
|
+
count: pagy.count,
|
|
68
|
+
next: pagy.next,
|
|
69
|
+
prev: pagy.prev,
|
|
70
|
+
from: pagy.from,
|
|
71
|
+
to: pagy.to
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def serialize_records(records, &serializer)
|
|
79
|
+
return records unless block_given?
|
|
80
|
+
|
|
81
|
+
records.map { |record| serializer.call(record) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module PagyInfiniteScroll
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
# Don't isolate namespace to allow helpers to load into main app
|
|
8
|
+
engine_name 'pagy_infinite_scroll'
|
|
9
|
+
|
|
10
|
+
# Ensure lib files are loaded
|
|
11
|
+
config.eager_load_paths << File.expand_path("../../", __FILE__)
|
|
12
|
+
|
|
13
|
+
# Load assets if asset pipeline is available
|
|
14
|
+
initializer "pagy_infinite_scroll.assets", before: :load_config_initializers do |app|
|
|
15
|
+
if app.config.respond_to?(:assets)
|
|
16
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
17
|
+
app.config.assets.precompile += %w[
|
|
18
|
+
pagy_infinite_scroll/infinite_scroll_controller.js
|
|
19
|
+
pagy_infinite_scroll/index.js
|
|
20
|
+
pagy_infinite_scroll_controller.js
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Support for importmap-rails
|
|
26
|
+
initializer "pagy_infinite_scroll.importmap", before: "importmap" do |app|
|
|
27
|
+
if defined?(Importmap)
|
|
28
|
+
# Make the standalone controller available to importmap
|
|
29
|
+
app.config.assets.paths << root.join("app/assets/javascripts") unless app.config.assets.paths.include?(root.join("app/assets/javascripts"))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Load helpers into ActionController and ActionView
|
|
34
|
+
initializer "pagy_infinite_scroll.helpers" do
|
|
35
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
36
|
+
helper PagyInfiniteScroll::ViewHelper if defined?(PagyInfiniteScroll::ViewHelper)
|
|
37
|
+
include PagyInfiniteScroll::ControllerHelper if defined?(PagyInfiniteScroll::ControllerHelper)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ActiveSupport.on_load(:action_view) do
|
|
41
|
+
include PagyInfiniteScroll::ViewHelper if defined?(PagyInfiniteScroll::ViewHelper)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fallback for direct inclusion
|
|
46
|
+
config.after_initialize do
|
|
47
|
+
if defined?(ActionController::Base)
|
|
48
|
+
ActionController::Base.include(PagyInfiniteScroll::ControllerHelper) unless ActionController::Base.include?(PagyInfiniteScroll::ControllerHelper)
|
|
49
|
+
ActionController::Base.helper(PagyInfiniteScroll::ViewHelper) if defined?(PagyInfiniteScroll::ViewHelper)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PagyInfiniteScroll
|
|
4
|
+
module ViewHelper
|
|
5
|
+
# Renders an infinite scroll container
|
|
6
|
+
#
|
|
7
|
+
# @param pagy [Pagy] The pagy object
|
|
8
|
+
# @param url [String] The URL to fetch more items
|
|
9
|
+
# @param options [Hash] HTML and Stimulus options
|
|
10
|
+
# @option options [String] :container_class CSS classes for the container
|
|
11
|
+
# @option options [String] :max_height Maximum height (e.g., "600px")
|
|
12
|
+
# @option options [Hash] :data Additional data attributes
|
|
13
|
+
# @yield The content to render inside the container
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# <%= infinite_scroll_container(@pagy, products_path(format: :json)) do %>
|
|
17
|
+
# <div data-infinite-scroll-target="itemsContainer">
|
|
18
|
+
# <%= render @products %>
|
|
19
|
+
# </div>
|
|
20
|
+
# <% end %>
|
|
21
|
+
#
|
|
22
|
+
def infinite_scroll_container(pagy, url, options = {}, &block)
|
|
23
|
+
container_class = options[:container_class] || ''
|
|
24
|
+
max_height = options[:max_height] || '600px'
|
|
25
|
+
data_attrs = options[:data] || {}
|
|
26
|
+
|
|
27
|
+
content_tag :div,
|
|
28
|
+
class: container_class,
|
|
29
|
+
style: "max-height: #{max_height}; overflow-y: auto;",
|
|
30
|
+
data: {
|
|
31
|
+
controller: 'pagy-infinite-scroll',
|
|
32
|
+
pagy_infinite_scroll_url_value: url,
|
|
33
|
+
pagy_infinite_scroll_page_value: pagy.page,
|
|
34
|
+
pagy_infinite_scroll_loading_value: false,
|
|
35
|
+
pagy_infinite_scroll_has_more_value: pagy.next.present?,
|
|
36
|
+
**stimulus_data_attributes(data_attrs)
|
|
37
|
+
} do
|
|
38
|
+
block.call
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Renders the loading indicator
|
|
43
|
+
#
|
|
44
|
+
# @param options [Hash] Options for the loading indicator
|
|
45
|
+
# @option options [String] :text Loading text
|
|
46
|
+
# @option options [String] :class CSS classes
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# <%= infinite_scroll_loading_indicator %>
|
|
50
|
+
#
|
|
51
|
+
# @example Custom text
|
|
52
|
+
# <%= infinite_scroll_loading_indicator(text: "Loading products...") %>
|
|
53
|
+
#
|
|
54
|
+
def infinite_scroll_loading_indicator(options = {})
|
|
55
|
+
text = options[:text] || 'Loading more items...'
|
|
56
|
+
css_class = options[:class] || 'hidden p-4 text-center'
|
|
57
|
+
|
|
58
|
+
content_tag :div,
|
|
59
|
+
class: css_class,
|
|
60
|
+
data: { pagy_infinite_scroll_target: 'loadingIndicator' } do
|
|
61
|
+
content_tag :div, class: 'inline-flex items-center gap-2 text-gray-600' do
|
|
62
|
+
concat(spinner_svg)
|
|
63
|
+
concat(content_tag(:span, text))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Renders a scroll target container for items
|
|
69
|
+
#
|
|
70
|
+
# @param options [Hash] HTML options
|
|
71
|
+
# @option options [String] :class CSS classes
|
|
72
|
+
# @option options [String] :tag HTML tag (default: 'div')
|
|
73
|
+
# @yield The content to render inside the container
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# <%= infinite_scroll_items_container do %>
|
|
77
|
+
# <%= render @products %>
|
|
78
|
+
# <% end %>
|
|
79
|
+
#
|
|
80
|
+
# @example With table rows
|
|
81
|
+
# <%= infinite_scroll_items_container(tag: 'tbody') do %>
|
|
82
|
+
# <%= render @items %>
|
|
83
|
+
# <% end %>
|
|
84
|
+
#
|
|
85
|
+
def infinite_scroll_items_container(options = {}, &block)
|
|
86
|
+
tag = options.delete(:tag) || 'div'
|
|
87
|
+
css_class = options.delete(:class) || ''
|
|
88
|
+
|
|
89
|
+
content_tag tag,
|
|
90
|
+
class: css_class,
|
|
91
|
+
data: { pagy_infinite_scroll_target: 'itemsContainer' } do
|
|
92
|
+
block.call
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def spinner_svg
|
|
99
|
+
<<~SVG.html_safe
|
|
100
|
+
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
101
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
102
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
103
|
+
</svg>
|
|
104
|
+
SVG
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def stimulus_data_attributes(attrs)
|
|
108
|
+
attrs.transform_keys { |key| "pagy_infinite_scroll_#{key}_value".to_sym }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pagy_infinite_scroll/version"
|
|
4
|
+
require_relative "pagy_infinite_scroll/configuration"
|
|
5
|
+
require_relative "pagy_infinite_scroll/controller_helper"
|
|
6
|
+
require_relative "pagy_infinite_scroll/view_helper"
|
|
7
|
+
require_relative "pagy_infinite_scroll/engine" if defined?(Rails)
|
|
8
|
+
|
|
9
|
+
module PagyInfiniteScroll
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :configuration
|
|
14
|
+
|
|
15
|
+
def configure
|
|
16
|
+
self.configuration ||= Configuration.new
|
|
17
|
+
yield(configuration) if block_given?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config
|
|
21
|
+
self.configuration ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pagy_infinite_scroll
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hassan Haroon
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pagy
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '6.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: sqlite3
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.4'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.4'
|
|
82
|
+
description: A Rails gem that adds infinite scroll functionality to any Rails application
|
|
83
|
+
using Pagy for efficient pagination and Stimulus for smooth frontend interactions.
|
|
84
|
+
Features include automatic lazy loading, state preservation, AJAX support, and customizable
|
|
85
|
+
behavior.
|
|
86
|
+
email:
|
|
87
|
+
- hassanharoon86@gmail.com
|
|
88
|
+
executables: []
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- ".rspec"
|
|
93
|
+
- LICENSE.txt
|
|
94
|
+
- README.md
|
|
95
|
+
- Rakefile
|
|
96
|
+
- app/assets/javascripts/pagy_infinite_scroll/index.js
|
|
97
|
+
- app/assets/javascripts/pagy_infinite_scroll/infinite_scroll_controller.js
|
|
98
|
+
- app/assets/javascripts/pagy_infinite_scroll_controller.js
|
|
99
|
+
- lib/generators/pagy_infinite_scroll/install/install_generator.rb
|
|
100
|
+
- lib/generators/pagy_infinite_scroll/install/templates/initializer.rb
|
|
101
|
+
- lib/pagy_infinite_scroll.rb
|
|
102
|
+
- lib/pagy_infinite_scroll/configuration.rb
|
|
103
|
+
- lib/pagy_infinite_scroll/controller_helper.rb
|
|
104
|
+
- lib/pagy_infinite_scroll/engine.rb
|
|
105
|
+
- lib/pagy_infinite_scroll/version.rb
|
|
106
|
+
- lib/pagy_infinite_scroll/view_helper.rb
|
|
107
|
+
- sig/pagy_infinite_scroll.rbs
|
|
108
|
+
homepage: https://github.com/hassanharoon86/pagy_infinite_scroll
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://github.com/hassanharoon86/pagy_infinite_scroll
|
|
113
|
+
source_code_uri: https://github.com/hassanharoon86/pagy_infinite_scroll
|
|
114
|
+
changelog_uri: https://github.com/hassanharoon86/pagy_infinite_scroll/blob/main/CHANGELOG.md
|
|
115
|
+
rdoc_options: []
|
|
116
|
+
require_paths:
|
|
117
|
+
- lib
|
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: 3.0.0
|
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
requirements: []
|
|
129
|
+
rubygems_version: 3.6.7
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Infinite scroll pagination for Rails using Pagy and Stimulus
|
|
132
|
+
test_files: []
|