reactive-actions 0.1.0.pre.alpha.1 β 0.1.0.pre.alpha.2
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 +4 -4
- data/README.md +507 -407
- data/app/assets/javascripts/reactive_actions.js +271 -13
- data/lib/generators/reactive_actions/install/install_generator.rb +109 -13
- data/lib/generators/reactive_actions/install/templates/README +96 -20
- data/lib/generators/reactive_actions/install/templates/initializer.rb +14 -0
- data/lib/reactive_actions/version.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# ReactiveActions
|
2
2
|
|
3
|
-
ReactiveActions is a Rails gem that provides a framework for handling reactive actions in your Rails application.
|
3
|
+
ReactiveActions is a Rails gem that provides a framework for handling reactive actions in your Rails application with Stimulus-style DOM binding support.
|
4
4
|
|
5
5
|
## π§ Status
|
6
6
|
|
7
|
-
This gem is currently in alpha (0.1.0-alpha.
|
7
|
+
This gem is currently in alpha (0.1.0-alpha.2). The API may change between versions.
|
8
8
|
|
9
9
|
## π¦ Installation
|
10
10
|
|
11
11
|
Add this line to your application's Gemfile:
|
12
12
|
|
13
13
|
```ruby
|
14
|
-
gem 'reactive-actions', '0.1.0-alpha.
|
14
|
+
gem 'reactive-actions', '0.1.0-alpha.2'
|
15
15
|
```
|
16
16
|
|
17
17
|
And then execute:
|
@@ -110,7 +110,7 @@ The generator will:
|
|
110
110
|
|
111
111
|
## β‘ Rails 8 Native JavaScript Integration
|
112
112
|
|
113
|
-
ReactiveActions
|
113
|
+
ReactiveActions uses Rails 8's native JavaScript approach with **Importmap + Propshaft**, providing seamless integration without additional build steps.
|
114
114
|
|
115
115
|
### Automatic Setup
|
116
116
|
|
@@ -145,21 +145,43 @@ The JavaScript client supports both Rails 8 (Importmap) and older setups (Sprock
|
|
145
145
|
|
146
146
|
## π Usage
|
147
147
|
|
148
|
-
###
|
148
|
+
### DOM Binding (Recommended)
|
149
149
|
|
150
|
-
|
150
|
+
The easiest way to use ReactiveActions is with DOM binding - no JavaScript required:
|
151
151
|
|
152
|
+
```html
|
153
|
+
<!-- Basic button click -->
|
154
|
+
<button reactive-action="click->update_user"
|
155
|
+
reactive-action-user-id="123">
|
156
|
+
Update User
|
157
|
+
</button>
|
158
|
+
|
159
|
+
<!-- Live search input -->
|
160
|
+
<input reactive-action="input->search_users"
|
161
|
+
reactive-action-live="true"
|
162
|
+
placeholder="Search...">
|
163
|
+
|
164
|
+
<!-- Form submission -->
|
165
|
+
<form reactive-action="submit->create_post">
|
166
|
+
<input name="title" type="text" required>
|
167
|
+
<button type="submit">Create Post</button>
|
168
|
+
</form>
|
169
|
+
|
170
|
+
<!-- RESTful actions with HTTP methods -->
|
171
|
+
<button reactive-action="click->post#create_user">Create</button>
|
172
|
+
<button reactive-action="click->put#update_user">Update</button>
|
173
|
+
<button reactive-action="click->delete#delete_user">Delete</button>
|
152
174
|
```
|
153
|
-
GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
|
154
|
-
```
|
155
175
|
|
156
|
-
|
176
|
+
### HTTP API
|
177
|
+
|
178
|
+
You can also access reactive actions by sending direct HTTP requests:
|
157
179
|
|
158
180
|
```
|
159
|
-
GET/POST/PUT/PATCH/DELETE /
|
181
|
+
GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
|
160
182
|
```
|
161
183
|
|
162
|
-
|
184
|
+
Parameters:
|
163
185
|
- `action_name`: The name of the action to execute
|
164
186
|
- `action_params`: Parameters for the action
|
165
187
|
|
@@ -173,35 +195,245 @@ response = Net::HTTP.post(
|
|
173
195
|
)
|
174
196
|
```
|
175
197
|
|
176
|
-
###
|
198
|
+
### JavaScript Client
|
199
|
+
|
200
|
+
For programmatic access, use the JavaScript client:
|
201
|
+
|
202
|
+
```javascript
|
203
|
+
// Basic usage (POST method by default)
|
204
|
+
ReactiveActions.execute('update_user', { id: 1, name: 'New Name' })
|
205
|
+
.then(response => {
|
206
|
+
if (response.ok) {
|
207
|
+
console.log('Success:', response);
|
208
|
+
} else {
|
209
|
+
console.error('Error:', response);
|
210
|
+
}
|
211
|
+
});
|
212
|
+
|
213
|
+
// Using specific HTTP methods
|
214
|
+
ReactiveActions.get('fetch_user', { id: 1 });
|
215
|
+
ReactiveActions.post('create_user', { name: 'New User' });
|
216
|
+
ReactiveActions.put('update_user', { id: 1, name: 'Updated Name' });
|
217
|
+
ReactiveActions.patch('partial_update', { id: 1, status: 'active' });
|
218
|
+
ReactiveActions.delete('delete_user', { id: 1 });
|
219
|
+
```
|
220
|
+
|
221
|
+
## π DOM Binding Reference
|
222
|
+
|
223
|
+
### Action Syntax
|
224
|
+
|
225
|
+
Use `reactive-action` with the format `event->action_name` or `event->method#action_name`:
|
226
|
+
|
227
|
+
```html
|
228
|
+
<!-- Basic actions (uses default POST method) -->
|
229
|
+
<button reactive-action="click->update_user">Update User</button>
|
230
|
+
<input reactive-action="change->search_users" type="text">
|
231
|
+
<div reactive-action="hover->show_preview">Hover me</div>
|
232
|
+
|
233
|
+
<!-- With HTTP methods -->
|
234
|
+
<button reactive-action="click->put#update_user">Update User (PUT)</button>
|
235
|
+
<button reactive-action="click->delete#delete_user">Delete User</button>
|
236
|
+
<button reactive-action="click->get#fetch_user">Fetch User</button>
|
237
|
+
|
238
|
+
<!-- Multiple actions -->
|
239
|
+
<button reactive-action="click->post#save mouseenter->get#preview">
|
240
|
+
Save Item
|
241
|
+
</button>
|
242
|
+
```
|
243
|
+
|
244
|
+
### Passing Data
|
245
|
+
|
246
|
+
Use `reactive-action-*` attributes to pass data:
|
247
|
+
|
248
|
+
```html
|
249
|
+
<button reactive-action="click->update_user"
|
250
|
+
reactive-action-user-id="123"
|
251
|
+
reactive-action-name="John Doe">
|
252
|
+
Update User
|
253
|
+
</button>
|
254
|
+
```
|
255
|
+
|
256
|
+
Data attributes are automatically converted from kebab-case to snake_case:
|
257
|
+
- `reactive-action-user-id="123"` β `{ user_id: "123" }`
|
258
|
+
- `reactive-action-first-name="John"` β `{ first_name: "John" }`
|
259
|
+
|
260
|
+
### Supported Events
|
261
|
+
|
262
|
+
- **`click`** - Mouse clicks
|
263
|
+
- **`hover`** - Mouse hover (mouseenter)
|
264
|
+
- **`change`** - Input value changes
|
265
|
+
- **`input`** - Input value changes (live)
|
266
|
+
- **`submit`** - Form submissions
|
267
|
+
- **`focus`** - Element receives focus
|
268
|
+
- **`blur`** - Element loses focus
|
269
|
+
- **`mouseenter`/`mouseleave`** - Mouse interactions
|
270
|
+
- **`keyup`/`keydown`** - Keyboard events
|
271
|
+
|
272
|
+
### Loading States
|
273
|
+
|
274
|
+
Elements automatically get loading states:
|
275
|
+
|
276
|
+
```css
|
277
|
+
.reactive-loading {
|
278
|
+
opacity: 0.6;
|
279
|
+
cursor: not-allowed;
|
280
|
+
}
|
281
|
+
|
282
|
+
/* Buttons get disabled and show loading text */
|
283
|
+
button.reactive-loading {
|
284
|
+
background-color: #ccc;
|
285
|
+
}
|
286
|
+
```
|
287
|
+
|
288
|
+
Custom loading text:
|
289
|
+
```html
|
290
|
+
<button reactive-action="click->slow_action"
|
291
|
+
data-loading-text="Processing...">
|
292
|
+
Start Process
|
293
|
+
</button>
|
294
|
+
```
|
177
295
|
|
178
|
-
|
296
|
+
### Success and Error Handling
|
297
|
+
|
298
|
+
#### Custom Events
|
299
|
+
```javascript
|
300
|
+
// Listen for successful actions
|
301
|
+
document.addEventListener('reactive-action:success', (event) => {
|
302
|
+
const { response, element, originalEvent } = event.detail;
|
303
|
+
console.log('Action succeeded:', response);
|
304
|
+
});
|
305
|
+
|
306
|
+
// Listen for action errors
|
307
|
+
document.addEventListener('reactive-action:error', (event) => {
|
308
|
+
const { error, element, originalEvent } = event.detail;
|
309
|
+
console.error('Action failed:', error);
|
310
|
+
});
|
311
|
+
```
|
312
|
+
|
313
|
+
#### Callback Functions
|
314
|
+
```html
|
315
|
+
<button reactive-action="click->update_user"
|
316
|
+
reactive-action-success="handleSuccess"
|
317
|
+
reactive-action-error="handleError">
|
318
|
+
Update User
|
319
|
+
</button>
|
320
|
+
|
321
|
+
<script>
|
322
|
+
function handleSuccess(response, element, event) {
|
323
|
+
alert('User updated successfully!');
|
324
|
+
}
|
325
|
+
|
326
|
+
function handleError(error, element, event) {
|
327
|
+
alert('Failed to update user: ' + error.message);
|
328
|
+
}
|
329
|
+
</script>
|
330
|
+
```
|
331
|
+
|
332
|
+
## βοΈ Configuration
|
333
|
+
|
334
|
+
ReactiveActions provides flexible initialization options:
|
335
|
+
|
336
|
+
### Automatic Initialization (Default)
|
337
|
+
|
338
|
+
```javascript
|
339
|
+
// Automatically set up during installation
|
340
|
+
// Available globally as window.ReactiveActions
|
341
|
+
ReactiveActions.execute('action_name', { param: 'value' })
|
342
|
+
```
|
343
|
+
|
344
|
+
### Manual Initialization
|
345
|
+
|
346
|
+
```javascript
|
347
|
+
// Import the client class
|
348
|
+
import ReactiveActionsClient from "reactive_actions"
|
349
|
+
|
350
|
+
// Create and configure instance
|
351
|
+
const reactiveActions = new ReactiveActionsClient({
|
352
|
+
baseUrl: '/custom/path/execute',
|
353
|
+
enableAutoBinding: true,
|
354
|
+
enableMutationObserver: true,
|
355
|
+
defaultHttpMethod: 'POST'
|
356
|
+
});
|
357
|
+
|
358
|
+
// Initialize DOM bindings
|
359
|
+
reactiveActions.initialize();
|
360
|
+
|
361
|
+
// Make available globally (optional)
|
362
|
+
window.ReactiveActions = reactiveActions;
|
363
|
+
```
|
364
|
+
|
365
|
+
### Configuration Options
|
366
|
+
|
367
|
+
| Option | Default | Description |
|
368
|
+
|--------|---------|-------------|
|
369
|
+
| `baseUrl` | `'/reactive_actions/execute'` | API endpoint for action requests |
|
370
|
+
| `enableAutoBinding` | `true` | Automatically bind elements on initialization |
|
371
|
+
| `enableMutationObserver` | `true` | Watch for dynamically added elements |
|
372
|
+
| `defaultHttpMethod` | `'POST'` | Default HTTP method when not specified |
|
373
|
+
|
374
|
+
### Advanced Configuration Examples
|
375
|
+
|
376
|
+
```javascript
|
377
|
+
// Environment-specific configuration
|
378
|
+
const reactiveActions = new ReactiveActionsClient({
|
379
|
+
baseUrl: Rails.env === 'development' ?
|
380
|
+
'http://localhost:3000/reactive_actions/execute' :
|
381
|
+
'/reactive_actions/execute'
|
382
|
+
});
|
383
|
+
|
384
|
+
// For SPAs with manual DOM control
|
385
|
+
const manualReactiveActions = new ReactiveActionsClient({
|
386
|
+
enableAutoBinding: false,
|
387
|
+
enableMutationObserver: false
|
388
|
+
});
|
389
|
+
|
390
|
+
// Initialize only when needed
|
391
|
+
document.addEventListener('turbo:load', () => {
|
392
|
+
manualReactiveActions.initialize();
|
393
|
+
});
|
394
|
+
|
395
|
+
// Reconfigure after creation
|
396
|
+
reactiveActions.configure({
|
397
|
+
defaultHttpMethod: 'PUT',
|
398
|
+
enableAutoBinding: false
|
399
|
+
}).reinitialize();
|
400
|
+
|
401
|
+
// Get current configuration
|
402
|
+
console.log(reactiveActions.getConfig());
|
403
|
+
|
404
|
+
// Bind specific elements manually
|
405
|
+
reactiveActions.bindElement(document.getElementById('my-button'));
|
406
|
+
|
407
|
+
// Force re-initialization
|
408
|
+
reactiveActions.reinitialize();
|
409
|
+
```
|
410
|
+
|
411
|
+
## π― Creating Custom Actions
|
412
|
+
|
413
|
+
Create custom actions by inheriting from `ReactiveActions::ReactiveAction`:
|
179
414
|
|
180
415
|
```ruby
|
181
416
|
# app/reactive_actions/update_user_action.rb
|
182
417
|
class UpdateUserAction < ReactiveActions::ReactiveAction
|
183
418
|
def action
|
184
|
-
user = User.find(action_params[:
|
185
|
-
user.update(action_params[:
|
419
|
+
user = User.find(action_params[:user_id])
|
420
|
+
user.update(name: action_params[:name])
|
186
421
|
|
187
422
|
@result = {
|
188
423
|
success: true,
|
189
|
-
user: user.as_json
|
424
|
+
user: user.as_json(only: [:id, :name, :email])
|
190
425
|
}
|
191
426
|
end
|
192
427
|
|
193
428
|
def response
|
194
|
-
render json:
|
195
|
-
success: true,
|
196
|
-
data: @result
|
197
|
-
}
|
429
|
+
render json: @result
|
198
430
|
end
|
199
431
|
end
|
200
432
|
```
|
201
433
|
|
202
434
|
### Action Directory Structure
|
203
435
|
|
204
|
-
Actions are placed in the `app/reactive_actions` directory
|
436
|
+
Actions are placed in the `app/reactive_actions` directory:
|
205
437
|
|
206
438
|
```
|
207
439
|
app/
|
@@ -216,82 +448,87 @@ app/
|
|
216
448
|
β βββ update_product_action.rb
|
217
449
|
```
|
218
450
|
|
219
|
-
Actions in subdirectories are automatically loaded and namespaced under `ReactiveActions`. For example, a file at `app/reactive_actions/user_actions/create_user_action.rb` becomes accessible as `ReactiveActions::CreateUserAction`.
|
220
|
-
|
221
451
|
### Action Naming Convention
|
222
452
|
|
223
|
-
Action files should follow the naming convention:
|
224
453
|
- File name: `snake_case_action.rb` (e.g., `update_user_action.rb`)
|
225
454
|
- Class name: `CamelCaseAction` (e.g., `UpdateUserAction`)
|
226
|
-
- HTTP parameter: `snake_case` without
|
455
|
+
- HTTP parameter: `snake_case` without `_action` suffix (e.g., `update_user`)
|
227
456
|
|
228
|
-
Examples
|
229
|
-
- `create_user_action.rb` β `CreateUserAction` β called with `action_name: "create_user"`
|
230
|
-
- `fetch_product_action.rb` β `FetchProductAction` β called with `action_name: "fetch_product"`
|
457
|
+
### Advanced Action Examples
|
231
458
|
|
232
|
-
|
459
|
+
#### RESTful User Management
|
460
|
+
```ruby
|
461
|
+
# app/reactive_actions/create_user_action.rb
|
462
|
+
class CreateUserAction < ReactiveActions::ReactiveAction
|
463
|
+
def action
|
464
|
+
user = User.create!(action_params.slice(:name, :email))
|
465
|
+
@result = { user: user.as_json, message: 'User created successfully' }
|
466
|
+
end
|
233
467
|
|
234
|
-
|
468
|
+
def response
|
469
|
+
render json: @result, status: :created
|
470
|
+
end
|
471
|
+
end
|
235
472
|
|
236
|
-
|
237
|
-
|
238
|
-
class ProcessPaymentAction < ReactiveActions::ReactiveAction
|
473
|
+
# app/reactive_actions/update_user_action.rb
|
474
|
+
class UpdateUserAction < ReactiveActions::ReactiveAction
|
239
475
|
def action
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
# Process the payment
|
244
|
-
payment_service = PaymentService.new(
|
245
|
-
amount: action_params[:amount],
|
246
|
-
currency: action_params[:currency],
|
247
|
-
payment_method: action_params[:payment_method]
|
248
|
-
)
|
249
|
-
|
250
|
-
@result = payment_service.process
|
251
|
-
|
252
|
-
# Log the transaction
|
253
|
-
PaymentLog.create(
|
254
|
-
amount: action_params[:amount],
|
255
|
-
status: @result[:status],
|
256
|
-
transaction_id: @result[:transaction_id]
|
257
|
-
)
|
258
|
-
rescue PaymentError => e
|
259
|
-
@error = { type: 'payment_failed', message: e.message }
|
260
|
-
rescue ValidationError => e
|
261
|
-
@error = { type: 'validation_failed', message: e.message }
|
476
|
+
user = User.find(action_params[:user_id])
|
477
|
+
user.update!(action_params.slice(:name, :email))
|
478
|
+
@result = { user: user.as_json, message: 'User updated successfully' }
|
262
479
|
end
|
263
480
|
|
264
481
|
def response
|
265
|
-
|
266
|
-
render json: { success: false, error: @error }, status: :unprocessable_entity
|
267
|
-
else
|
268
|
-
render json: { success: true, data: @result }
|
269
|
-
end
|
482
|
+
render json: @result
|
270
483
|
end
|
484
|
+
end
|
271
485
|
|
272
|
-
|
486
|
+
# app/reactive_actions/delete_user_action.rb
|
487
|
+
class DeleteUserAction < ReactiveActions::ReactiveAction
|
488
|
+
def action
|
489
|
+
user = User.find(action_params[:user_id])
|
490
|
+
user.destroy!
|
491
|
+
@result = { message: 'User deleted successfully' }
|
492
|
+
end
|
273
493
|
|
274
|
-
def
|
275
|
-
|
276
|
-
missing_params = required_params.select { |param| action_params[param].blank? }
|
277
|
-
|
278
|
-
raise ValidationError, "Missing parameters: #{missing_params.join(', ')}" if missing_params.any?
|
279
|
-
raise ValidationError, "Invalid amount" unless action_params[:amount].to_f > 0
|
494
|
+
def response
|
495
|
+
render json: @result
|
280
496
|
end
|
281
497
|
end
|
282
498
|
```
|
283
499
|
|
284
|
-
####
|
500
|
+
#### Live Search with Filtering
|
501
|
+
```ruby
|
502
|
+
# app/reactive_actions/search_users_action.rb
|
503
|
+
class SearchUsersAction < ReactiveActions::ReactiveAction
|
504
|
+
def action
|
505
|
+
query = action_params[:value] || action_params[:query]
|
506
|
+
|
507
|
+
users = User.where("name ILIKE ? OR email ILIKE ?", "%#{query}%", "%#{query}%")
|
508
|
+
.limit(10)
|
509
|
+
.select(:id, :name, :email)
|
510
|
+
|
511
|
+
@result = {
|
512
|
+
users: users.as_json,
|
513
|
+
count: users.count,
|
514
|
+
query: query
|
515
|
+
}
|
516
|
+
end
|
517
|
+
|
518
|
+
def response
|
519
|
+
render json: @result
|
520
|
+
end
|
521
|
+
end
|
522
|
+
```
|
285
523
|
|
524
|
+
#### Background Job Integration
|
286
525
|
```ruby
|
287
526
|
# app/reactive_actions/generate_report_action.rb
|
288
527
|
class GenerateReportAction < ReactiveActions::ReactiveAction
|
289
528
|
def action
|
290
|
-
# Queue the report generation job
|
291
529
|
job = ReportGenerationJob.perform_later(
|
292
530
|
user_id: action_params[:user_id],
|
293
|
-
report_type: action_params[:report_type]
|
294
|
-
filters: action_params[:filters] || {}
|
531
|
+
report_type: action_params[:report_type]
|
295
532
|
)
|
296
533
|
|
297
534
|
@result = {
|
@@ -302,105 +539,204 @@ class GenerateReportAction < ReactiveActions::ReactiveAction
|
|
302
539
|
end
|
303
540
|
|
304
541
|
def response
|
305
|
-
render json:
|
306
|
-
success: true,
|
307
|
-
message: 'Report generation started',
|
308
|
-
data: @result
|
309
|
-
}
|
542
|
+
render json: @result, status: :accepted
|
310
543
|
end
|
311
544
|
end
|
312
545
|
```
|
313
546
|
|
314
|
-
## π»
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
547
|
+
## π» Complete DOM Binding Examples
|
548
|
+
|
549
|
+
### User Management Interface
|
550
|
+
|
551
|
+
```html
|
552
|
+
<!-- User List with Actions -->
|
553
|
+
<div class="user-list">
|
554
|
+
<% @users.each do |user| %>
|
555
|
+
<div class="user-card" id="user-<%= user.id %>">
|
556
|
+
<h3><%= user.name %></h3>
|
557
|
+
<p><%= user.email %></p>
|
558
|
+
|
559
|
+
<!-- Update user -->
|
560
|
+
<button reactive-action="click->put#update_user"
|
561
|
+
reactive-action-user-id="<%= user.id %>"
|
562
|
+
reactive-action-name="<%= user.name %>"
|
563
|
+
reactive-action-success="handleUserUpdate">
|
564
|
+
Quick Update
|
565
|
+
</button>
|
566
|
+
|
567
|
+
<!-- Delete user -->
|
568
|
+
<button reactive-action="click->delete#delete_user"
|
569
|
+
reactive-action-user-id="<%= user.id %>"
|
570
|
+
reactive-action-success="handleUserDelete"
|
571
|
+
class="danger">
|
572
|
+
Delete
|
573
|
+
</button>
|
574
|
+
|
575
|
+
<!-- Show preview on hover -->
|
576
|
+
<div reactive-action="mouseenter->get#show_user_preview mouseleave->post#hide_preview"
|
577
|
+
reactive-action-user-id="<%= user.id %>"
|
578
|
+
reactive-action-target="preview-<%= user.id %>">
|
579
|
+
<img src="<%= user.avatar %>" alt="Hover for details">
|
580
|
+
</div>
|
581
|
+
</div>
|
582
|
+
<% end %>
|
583
|
+
</div>
|
584
|
+
|
585
|
+
<!-- Live Search -->
|
586
|
+
<div class="search-container">
|
587
|
+
<input type="text"
|
588
|
+
reactive-action="input->get#search_users"
|
589
|
+
reactive-action-min-length="2"
|
590
|
+
reactive-action-success="updateSearchResults"
|
591
|
+
placeholder="Search users...">
|
592
|
+
|
593
|
+
<div id="search-results"></div>
|
594
|
+
</div>
|
595
|
+
|
596
|
+
<!-- Create User Form -->
|
597
|
+
<form reactive-action="submit->post#create_user"
|
598
|
+
reactive-action-success="handleUserCreate">
|
599
|
+
<input name="name" type="text" placeholder="Name" required>
|
600
|
+
<input name="email" type="email" placeholder="Email" required>
|
601
|
+
<button type="submit">Create User</button>
|
602
|
+
</form>
|
603
|
+
|
604
|
+
<script>
|
605
|
+
function handleUserUpdate(response, element, event) {
|
606
|
+
if (response.success) {
|
607
|
+
// Update the UI without page refresh
|
608
|
+
const userCard = element.closest('.user-card');
|
609
|
+
userCard.querySelector('h3').textContent = response.user.name;
|
610
|
+
|
611
|
+
// Show success message
|
612
|
+
showFlash('User updated successfully!', 'success');
|
613
|
+
}
|
614
|
+
}
|
346
615
|
|
347
|
-
|
616
|
+
function handleUserDelete(response, element, event) {
|
617
|
+
if (response.success) {
|
618
|
+
// Remove the user card from the UI
|
619
|
+
const userCard = element.closest('.user-card');
|
620
|
+
userCard.remove();
|
621
|
+
|
622
|
+
showFlash('User deleted successfully!', 'success');
|
623
|
+
}
|
624
|
+
}
|
348
625
|
|
349
|
-
|
626
|
+
function handleUserCreate(response, element, event) {
|
627
|
+
if (response.success) {
|
628
|
+
// Reset the form
|
629
|
+
element.reset();
|
630
|
+
|
631
|
+
// Add new user to the list or refresh
|
632
|
+
location.reload(); // Or dynamically add to the list
|
633
|
+
|
634
|
+
showFlash('User created successfully!', 'success');
|
635
|
+
}
|
636
|
+
}
|
350
637
|
|
351
|
-
|
352
|
-
|
638
|
+
function updateSearchResults(response, element, event) {
|
639
|
+
const resultsDiv = document.getElementById('search-results');
|
640
|
+
resultsDiv.innerHTML = response.users.map(user =>
|
641
|
+
`<div class="search-result">
|
642
|
+
<strong>${user.name}</strong> - ${user.email}
|
643
|
+
</div>`
|
644
|
+
).join('');
|
645
|
+
}
|
353
646
|
|
354
|
-
|
355
|
-
|
647
|
+
function showFlash(message, type) {
|
648
|
+
// Your flash message implementation
|
649
|
+
console.log(`${type}: ${message}`);
|
650
|
+
}
|
651
|
+
</script>
|
652
|
+
```
|
653
|
+
|
654
|
+
### E-commerce Product Interactions
|
655
|
+
|
656
|
+
```html
|
657
|
+
<!-- Product Cards -->
|
658
|
+
<div class="products-grid">
|
659
|
+
<% @products.each do |product| %>
|
660
|
+
<div class="product-card">
|
661
|
+
<h3><%= product.name %></h3>
|
662
|
+
<p class="price">$<%= product.price %></p>
|
663
|
+
|
664
|
+
<!-- Add to cart -->
|
665
|
+
<button reactive-action="click->post#add_to_cart"
|
666
|
+
reactive-action-product-id="<%= product.id %>"
|
667
|
+
reactive-action-quantity="1"
|
668
|
+
reactive-action-success="updateCartCount">
|
669
|
+
Add to Cart
|
670
|
+
</button>
|
671
|
+
|
672
|
+
<!-- Wishlist toggle -->
|
673
|
+
<button reactive-action="click->post#toggle_wishlist"
|
674
|
+
reactive-action-product-id="<%= product.id %>"
|
675
|
+
reactive-action-success="toggleWishlistUI"
|
676
|
+
class="<%= 'wishlisted' if current_user.wishlist.include?(product) %>">
|
677
|
+
β₯ Wishlist
|
678
|
+
</button>
|
679
|
+
|
680
|
+
<!-- Quick view on hover -->
|
681
|
+
<div reactive-action="mouseenter->get#quick_view"
|
682
|
+
reactive-action-product-id="<%= product.id %>"
|
683
|
+
reactive-action-success="showQuickView">
|
684
|
+
<img src="<%= product.image %>" alt="<%= product.name %>">
|
685
|
+
</div>
|
686
|
+
|
687
|
+
<!-- Quantity selector -->
|
688
|
+
<select reactive-action="change->put#update_cart_quantity"
|
689
|
+
reactive-action-product-id="<%= product.id %>"
|
690
|
+
reactive-action-success="updateCartTotal">
|
691
|
+
<% (1..10).each do |qty| %>
|
692
|
+
<option value="<%= qty %>"><%= qty %></option>
|
693
|
+
<% end %>
|
694
|
+
</select>
|
695
|
+
</div>
|
696
|
+
<% end %>
|
697
|
+
</div>
|
698
|
+
|
699
|
+
<!-- Product Filter -->
|
700
|
+
<div class="filters">
|
701
|
+
<select reactive-action="change->get#filter_products"
|
702
|
+
reactive-action-success="updateProductGrid">
|
703
|
+
<option value="">All Categories</option>
|
704
|
+
<option value="electronics">Electronics</option>
|
705
|
+
<option value="clothing">Clothing</option>
|
706
|
+
<option value="books">Books</option>
|
707
|
+
</select>
|
708
|
+
|
709
|
+
<input type="range"
|
710
|
+
reactive-action="input->get#filter_by_price"
|
711
|
+
reactive-action-success="updateProductGrid"
|
712
|
+
min="0" max="1000" step="10">
|
713
|
+
</div>
|
356
714
|
```
|
357
715
|
|
358
|
-
### Client Features
|
359
|
-
|
360
|
-
The client automatically:
|
361
|
-
- β
**Handles CSRF tokens** - Automatically includes Rails CSRF protection
|
362
|
-
- β
**Formats requests** - Properly formats GET vs POST/PUT/PATCH/DELETE requests
|
363
|
-
- β
**Parses responses** - Automatically parses JSON responses
|
364
|
-
- β
**Returns promises** - Modern async/await compatible
|
365
|
-
- β
**Error handling** - Provides structured error information
|
366
|
-
- β
**Multiple HTTP methods** - Support for GET, POST, PUT, PATCH, DELETE
|
367
|
-
|
368
716
|
## Security
|
369
717
|
|
370
|
-
ReactiveActions implements several security measures
|
371
|
-
|
372
|
-
### π **Built-in Security Features**
|
373
|
-
|
374
|
-
#### Parameter Sanitization
|
375
|
-
- **Input validation**: Action names are validated against safe patterns (`/\A[a-zA-Z_][a-zA-Z0-9_]*\z/`)
|
376
|
-
- **Parameter key sanitization**: Only alphanumeric characters, underscores, and hyphens allowed
|
377
|
-
- **String length limits**: Prevents memory exhaustion attacks (max 10,000 chars)
|
378
|
-
- **Dangerous prefix filtering**: Blocks parameters starting with `__`, `eval`, `exec`, `system`, etc.
|
718
|
+
ReactiveActions implements several security measures:
|
379
719
|
|
380
|
-
|
381
|
-
- **Automatic CSRF tokens**: JavaScript client automatically includes Rails CSRF tokens
|
382
|
-
- **Same-origin requests**: Credentials are sent only to same-origin requests
|
383
|
-
- **Controller integration**: Inherits from `ActionController::Base` with CSRF protection
|
720
|
+
### π Built-in Security Features
|
384
721
|
|
385
|
-
|
386
|
-
- **
|
387
|
-
- **
|
388
|
-
- **
|
722
|
+
- **Parameter sanitization** - Input validation and safe patterns
|
723
|
+
- **CSRF protection** - Automatic Rails CSRF token handling
|
724
|
+
- **Code injection prevention** - Sanitized class names and parameters
|
725
|
+
- **Length limits** - Prevents memory exhaustion attacks
|
389
726
|
|
390
|
-
### π‘οΈ
|
727
|
+
### π‘οΈ Security Best Practices
|
391
728
|
|
392
729
|
```ruby
|
393
|
-
#
|
730
|
+
# Always validate user permissions
|
394
731
|
class SecureAction < ReactiveActions::ReactiveAction
|
395
732
|
def action
|
396
|
-
# Always validate user permissions
|
397
733
|
raise ReactiveActions::UnauthorizedError unless current_user&.admin?
|
398
734
|
|
399
735
|
# Validate and sanitize inputs
|
400
736
|
user_id = action_params[:user_id].to_i
|
401
|
-
raise ReactiveActions::InvalidParametersError
|
737
|
+
raise ReactiveActions::InvalidParametersError if user_id <= 0
|
402
738
|
|
403
|
-
# Use strong parameters
|
739
|
+
# Use strong parameters
|
404
740
|
permitted_params = action_params.slice(:name, :email).permit!
|
405
741
|
|
406
742
|
@result = User.find(user_id).update(permitted_params)
|
@@ -408,130 +744,9 @@ class SecureAction < ReactiveActions::ReactiveAction
|
|
408
744
|
end
|
409
745
|
```
|
410
746
|
|
411
|
-
### β οΈ **Security Considerations**
|
412
|
-
|
413
|
-
- **Always validate user permissions** in your actions
|
414
|
-
- **Use Rails strong parameters** when working with model updates
|
415
|
-
- **Sanitize file uploads** if handling file parameters
|
416
|
-
- **Implement rate limiting** for public-facing actions
|
417
|
-
- **Log security events** for audit trails
|
418
|
-
|
419
|
-
## Performance
|
420
|
-
|
421
|
-
### π **Performance Characteristics**
|
422
|
-
|
423
|
-
ReactiveActions is designed to be lightweight and efficient:
|
424
|
-
|
425
|
-
- **Minimal overhead**: Direct controller execution without complex middleware chains
|
426
|
-
- **No database dependencies**: Core functionality doesn't require database connections
|
427
|
-
- **Efficient autoloading**: Actions are loaded on-demand using Rails' autoloading
|
428
|
-
- **Memory efficient**: Parameter sanitization prevents memory exhaustion attacks
|
429
|
-
|
430
|
-
### π **Performance Best Practices**
|
431
|
-
|
432
|
-
#### Action Design
|
433
|
-
```ruby
|
434
|
-
# β
Good: Lightweight action with focused responsibility
|
435
|
-
class QuickUpdateAction < ReactiveActions::ReactiveAction
|
436
|
-
def action
|
437
|
-
User.where(id: action_params[:id]).update_all(
|
438
|
-
last_seen_at: Time.current
|
439
|
-
)
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
# β Avoid: Heavy operations that should be background jobs
|
444
|
-
class SlowReportAction < ReactiveActions::ReactiveAction
|
445
|
-
def action
|
446
|
-
# This should be a background job instead
|
447
|
-
@result = generate_complex_report_synchronously
|
448
|
-
end
|
449
|
-
end
|
450
|
-
```
|
451
|
-
|
452
|
-
#### Use Background Jobs for Heavy Operations
|
453
|
-
```ruby
|
454
|
-
# β
Better approach for time-consuming operations
|
455
|
-
class InitiateReportAction < ReactiveActions::ReactiveAction
|
456
|
-
def action
|
457
|
-
ReportGenerationJob.perform_later(action_params)
|
458
|
-
@result = { status: 'queued', job_id: SecureRandom.uuid }
|
459
|
-
end
|
460
|
-
end
|
461
|
-
```
|
462
|
-
|
463
|
-
#### Optimize Database Queries
|
464
|
-
```ruby
|
465
|
-
class OptimizedAction < ReactiveActions::ReactiveAction
|
466
|
-
def action
|
467
|
-
# Use includes to avoid N+1 queries
|
468
|
-
@users = User.includes(:profile, :posts)
|
469
|
-
.where(id: action_params[:user_ids])
|
470
|
-
|
471
|
-
# Use select to limit returned columns
|
472
|
-
@summary = User.select(:id, :name, :created_at)
|
473
|
-
.where(active: true)
|
474
|
-
end
|
475
|
-
end
|
476
|
-
```
|
477
|
-
|
478
|
-
### π **Monitoring and Optimization**
|
479
|
-
|
480
|
-
- **Monitor response times**: Actions should typically complete in < 100ms
|
481
|
-
- **Use Rails logging**: ReactiveActions logs execution details at debug level
|
482
|
-
- **Profile memory usage**: Large parameter sets can impact memory
|
483
|
-
- **Consider caching**: Use Rails caching for frequently accessed data
|
484
|
-
|
485
|
-
## βοΈ Configuration
|
486
|
-
|
487
|
-
The gem can be configured using an initializer (automatically created by the install generator):
|
488
|
-
|
489
|
-
```ruby
|
490
|
-
# config/initializers/reactive_actions.rb
|
491
|
-
ReactiveActions.configure do |config|
|
492
|
-
# Configure methods to delegate from the controller to action classes
|
493
|
-
config.delegated_controller_methods += [:custom_method]
|
494
|
-
|
495
|
-
# Configure instance variables to delegate from the controller to action classes
|
496
|
-
config.delegated_instance_variables += [:custom_variable]
|
497
|
-
end
|
498
|
-
|
499
|
-
# Set the logger for ReactiveActions
|
500
|
-
ReactiveActions.logger = Rails.logger
|
501
|
-
```
|
502
|
-
|
503
|
-
### Advanced Configuration
|
504
|
-
|
505
|
-
During installation, you can choose to configure advanced options:
|
506
|
-
|
507
|
-
- **Custom Controller Methods**: Add additional controller methods to delegate to action classes
|
508
|
-
- **Logging Level**: Set a custom logging level for ReactiveActions
|
509
|
-
- **Instance Variables**: Configure which instance variables to delegate from controllers to actions
|
510
|
-
|
511
|
-
### Default Delegated Methods
|
512
|
-
|
513
|
-
By default, the following controller methods are available in your actions:
|
514
|
-
- `render`
|
515
|
-
- `redirect_to`
|
516
|
-
- `head`
|
517
|
-
- `params`
|
518
|
-
- `session`
|
519
|
-
- `cookies`
|
520
|
-
- `flash`
|
521
|
-
- `request`
|
522
|
-
- `response`
|
523
|
-
|
524
747
|
## β Error Handling
|
525
748
|
|
526
|
-
ReactiveActions provides structured error handling
|
527
|
-
|
528
|
-
- `ActionNotFoundError`: When the requested action doesn't exist
|
529
|
-
- `MissingParameterError`: When required parameters are missing
|
530
|
-
- `InvalidParametersError`: When parameters have invalid types or formats
|
531
|
-
- `UnauthorizedError`: When the user lacks permission for the action
|
532
|
-
- `ActionExecutionError`: When an error occurs during action execution
|
533
|
-
|
534
|
-
All errors return JSON responses with consistent structure:
|
749
|
+
ReactiveActions provides structured error handling:
|
535
750
|
|
536
751
|
```json
|
537
752
|
{
|
@@ -544,166 +759,51 @@ All errors return JSON responses with consistent structure:
|
|
544
759
|
}
|
545
760
|
```
|
546
761
|
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
**Problem**: Getting `ActionNotFoundError` for existing actions
|
554
|
-
|
555
|
-
**Solutions**:
|
556
|
-
```bash
|
557
|
-
# 1. Check file naming convention
|
558
|
-
# File: app/reactive_actions/my_action.rb
|
559
|
-
# Class: MyAction
|
560
|
-
# Call with: action_name: "my"
|
561
|
-
|
562
|
-
# 2. Restart Rails to reload autoloading
|
563
|
-
rails restart
|
564
|
-
|
565
|
-
# 3. Check for syntax errors in action file
|
566
|
-
rails console
|
567
|
-
> MyAction # Should load without errors
|
568
|
-
```
|
569
|
-
|
570
|
-
#### β **JavaScript Client Not Working**
|
571
|
-
|
572
|
-
**Problem**: `ReactiveActions is not defined` in browser
|
573
|
-
|
574
|
-
**Solutions**:
|
575
|
-
```javascript
|
576
|
-
// 1. Check importmap.rb includes the pin
|
577
|
-
// config/importmap.rb should have:
|
578
|
-
pin "reactive_actions", to: "reactive_actions.js"
|
579
|
-
|
580
|
-
// 2. Check application.js imports it
|
581
|
-
// app/javascript/application.js should have:
|
582
|
-
import "reactive_actions"
|
583
|
-
|
584
|
-
// 3. Clear browser cache and restart Rails
|
585
|
-
```
|
586
|
-
|
587
|
-
#### β **CSRF Token Errors**
|
588
|
-
|
589
|
-
**Problem**: Getting `Can't verify CSRF token authenticity`
|
590
|
-
|
591
|
-
**Solutions**:
|
592
|
-
```erb
|
593
|
-
<!-- 1. Ensure CSRF meta tags are in your layout -->
|
594
|
-
<%= csrf_meta_tags %>
|
595
|
-
|
596
|
-
<!-- 2. Check that protect_from_forgery is enabled -->
|
597
|
-
<%# In your ApplicationController %>
|
598
|
-
protect_from_forgery with: :exception
|
599
|
-
```
|
600
|
-
|
601
|
-
#### β **Parameter Sanitization Issues**
|
602
|
-
|
603
|
-
**Problem**: Parameters are being rejected or modified unexpectedly
|
604
|
-
|
605
|
-
**Solutions**:
|
606
|
-
```ruby
|
607
|
-
# 1. Check parameter key format (alphanumeric, underscore, hyphen only)
|
608
|
-
# β
Good: { user_name: "John", user-id: 123 }
|
609
|
-
# β Bad: { "__eval": "code", "system()": "bad" }
|
610
|
-
|
611
|
-
# 2. Check string length limits (max 10,000 characters)
|
612
|
-
# 3. Review logs for specific sanitization messages
|
613
|
-
```
|
614
|
-
|
615
|
-
#### β **Performance Issues**
|
616
|
-
|
617
|
-
**Problem**: Actions are slow or timing out
|
618
|
-
|
619
|
-
**Solutions**:
|
620
|
-
```ruby
|
621
|
-
# 1. Move heavy operations to background jobs
|
622
|
-
class SlowAction < ReactiveActions::ReactiveAction
|
623
|
-
def action
|
624
|
-
# Instead of this:
|
625
|
-
# heavy_operation
|
626
|
-
|
627
|
-
# Do this:
|
628
|
-
HeavyOperationJob.perform_later(action_params)
|
629
|
-
@result = { status: 'queued' }
|
630
|
-
end
|
631
|
-
end
|
632
|
-
|
633
|
-
# 2. Optimize database queries
|
634
|
-
# 3. Add caching where appropriate
|
635
|
-
# 4. Monitor with Rails logs at debug level
|
636
|
-
```
|
637
|
-
|
638
|
-
### Debug Mode
|
639
|
-
|
640
|
-
Enable detailed logging for troubleshooting:
|
641
|
-
|
642
|
-
```ruby
|
643
|
-
# config/initializers/reactive_actions.rb
|
644
|
-
ReactiveActions.logger.level = :debug
|
645
|
-
|
646
|
-
# This will log:
|
647
|
-
# - Action execution details
|
648
|
-
# - Parameter sanitization steps
|
649
|
-
# - Error stack traces
|
650
|
-
# - Performance metrics
|
651
|
-
```
|
762
|
+
**Error Types:**
|
763
|
+
- `ActionNotFoundError` - Action doesn't exist
|
764
|
+
- `MissingParameterError` - Required parameters missing
|
765
|
+
- `InvalidParametersError` - Invalid parameter format
|
766
|
+
- `UnauthorizedError` - Permission denied
|
767
|
+
- `ActionExecutionError` - Runtime execution error
|
652
768
|
|
653
769
|
## π Rails 8 Compatibility
|
654
770
|
|
655
|
-
|
656
|
-
|
657
|
-
- β
**
|
658
|
-
- β
**
|
659
|
-
- β
**
|
660
|
-
- β
**
|
661
|
-
- β
**Backward compatibility** - Still works with Sprockets if needed
|
771
|
+
Designed specifically for Rails 8:
|
772
|
+
- β
**Propshaft** - Modern asset pipeline
|
773
|
+
- β
**Importmap** - Native ES modules
|
774
|
+
- β
**Rails 8 conventions** - Current best practices
|
775
|
+
- β
**Modern JavaScript** - ES6+ features
|
776
|
+
- β
**Backward compatibility** - Works with Sprockets
|
662
777
|
|
663
778
|
## πΊοΈ Roadmap & Future Improvements
|
664
779
|
|
665
|
-
Planned
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
* Built-in testing utilities and helpers
|
673
|
-
* Auto-generated API documentation
|
674
|
-
* And much more
|
780
|
+
Planned features:
|
781
|
+
- Security hooks for authentication/authorization
|
782
|
+
- Rate limiting and throttling
|
783
|
+
- Enhanced error handling
|
784
|
+
- Action composition for complex workflows
|
785
|
+
- Built-in testing utilities
|
786
|
+
- Auto-generated API documentation
|
675
787
|
|
676
788
|
## π οΈ Development
|
677
789
|
|
678
|
-
After checking out the repo
|
790
|
+
After checking out the repo:
|
679
791
|
|
680
792
|
```bash
|
681
793
|
$ bundle install
|
682
|
-
|
683
|
-
|
684
|
-
Then, run the tests:
|
685
|
-
|
686
|
-
```bash
|
687
|
-
$ bundle exec rspec
|
688
|
-
```
|
689
|
-
|
690
|
-
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
691
|
-
|
692
|
-
To install this gem onto your local machine, run:
|
693
|
-
|
694
|
-
```bash
|
695
|
-
$ bundle exec rake install
|
794
|
+
$ bundle exec rspec # Run tests
|
795
|
+
$ bin/console # Interactive prompt
|
696
796
|
```
|
697
797
|
|
698
798
|
## π§ͺ Testing
|
699
799
|
|
700
|
-
|
800
|
+
Run the test suite:
|
701
801
|
|
702
802
|
```bash
|
703
803
|
$ bundle exec rspec
|
704
804
|
```
|
705
805
|
|
706
|
-
**Note**: The dummy application is only available in the source repository
|
806
|
+
**Note**: The dummy application is only available in the source repository.
|
707
807
|
|
708
808
|
## π€ Contributing
|
709
809
|
|