rails_claude_skills 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/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- metadata +199 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-pagination-kaminari
|
|
3
|
+
description: "Pagination for Ruby on Rails applications using Kaminari. Use when: (1) Implementing pagination for database records, (2) Building paginated API endpoints, (3) Customizing pagination UI with themes, (4) Handling large datasets efficiently, (5) Creating infinite scroll, (6) Paginating arrays or custom collections, (7) Adding SEO-friendly pagination URLs, (8) Internationalizing pagination labels"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rails Pagination with Kaminari
|
|
7
|
+
|
|
8
|
+
Kaminari is a scope and engine-based pagination library that provides a clean, powerful, customizable paginator for Rails applications. It's non-intrusive, chainable with ActiveRecord, and highly customizable.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Add to Gemfile
|
|
14
|
+
bundle add kaminari
|
|
15
|
+
|
|
16
|
+
# Generate configuration file (optional)
|
|
17
|
+
rails g kaminari:config
|
|
18
|
+
|
|
19
|
+
# Generate view templates for customization (optional)
|
|
20
|
+
rails g kaminari:views default
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
### Controller Pagination
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# app/controllers/posts_controller.rb
|
|
29
|
+
class PostsController < ApplicationController
|
|
30
|
+
def index
|
|
31
|
+
@posts = Post.order(:created_at).page(params[:page])
|
|
32
|
+
# Returns 25 items per page by default
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### View Helper
|
|
38
|
+
|
|
39
|
+
```erb
|
|
40
|
+
<!-- app/views/posts/index.html.erb -->
|
|
41
|
+
<%= paginate @posts %>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's it! Kaminari automatically adds pagination links.
|
|
45
|
+
|
|
46
|
+
## Core Methods
|
|
47
|
+
|
|
48
|
+
### Page Scope
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# Basic pagination
|
|
52
|
+
User.page(1) # First page, 25 items
|
|
53
|
+
User.page(params[:page]) # Dynamic page from params
|
|
54
|
+
|
|
55
|
+
# Custom per-page
|
|
56
|
+
User.page(1).per(50) # 50 items per page
|
|
57
|
+
|
|
58
|
+
# Chaining with scopes
|
|
59
|
+
User.active.order(:name).page(params[:page]).per(20)
|
|
60
|
+
|
|
61
|
+
# With associations
|
|
62
|
+
User.includes(:posts).page(params[:page])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Pagination Metadata
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
users = User.page(2).per(20)
|
|
69
|
+
|
|
70
|
+
users.current_page #=> 2
|
|
71
|
+
users.total_pages #=> 10
|
|
72
|
+
users.total_count #=> 200
|
|
73
|
+
users.limit_value #=> 20
|
|
74
|
+
users.first_page? #=> false
|
|
75
|
+
users.last_page? #=> false
|
|
76
|
+
users.next_page #=> 3
|
|
77
|
+
users.prev_page #=> 1
|
|
78
|
+
users.out_of_range? #=> false
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
### Global Configuration
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/initializers/kaminari_config.rb
|
|
87
|
+
Kaminari.configure do |config|
|
|
88
|
+
config.default_per_page = 25 # Default items per page
|
|
89
|
+
config.max_per_page = 100 # Maximum allowed per page
|
|
90
|
+
config.max_pages = nil # Maximum pages (nil = unlimited)
|
|
91
|
+
config.window = 4 # Inner window size
|
|
92
|
+
config.outer_window = 0 # Outer window size
|
|
93
|
+
config.left = 0 # Left outer window
|
|
94
|
+
config.right = 0 # Right outer window
|
|
95
|
+
config.page_method_name = :page # Method name (change if conflicts)
|
|
96
|
+
config.param_name = :page # URL parameter name
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Per-Model Configuration
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# app/models/post.rb
|
|
104
|
+
class Post < ApplicationRecord
|
|
105
|
+
paginates_per 50 # This model shows 50 per page
|
|
106
|
+
max_paginates_per 100 # User can't request more than 100
|
|
107
|
+
max_pages 100 # Limit to 100 pages total
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## View Helpers
|
|
112
|
+
|
|
113
|
+
### Basic Pagination
|
|
114
|
+
|
|
115
|
+
```erb
|
|
116
|
+
<!-- Simple pagination links -->
|
|
117
|
+
<%= paginate @posts %>
|
|
118
|
+
|
|
119
|
+
<!-- With options -->
|
|
120
|
+
<%= paginate @posts, window: 2 %>
|
|
121
|
+
<%= paginate @posts, outer_window: 1 %>
|
|
122
|
+
<%= paginate @posts, left: 1, right: 1 %>
|
|
123
|
+
|
|
124
|
+
<!-- Custom parameter name -->
|
|
125
|
+
<%= paginate @posts, param_name: :pagina %>
|
|
126
|
+
|
|
127
|
+
<!-- For AJAX/Turbo -->
|
|
128
|
+
<%= paginate @posts, remote: true %>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Navigation Links
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<!-- Previous/Next links -->
|
|
135
|
+
<%= link_to_prev_page @posts, 'Previous', class: 'btn' %>
|
|
136
|
+
<%= link_to_next_page @posts, 'Next', class: 'btn' %>
|
|
137
|
+
|
|
138
|
+
<!-- With custom content -->
|
|
139
|
+
<%= link_to_prev_page @posts do %>
|
|
140
|
+
<span aria-hidden="true">←</span> Older
|
|
141
|
+
<% end %>
|
|
142
|
+
|
|
143
|
+
<%= link_to_next_page @posts do %>
|
|
144
|
+
Newer <span aria-hidden="true">→</span>
|
|
145
|
+
<% end %>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Page Info
|
|
149
|
+
|
|
150
|
+
```erb
|
|
151
|
+
<!-- Shows: "Displaying posts 1 - 25 of 100 in total" -->
|
|
152
|
+
<%= page_entries_info @posts %>
|
|
153
|
+
|
|
154
|
+
<!-- Custom format -->
|
|
155
|
+
<%= page_entries_info @posts, entry_name: 'item' %>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### SEO Helpers
|
|
159
|
+
|
|
160
|
+
```erb
|
|
161
|
+
<!-- Add rel="next" and rel="prev" link tags to <head> -->
|
|
162
|
+
<%= rel_next_prev_link_tags @posts %>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### URL Helpers
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# Get URLs for navigation
|
|
169
|
+
path_to_next_page(@posts) #=> "/posts?page=3"
|
|
170
|
+
path_to_prev_page(@posts) #=> "/posts?page=1"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Customization
|
|
174
|
+
|
|
175
|
+
### Generating Custom Views
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Generate default theme
|
|
179
|
+
rails g kaminari:views default
|
|
180
|
+
|
|
181
|
+
# Generate with namespace
|
|
182
|
+
rails g kaminari:views default --views-prefix admin
|
|
183
|
+
|
|
184
|
+
# Generate specific theme
|
|
185
|
+
rails g kaminari:views bootstrap4
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
This creates templates in `app/views/kaminari/`:
|
|
189
|
+
- `_first_page.html.erb`
|
|
190
|
+
- `_prev_page.html.erb`
|
|
191
|
+
- `_page.html.erb`
|
|
192
|
+
- `_next_page.html.erb`
|
|
193
|
+
- `_last_page.html.erb`
|
|
194
|
+
- `_gap.html.erb`
|
|
195
|
+
- `_paginator.html.erb`
|
|
196
|
+
|
|
197
|
+
### Using Themes
|
|
198
|
+
|
|
199
|
+
```erb
|
|
200
|
+
<!-- Default theme -->
|
|
201
|
+
<%= paginate @posts %>
|
|
202
|
+
|
|
203
|
+
<!-- Custom theme -->
|
|
204
|
+
<%= paginate @posts, theme: 'my_theme' %>
|
|
205
|
+
|
|
206
|
+
<!-- Bootstrap theme -->
|
|
207
|
+
<%= paginate @posts, theme: 'twitter-bootstrap-4' %>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Custom Pagination Template
|
|
211
|
+
|
|
212
|
+
```erb
|
|
213
|
+
<!-- app/views/kaminari/_paginator.html.erb -->
|
|
214
|
+
<nav class="pagination" role="navigation" aria-label="Pagination">
|
|
215
|
+
<ul class="pagination-list">
|
|
216
|
+
<%= first_page_tag %>
|
|
217
|
+
<%= prev_page_tag %>
|
|
218
|
+
|
|
219
|
+
<% each_page do |page| %>
|
|
220
|
+
<% if page.left_outer? || page.right_outer? || page.inside_window? %>
|
|
221
|
+
<%= page_tag page %>
|
|
222
|
+
<% elsif !page.was_truncated? %>
|
|
223
|
+
<%= gap_tag %>
|
|
224
|
+
<% end %>
|
|
225
|
+
<% end %>
|
|
226
|
+
|
|
227
|
+
<%= next_page_tag %>
|
|
228
|
+
<%= last_page_tag %>
|
|
229
|
+
</ul>
|
|
230
|
+
</nav>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## API Pagination
|
|
234
|
+
|
|
235
|
+
### JSON Response
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
# app/controllers/api/v1/posts_controller.rb
|
|
239
|
+
module Api
|
|
240
|
+
module V1
|
|
241
|
+
class PostsController < ApplicationController
|
|
242
|
+
def index
|
|
243
|
+
@posts = Post.page(params[:page]).per(params[:per_page] || 20)
|
|
244
|
+
|
|
245
|
+
render json: {
|
|
246
|
+
posts: @posts.map { |p| PostSerializer.new(p) },
|
|
247
|
+
meta: pagination_meta(@posts)
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def pagination_meta(collection)
|
|
254
|
+
{
|
|
255
|
+
current_page: collection.current_page,
|
|
256
|
+
next_page: collection.next_page,
|
|
257
|
+
prev_page: collection.prev_page,
|
|
258
|
+
total_pages: collection.total_pages,
|
|
259
|
+
total_count: collection.total_count
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### API Response Helper
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# app/controllers/concerns/paginatable.rb
|
|
271
|
+
module Paginatable
|
|
272
|
+
extend ActiveSupport::Concern
|
|
273
|
+
|
|
274
|
+
def paginate(collection)
|
|
275
|
+
collection
|
|
276
|
+
.page(params[:page] || 1)
|
|
277
|
+
.per(params[:per_page] || default_per_page)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def pagination_links(collection)
|
|
281
|
+
{
|
|
282
|
+
self: request.original_url,
|
|
283
|
+
first: url_for(page: 1),
|
|
284
|
+
prev: collection.prev_page ? url_for(page: collection.prev_page) : nil,
|
|
285
|
+
next: collection.next_page ? url_for(page: collection.next_page) : nil,
|
|
286
|
+
last: url_for(page: collection.total_pages)
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def pagination_meta(collection)
|
|
291
|
+
{
|
|
292
|
+
current_page: collection.current_page,
|
|
293
|
+
total_pages: collection.total_pages,
|
|
294
|
+
total_count: collection.total_count,
|
|
295
|
+
per_page: collection.limit_value
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private
|
|
300
|
+
|
|
301
|
+
def default_per_page
|
|
302
|
+
20
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Performance Optimization
|
|
308
|
+
|
|
309
|
+
### Without Count Query
|
|
310
|
+
|
|
311
|
+
For very large datasets, skip expensive COUNT queries:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# Controller
|
|
315
|
+
def index
|
|
316
|
+
@posts = Post.order(:created_at).page(params[:page]).without_count
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# View - use simple navigation only
|
|
320
|
+
<%= link_to_prev_page @posts, 'Previous' %>
|
|
321
|
+
<%= link_to_next_page @posts, 'Next' %>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Note**: `total_pages`, `total_count`, and numbered page links won't work with `without_count`.
|
|
325
|
+
|
|
326
|
+
### Eager Loading
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# Prevent N+1 queries
|
|
330
|
+
@posts = Post.includes(:user, :comments)
|
|
331
|
+
.order(:created_at)
|
|
332
|
+
.page(params[:page])
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Caching
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
# Fragment caching
|
|
339
|
+
<% cache ["posts-page", @posts.current_page] do %>
|
|
340
|
+
<%= render @posts %>
|
|
341
|
+
<%= paginate @posts %>
|
|
342
|
+
<% end %>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Advanced Features
|
|
346
|
+
|
|
347
|
+
### Paginating Arrays
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
# Controller
|
|
351
|
+
@items = expensive_operation_returning_array
|
|
352
|
+
@paginated_items = Kaminari.paginate_array(@items, total_count: @items.count)
|
|
353
|
+
.page(params[:page])
|
|
354
|
+
.per(10)
|
|
355
|
+
|
|
356
|
+
# Or with known total
|
|
357
|
+
@paginated_items = Kaminari.paginate_array(
|
|
358
|
+
@items,
|
|
359
|
+
total_count: 145,
|
|
360
|
+
limit: 10,
|
|
361
|
+
offset: (params[:page].to_i - 1) * 10
|
|
362
|
+
).page(params[:page]).per(10)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### SEO-Friendly URLs
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# config/routes.rb
|
|
369
|
+
resources :posts do
|
|
370
|
+
get 'page/:page', action: :index, on: :collection
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Creates URLs like: /posts/page/2 instead of /posts?page=2
|
|
374
|
+
|
|
375
|
+
# Or using concerns
|
|
376
|
+
concern :paginatable do
|
|
377
|
+
get '(page/:page)', action: :index, on: :collection, as: ''
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
resources :posts, concerns: :paginatable
|
|
381
|
+
resources :articles, concerns: :paginatable
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Infinite Scroll
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
# app/controllers/posts_controller.rb
|
|
388
|
+
def index
|
|
389
|
+
@posts = Post.order(:created_at).page(params[:page])
|
|
390
|
+
|
|
391
|
+
respond_to do |format|
|
|
392
|
+
format.html
|
|
393
|
+
format.js # For infinite scroll
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// app/views/posts/index.js.erb
|
|
400
|
+
$('#posts').append('<%= j render @posts %>');
|
|
401
|
+
|
|
402
|
+
<% if @posts.next_page %>
|
|
403
|
+
$('.pagination').replaceWith('<%= j paginate @posts %>');
|
|
404
|
+
<% else %>
|
|
405
|
+
$('.pagination').remove();
|
|
406
|
+
<% end %>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Custom Scopes with Pagination
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# app/models/post.rb
|
|
413
|
+
class Post < ApplicationRecord
|
|
414
|
+
scope :published, -> { where(published: true) }
|
|
415
|
+
scope :by_author, ->(author_id) { where(author_id: author_id) }
|
|
416
|
+
scope :recent_first, -> { order(created_at: :desc) }
|
|
417
|
+
|
|
418
|
+
# Chainable with pagination
|
|
419
|
+
# Post.published.recent_first.page(1)
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Internationalization
|
|
424
|
+
|
|
425
|
+
```yaml
|
|
426
|
+
# config/locales/en.yml
|
|
427
|
+
en:
|
|
428
|
+
views:
|
|
429
|
+
pagination:
|
|
430
|
+
first: "« First"
|
|
431
|
+
last: "Last »"
|
|
432
|
+
previous: "‹ Prev"
|
|
433
|
+
next: "Next ›"
|
|
434
|
+
truncate: "…"
|
|
435
|
+
helpers:
|
|
436
|
+
page_entries_info:
|
|
437
|
+
one_page:
|
|
438
|
+
display_entries:
|
|
439
|
+
zero: "No %{entry_name} found"
|
|
440
|
+
one: "Displaying <b>1</b> %{entry_name}"
|
|
441
|
+
other: "Displaying <b>all %{count}</b> %{entry_name}"
|
|
442
|
+
more_pages:
|
|
443
|
+
display_entries: "Displaying %{entry_name} <b>%{first} - %{last}</b> of <b>%{total}</b> in total"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Common Patterns
|
|
447
|
+
|
|
448
|
+
### Search with Pagination
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
# app/controllers/posts_controller.rb
|
|
452
|
+
def index
|
|
453
|
+
@posts = Post.all
|
|
454
|
+
@posts = @posts.where('title LIKE ?', "%#{params[:q]}%") if params[:q].present?
|
|
455
|
+
@posts = @posts.order(:created_at).page(params[:page])
|
|
456
|
+
end
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
```erb
|
|
460
|
+
<!-- app/views/posts/index.html.erb -->
|
|
461
|
+
<%= form_with url: posts_path, method: :get do |f| %>
|
|
462
|
+
<%= f.text_field :q, value: params[:q], placeholder: 'Search...' %>
|
|
463
|
+
<%= f.submit 'Search' %>
|
|
464
|
+
<% end %>
|
|
465
|
+
|
|
466
|
+
<%= render @posts %>
|
|
467
|
+
<%= paginate @posts, params: { q: params[:q] } %>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Filtered Pagination
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# app/controllers/posts_controller.rb
|
|
474
|
+
def index
|
|
475
|
+
@posts = Post.all
|
|
476
|
+
@posts = @posts.where(category_id: params[:category_id]) if params[:category_id]
|
|
477
|
+
@posts = @posts.where(status: params[:status]) if params[:status]
|
|
478
|
+
@posts = @posts.order(:created_at).page(params[:page])
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
```erb
|
|
483
|
+
<%= paginate @posts, params: { category_id: params[:category_id], status: params[:status] } %>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Admin Pagination
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
# app/controllers/admin/users_controller.rb
|
|
490
|
+
module Admin
|
|
491
|
+
class UsersController < AdminController
|
|
492
|
+
def index
|
|
493
|
+
@users = User.order(:email).page(params[:page]).per(50)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Testing
|
|
500
|
+
|
|
501
|
+
### RSpec
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
# spec/models/post_spec.rb
|
|
505
|
+
RSpec.describe Post, type: :model do
|
|
506
|
+
describe '.page' do
|
|
507
|
+
let!(:posts) { create_list(:post, 30) }
|
|
508
|
+
|
|
509
|
+
it 'returns first page with default per_page' do
|
|
510
|
+
page = Post.page(1)
|
|
511
|
+
expect(page.count).to eq(25)
|
|
512
|
+
expect(page.current_page).to eq(1)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
it 'returns correct page' do
|
|
516
|
+
page = Post.page(2).per(10)
|
|
517
|
+
expect(page.count).to eq(10)
|
|
518
|
+
expect(page.current_page).to eq(2)
|
|
519
|
+
expect(page.total_pages).to eq(3)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# spec/requests/posts_spec.rb
|
|
525
|
+
RSpec.describe 'Posts', type: :request do
|
|
526
|
+
describe 'GET /posts' do
|
|
527
|
+
let!(:posts) { create_list(:post, 30) }
|
|
528
|
+
|
|
529
|
+
it 'paginates posts' do
|
|
530
|
+
get posts_path, params: { page: 2 }
|
|
531
|
+
expect(response).to have_http_status(:ok)
|
|
532
|
+
expect(assigns(:posts).current_page).to eq(2)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 'handles out of range pages' do
|
|
536
|
+
get posts_path, params: { page: 999 }
|
|
537
|
+
expect(response).to have_http_status(:ok)
|
|
538
|
+
expect(assigns(:posts)).to be_empty
|
|
539
|
+
expect(assigns(:posts).out_of_range?).to be true
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Controller Tests
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
# spec/controllers/posts_controller_spec.rb
|
|
549
|
+
RSpec.describe PostsController, type: :controller do
|
|
550
|
+
describe 'GET #index' do
|
|
551
|
+
let!(:posts) { create_list(:post, 30) }
|
|
552
|
+
|
|
553
|
+
it 'assigns paginated posts' do
|
|
554
|
+
get :index, params: { page: 1 }
|
|
555
|
+
expect(assigns(:posts).count).to eq(25)
|
|
556
|
+
expect(assigns(:posts).total_count).to eq(30)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it 'respects per_page parameter' do
|
|
560
|
+
get :index, params: { page: 1, per_page: 10 }
|
|
561
|
+
expect(assigns(:posts).count).to eq(10)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Troubleshooting
|
|
568
|
+
|
|
569
|
+
### Page Parameter Not Working
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# If using custom routes, ensure page param is permitted
|
|
573
|
+
params.permit(:page, :per_page)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Total Count Performance
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
# For large tables, use counter cache or without_count
|
|
580
|
+
class Post < ApplicationRecord
|
|
581
|
+
# Option 1: Counter cache
|
|
582
|
+
belongs_to :category, counter_cache: true
|
|
583
|
+
|
|
584
|
+
# Option 2: Skip count query
|
|
585
|
+
# Use .without_count in controller
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Styling Issues
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
# Ensure you've generated views
|
|
593
|
+
rails g kaminari:views default
|
|
594
|
+
|
|
595
|
+
# Or use a theme
|
|
596
|
+
rails g kaminari:views bootstrap4
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Best Practices
|
|
600
|
+
|
|
601
|
+
1. **Always order before paginating**: Ensures consistent results across pages
|
|
602
|
+
2. **Use `per` wisely**: Set reasonable limits with `max_paginates_per`
|
|
603
|
+
3. **Eager load associations**: Prevent N+1 queries with `includes`
|
|
604
|
+
4. **Cache pagination**: Use fragment caching for expensive queries
|
|
605
|
+
5. **Handle out of range**: Check `out_of_range?` and redirect if needed
|
|
606
|
+
6. **API pagination**: Always include metadata in JSON responses
|
|
607
|
+
7. **SEO**: Use `rel_next_prev_link_tags` for better search indexing
|
|
608
|
+
8. **Test edge cases**: Empty results, last page, out of range pages
|
|
609
|
+
9. **Use `without_count` for large datasets**: Skip COUNT queries when possible
|
|
610
|
+
10. **Preserve filters**: Pass filter params to `paginate` helper
|
|
611
|
+
|
|
612
|
+
## Additional Resources
|
|
613
|
+
|
|
614
|
+
For more advanced patterns, see:
|
|
615
|
+
- **API Pagination**: [references/api-pagination.md](references/api-pagination.md)
|
|
616
|
+
- **Custom Themes**: [references/custom-themes.md](references/custom-themes.md)
|
|
617
|
+
- **Performance Optimization**: [references/performance.md](references/performance.md)
|
|
618
|
+
|
|
619
|
+
## Resources
|
|
620
|
+
|
|
621
|
+
- [Kaminari GitHub](https://github.com/kaminari/kaminari)
|
|
622
|
+
- [Kaminari Wiki](https://github.com/kaminari/kaminari/wiki)
|