pagy_infinite_scroll 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b9818e60d4272ee1490013e0c73ed9afbcf7ba081420668c7b1ecfd5e3b3685
4
- data.tar.gz: f01be9e52a172cc5e18f96ca63ce5f6e21e991d16152a1fd771a686c7bba3511
3
+ metadata.gz: b82bfda46dc2de115eeaac522f4f982df35e6517413522ff12446bef637e3b03
4
+ data.tar.gz: abf34ba36bdf896c79a4fb723b287d65c55d70f5487bcec92259486f41c1bb28
5
5
  SHA512:
6
- metadata.gz: 3f8f33471cc148d24c5745c6841c72a2bf8e3f919a5ba0cc8007fb71908072fc20b6ae4156e5f994c10ac3c9286d4081efadce8f6703174ad8849cb770bea143
7
- data.tar.gz: ffdf94c5b5ef5310ab7fe7916257e7e67e8153c877ced315fa890b550405988b08c4a3a8a81df27d876cdea3c2f07890a48de130e285a2b6e2ba8f7aa13c60c4
6
+ metadata.gz: 2655b2c6353d27bd94f8839aeaa1203c0ab6a3f13e2f4a38b409760fefb31dcabcab48b2dc4519c1153d1688cbaea908be022dae1fb6aab8069caba61234aadf
7
+ data.tar.gz: 4e6d2003f79144666d2ca6f28d20bca2d29613c1ca52a1d2f1266510adadc5cef07277f0723b0d1674a239de14209050445a920b26f6d05d18977ba3151d6be8
data/README.md CHANGED
@@ -16,7 +16,7 @@ A Rails gem that adds infinite scroll functionality using Pagy and Stimulus. Loa
16
16
  Add to your Gemfile:
17
17
 
18
18
  ```ruby
19
- gem 'pagy_infinite_scroll', github: 'hassanharoon86/pagy_infinite_scroll'
19
+ gem 'pagy_infinite_scroll', '~> 0.2.0'
20
20
  ```
21
21
 
22
22
  Then run:
@@ -44,6 +44,19 @@ The generator will:
44
44
 
45
45
  ## Quick Start
46
46
 
47
+ This gem provides **two approaches** for infinite scrolling:
48
+
49
+ 1. **Server-Side Rendering** (Simple) - HTML rendered on the server using `.js.erb` templates
50
+ 2. **JSON API** (Advanced) - JSON responses with client-side HTML rendering
51
+
52
+ Choose the approach that fits your needs!
53
+
54
+ ---
55
+
56
+ ## Approach 1: Server-Side Rendering (Recommended for Simple Use Cases)
57
+
58
+ This approach is simpler and requires minimal JavaScript knowledge. Perfect for standard CRUD operations.
59
+
47
60
  ### 1. Controller Setup
48
61
 
49
62
  Use the gem's helper methods:
@@ -51,13 +64,61 @@ Use the gem's helper methods:
51
64
  ```ruby
52
65
  class ProductsController < ApplicationController
53
66
  def index
54
- # Use pagy_infinite_scroll instead of regular pagy
67
+ @pagy, @products = pagy_infinite_scroll(Product.all, limit: 50)
68
+
69
+ respond_to do |format|
70
+ format.html
71
+ format.js # Responds to .js.erb template
72
+ end
73
+ end
74
+ end
75
+ ```
76
+
77
+ ### 2. View Setup (HTML)
78
+
79
+ ```erb
80
+ <!-- app/views/products/index.html.erb -->
81
+ <%= infinite_scroll_container(@pagy, products_path(format: :js),
82
+ data: { render_mode: 'js' }) do %>
83
+ <%= infinite_scroll_items_container(tag: 'div', class: 'products-list') do %>
84
+ <%= render @products %>
85
+ <% end %>
86
+ <%= infinite_scroll_loading_indicator %>
87
+ <% end %>
88
+ ```
89
+
90
+ ### 3. Create JavaScript Response Template
91
+
92
+ ```erb
93
+ <!-- app/views/products/index.js.erb -->
94
+ <%= pagy_infinite_scroll_append '.products-list', @pagy, @products %>
95
+ ```
96
+
97
+ **That's it!** The gem automatically:
98
+ - Renders your `_product.html.erb` partial for each item
99
+ - Appends the HTML to the container
100
+ - Updates pagination state
101
+ - No JavaScript customization needed!
102
+
103
+ ---
104
+
105
+ ## Approach 2: JSON API (For Complex Use Cases)
106
+
107
+ Use this approach when you need:
108
+ - API reusability (mobile apps, SPAs)
109
+ - Complex client-side logic
110
+ - Full control over rendering
111
+
112
+ ### 1. Controller Setup
113
+
114
+ ```ruby
115
+ class ProductsController < ApplicationController
116
+ def index
55
117
  @pagy, @products = pagy_infinite_scroll(Product.all, limit: 50)
56
118
 
57
119
  respond_to do |format|
58
120
  format.html
59
121
  format.json do
60
- # Use the JSON helper to format response
61
122
  render json: pagy_infinite_scroll_json(@pagy, @products) { |product|
62
123
  {
63
124
  id: product.id,
@@ -73,45 +134,18 @@ end
73
134
 
74
135
  ### 2. View Setup
75
136
 
76
- Add the infinite scroll container to your view:
77
-
78
137
  ```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>
138
+ <%= infinite_scroll_container(@pagy, products_path(format: :json)) do %>
139
+ <%= infinite_scroll_items_container do %>
140
+ <%= render @products %>
141
+ <% end %>
142
+ <%= infinite_scroll_loading_indicator %>
143
+ <% end %>
100
144
  ```
101
145
 
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
146
+ ### 3. Create Custom Stimulus Controller
113
147
 
114
- ### Solution: Extend the Controller
148
+ **For JSON API approach, you MUST override the `createItemHTML` method** to tell the gem how to render your specific HTML.
115
149
 
116
150
  **For jsbundling-rails apps:**
117
151
 
@@ -221,11 +255,12 @@ Edit `config/initializers/pagy_infinite_scroll.rb`:
221
255
 
222
256
  ```ruby
223
257
  PagyInfiniteScroll.configure do |config|
224
- config.items_per_page = 50 # Items per page (default: 25)
258
+ config.items_per_page = 50 # Items per page (default: 50)
225
259
  config.scroll_threshold = 100 # Pixels from bottom to trigger load (default: 100)
226
260
  config.loading_indicator = true # Show loading indicator (default: true)
227
261
  config.preserve_state = true # Preserve URL params (default: true)
228
262
  config.debounce_delay = 500 # Debounce for search in ms (default: 500)
263
+ config.render_mode = 'json' # Rendering mode: 'json' or 'js' (default: 'json')
229
264
  end
230
265
  ```
231
266
 
@@ -10,7 +10,8 @@ export default class PagyInfiniteScrollController extends Controller {
10
10
  loading: Boolean,
11
11
  hasMore: Boolean,
12
12
  threshold: { type: Number, default: 100 },
13
- preserveState: { type: Boolean, default: true }
13
+ preserveState: { type: Boolean, default: true },
14
+ renderMode: { type: String, default: 'json' } // 'json' or 'js'
14
15
  }
15
16
 
16
17
  connect() {
@@ -19,6 +20,9 @@ export default class PagyInfiniteScrollController extends Controller {
19
20
  this.loadingValue = false
20
21
  this.boundHandleScroll = this.handleScroll.bind(this)
21
22
  this.element.addEventListener('scroll', this.boundHandleScroll)
23
+
24
+ // Make controller accessible for server-side updates
25
+ this.element.pagyInfiniteScroll = this
22
26
  }
23
27
 
24
28
  disconnect() {
@@ -55,9 +59,14 @@ export default class PagyInfiniteScrollController extends Controller {
55
59
  }
56
60
  })
57
61
 
62
+ // Determine Accept header based on render mode
63
+ const acceptHeader = this.renderModeValue === 'js'
64
+ ? 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript'
65
+ : 'application/json'
66
+
58
67
  const response = await fetch(url, {
59
68
  headers: {
60
- 'Accept': 'application/json',
69
+ 'Accept': acceptHeader,
61
70
  'X-Requested-With': 'XMLHttpRequest'
62
71
  }
63
72
  })
@@ -66,32 +75,41 @@ export default class PagyInfiniteScrollController extends Controller {
66
75
  throw new Error(`HTTP error! status: ${response.status}`)
67
76
  }
68
77
 
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
- }
78
+ // Handle different response types
79
+ if (this.renderModeValue === 'js') {
80
+ // For .js.erb responses, evaluate the JavaScript
81
+ const jsCode = await response.text()
82
+ eval(jsCode)
83
+ // Note: State is updated by the server-rendered JS via pagy_infinite_scroll_append
84
+ } else {
85
+ // For JSON responses, use the client-side rendering
86
+ const data = await response.json()
87
+
88
+ // Dispatch event with data for custom handling
89
+ const event = this.dispatch('beforeAppend', {
90
+ detail: { data },
91
+ cancelable: true
92
+ })
93
+
94
+ if (!event.defaultPrevented) {
95
+ this.appendItems(data)
96
+ }
80
97
 
81
- // Update pagination state
82
- this.pageValue = data.pagy.page
83
- this.hasMoreValue = data.pagy.next !== null
98
+ // Update pagination state
99
+ this.pageValue = data.pagy.page
100
+ this.hasMoreValue = data.pagy.next !== null
84
101
 
85
- console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
102
+ console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
86
103
 
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
- })
104
+ // Dispatch success event
105
+ this.dispatch('loaded', {
106
+ detail: {
107
+ page: this.pageValue,
108
+ hasMore: this.hasMoreValue,
109
+ count: data.records.length
110
+ }
111
+ })
112
+ }
95
113
 
96
114
  } catch (error) {
97
115
  console.error('[PagyInfiniteScroll] Error loading more items:', error)
@@ -21,7 +21,8 @@
21
21
  loading: Boolean,
22
22
  hasMore: Boolean,
23
23
  threshold: { type: Number, default: 100 },
24
- preserveState: { type: Boolean, default: true }
24
+ preserveState: { type: Boolean, default: true },
25
+ renderMode: { type: String, default: 'json' } // 'json' or 'js'
25
26
  }
26
27
 
27
28
  connect() {
@@ -30,6 +31,9 @@
30
31
  this.loadingValue = false
31
32
  this.boundHandleScroll = this.handleScroll.bind(this)
32
33
  this.element.addEventListener('scroll', this.boundHandleScroll)
34
+
35
+ // Make controller accessible for server-side updates
36
+ this.element.pagyInfiniteScroll = this
33
37
  }
34
38
 
35
39
  disconnect() {
@@ -66,9 +70,14 @@
66
70
  }
67
71
  })
68
72
 
73
+ // Determine Accept header based on render mode
74
+ const acceptHeader = this.renderModeValue === 'js'
75
+ ? 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript'
76
+ : 'application/json'
77
+
69
78
  const response = await fetch(url, {
70
79
  headers: {
71
- 'Accept': 'application/json',
80
+ 'Accept': acceptHeader,
72
81
  'X-Requested-With': 'XMLHttpRequest'
73
82
  }
74
83
  })
@@ -77,32 +86,41 @@
77
86
  throw new Error(`HTTP error! status: ${response.status}`)
78
87
  }
79
88
 
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
- }
89
+ // Handle different response types
90
+ if (this.renderModeValue === 'js') {
91
+ // For .js.erb responses, evaluate the JavaScript
92
+ const jsCode = await response.text()
93
+ eval(jsCode)
94
+ // Note: State is updated by the server-rendered JS via pagy_infinite_scroll_append
95
+ } else {
96
+ // For JSON responses, use the client-side rendering
97
+ const data = await response.json()
98
+
99
+ // Dispatch event with data for custom handling
100
+ const event = this.dispatch('beforeAppend', {
101
+ detail: { data },
102
+ cancelable: true
103
+ })
104
+
105
+ if (!event.defaultPrevented) {
106
+ this.appendItems(data)
107
+ }
91
108
 
92
- // Update pagination state
93
- this.pageValue = data.pagy.page
94
- this.hasMoreValue = data.pagy.next !== null
109
+ // Update pagination state
110
+ this.pageValue = data.pagy.page
111
+ this.hasMoreValue = data.pagy.next !== null
95
112
 
96
- console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
113
+ console.log(`[PagyInfiniteScroll] Loaded page ${this.pageValue}, has more: ${this.hasMoreValue}`)
97
114
 
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
- })
115
+ // Dispatch success event
116
+ this.dispatch('loaded', {
117
+ detail: {
118
+ page: this.pageValue,
119
+ hasMore: this.hasMoreValue,
120
+ count: data.records.length
121
+ }
122
+ })
123
+ }
106
124
 
107
125
  } catch (error) {
108
126
  console.error('[PagyInfiniteScroll] Error loading more items:', error)
@@ -53,7 +53,19 @@ module PagyInfiniteScroll
53
53
  end
54
54
 
55
55
  say "\n"
56
- say " 3. Use in your controllers: pagy_infinite_scroll(collection)"
56
+ say " 3. Choose your rendering approach:", :yellow
57
+ say "\n"
58
+ say " Option A: Server-Side Rendering (Simpler - Recommended for most apps)"
59
+ say " - Use format.js in controller"
60
+ say " - Create .js.erb template with: <%= pagy_infinite_scroll_append '.selector', @pagy, @records %>"
61
+ say " - Set data-pagy-infinite-scroll-render-mode-value='js' in view"
62
+ say " - No JavaScript customization needed!"
63
+ say "\n"
64
+ say " Option B: JSON API (Advanced - For APIs/SPAs)"
65
+ say " - Use format.json in controller"
66
+ say " - Create custom Stimulus controller and override createItemHTML()"
67
+ say " - Full control over client-side rendering"
68
+ say "\n"
57
69
  say " 4. See full documentation: https://github.com/hassanharoon86/pagy_infinite_scroll"
58
70
  say "\n"
59
71
  end
@@ -22,4 +22,12 @@ PagyInfiniteScroll.configure do |config|
22
22
  # Debounce delay for search in milliseconds
23
23
  # Default: 500
24
24
  config.debounce_delay = 500
25
+
26
+ # Default rendering mode for infinite scroll
27
+ # Options: 'json' (client-side rendering) or 'js' (server-side rendering with .js.erb)
28
+ # Default: 'json'
29
+ #
30
+ # 'json' mode: Requires custom Stimulus controller with createItemHTML() method
31
+ # 'js' mode: Uses server-rendered HTML via .js.erb templates (simpler, recommended for most apps)
32
+ config.render_mode = 'json'
25
33
  end
@@ -7,7 +7,8 @@ module PagyInfiniteScroll
7
7
  :loading_indicator,
8
8
  :auto_initialize,
9
9
  :preserve_state,
10
- :debounce_delay
10
+ :debounce_delay,
11
+ :render_mode
11
12
 
12
13
  def initialize
13
14
  @items_per_page = 50
@@ -16,6 +17,7 @@ module PagyInfiniteScroll
16
17
  @auto_initialize = true
17
18
  @preserve_state = true
18
19
  @debounce_delay = 500 # milliseconds
20
+ @render_mode = 'json' # 'json' or 'js' (for .js.erb templates)
19
21
  end
20
22
  end
21
23
  end
@@ -7,8 +7,9 @@ module PagyInfiniteScroll
7
7
  # Don't isolate namespace to allow helpers to load into main app
8
8
  engine_name 'pagy_infinite_scroll'
9
9
 
10
- # Ensure lib files are loaded
11
- config.eager_load_paths << File.expand_path("../../", __FILE__)
10
+ # Don't eager load lib directory - we manually require what we need
11
+ # This avoids Zeitwerk trying to autoload version.rb as a constant
12
+ config.eager_load_paths.delete(File.expand_path("../../", __FILE__))
12
13
 
13
14
  # Load assets if asset pipeline is available
14
15
  initializer "pagy_infinite_scroll.assets", before: :load_config_initializers do |app|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PagyInfiniteScroll
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -33,6 +33,7 @@ module PagyInfiniteScroll
33
33
  pagy_infinite_scroll_page_value: pagy.page,
34
34
  pagy_infinite_scroll_loading_value: false,
35
35
  pagy_infinite_scroll_has_more_value: pagy.next.present?,
36
+ pagy_infinite_scroll_render_mode_value: PagyInfiniteScroll.config.render_mode,
36
37
  **stimulus_data_attributes(data_attrs)
37
38
  } do
38
39
  block.call
@@ -93,6 +94,67 @@ module PagyInfiniteScroll
93
94
  end
94
95
  end
95
96
 
97
+ # Server-side rendering helper for .js.erb templates
98
+ # This provides a simpler alternative to the JSON API approach
99
+ #
100
+ # @param container_selector [String] jQuery/CSS selector for the container
101
+ # @param pagy [Pagy] The pagy object
102
+ # @param records [ActiveRecord::Relation, Array] Records or render options
103
+ # @param options [Hash] Rendering options
104
+ # @option options [String] :partial Partial path (if not using collection inference)
105
+ # @option options [Hash] :locals Additional local variables for the partial
106
+ #
107
+ # @example Simple usage (auto-detects partial from collection)
108
+ # <%= pagy_infinite_scroll_append "#products", @pagy, @products %>
109
+ #
110
+ # @example With explicit partial
111
+ # <%= pagy_infinite_scroll_append "#products", @pagy, @products, partial: "products/card" %>
112
+ #
113
+ # @example With locals
114
+ # <%= pagy_infinite_scroll_append "#items", @pagy, @items, partial: "items/row", locals: { show_actions: true } %>
115
+ #
116
+ def pagy_infinite_scroll_append(container_selector, pagy, records, options = {})
117
+ # Render the HTML for the records
118
+ html = if options[:partial]
119
+ render partial: options[:partial], collection: records, locals: options[:locals] || {}
120
+ else
121
+ render records
122
+ end
123
+
124
+ # Escape the HTML for JavaScript
125
+ escaped_html = escape_javascript(html)
126
+
127
+ # Generate JavaScript to append items and update pagination state
128
+ javascript = <<~JS
129
+ (function() {
130
+ var container = document.querySelector('#{container_selector}');
131
+ if (container) {
132
+ container.insertAdjacentHTML('beforeend', '#{escaped_html}');
133
+
134
+ // Dispatch event for custom handling
135
+ var event = new CustomEvent('pagy-infinite-scroll:appended', {
136
+ detail: {
137
+ page: #{pagy.page},
138
+ hasMore: #{pagy.next.present?},
139
+ count: #{records.size}
140
+ }
141
+ });
142
+ document.dispatchEvent(event);
143
+
144
+ // Update controller values if using Stimulus controller
145
+ var scrollContainer = container.closest('[data-controller~="pagy-infinite-scroll"]');
146
+ if (scrollContainer && scrollContainer.pagyInfiniteScroll) {
147
+ scrollContainer.pagyInfiniteScroll.pageValue = #{pagy.page};
148
+ scrollContainer.pagyInfiniteScroll.hasMoreValue = #{pagy.next.present?};
149
+ scrollContainer.pagyInfiniteScroll.loadingValue = false;
150
+ }
151
+ }
152
+ })();
153
+ JS
154
+
155
+ javascript.html_safe
156
+ end
157
+
96
158
  private
97
159
 
98
160
  def spinner_svg
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pagy_infinite_scroll
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hassan Haroon