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.
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.1). The API may change between versions.
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.1'
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 now uses Rails 8's native JavaScript approach with **Importmap + Propshaft**, providing seamless integration without additional build steps.
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
- ### HTTP API
148
+ ### DOM Binding (Recommended)
149
149
 
150
- Once installed, you can access the reactive actions by sending requests to your configured endpoint:
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
- Or if you used a custom mount path:
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 /your-custom-path/execute
181
+ GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
160
182
  ```
161
183
 
162
- You can pass parameters:
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
- ### Creating Custom Actions
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
- You can create custom actions by inheriting from `ReactiveActions::ReactiveAction`:
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[:id])
185
- user.update(action_params[:user_attributes])
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 structure:
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 the `_action` suffix (e.g., `update_user`)
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
- ### Advanced Examples
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
- #### Complex Action with Validation and Error Handling
468
+ def response
469
+ render json: @result, status: :created
470
+ end
471
+ end
235
472
 
236
- ```ruby
237
- # app/reactive_actions/process_payment_action.rb
238
- class ProcessPaymentAction < ReactiveActions::ReactiveAction
473
+ # app/reactive_actions/update_user_action.rb
474
+ class UpdateUserAction < ReactiveActions::ReactiveAction
239
475
  def action
240
- # Validate required parameters
241
- validate_parameters!
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
- if @error
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
- private
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 validate_parameters!
275
- required_params = %i[amount currency payment_method]
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
- #### Action with Background Job Integration
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
- ## πŸ’» JavaScript Client
315
-
316
- ReactiveActions includes a modern JavaScript client that's automatically available after installation.
317
-
318
- ### Global Usage (Recommended)
319
-
320
- After installation, `ReactiveActions` is globally available:
321
-
322
- ```javascript
323
- // Basic usage (POST method by default)
324
- ReactiveActions.execute('update_user', { id: 1, name: 'New Name' })
325
- .then(response => {
326
- if (response.ok) {
327
- console.log('Success:', response);
328
- } else {
329
- console.error('Error:', response);
330
- }
331
- });
332
-
333
- // Using specific HTTP methods
334
- ReactiveActions.get('fetch_user', { id: 1 });
335
- ReactiveActions.post('create_user', { name: 'New User' });
336
- ReactiveActions.put('update_user', { id: 1, name: 'Updated Name' });
337
- ReactiveActions.patch('partial_update', { id: 1, status: 'active' });
338
- ReactiveActions.delete('delete_user', { id: 1 });
339
-
340
- // With custom options
341
- ReactiveActions.execute('custom_action', { data: 'value' }, {
342
- method: 'POST',
343
- contentType: 'application/json'
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
- ### ES Module Import (Advanced)
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
- For more control, you can import it explicitly:
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
- ```javascript
352
- import ReactiveActions from "reactive_actions"
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
- // Use it in your module
355
- ReactiveActions.execute('action_name', { param: 'value' })
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 to protect your application:
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
- #### CSRF Protection
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
- #### Code Injection Prevention
386
- - **Class name validation**: Action names are sanitized before constant lookup
387
- - **Namespace isolation**: Actions are properly namespaced to prevent conflicts
388
- - **Parameter filtering**: Recursive parameter sanitization for nested structures
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
- ### πŸ›‘οΈ **Security Best Practices**
727
+ ### πŸ›‘οΈ Security Best Practices
391
728
 
392
729
  ```ruby
393
- # app/reactive_actions/secure_action.rb
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, "Invalid user ID" if user_id <= 0
737
+ raise ReactiveActions::InvalidParametersError if user_id <= 0
402
738
 
403
- # Use strong parameters if integrating with models
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 with specific error types:
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
- ## πŸ”§ Troubleshooting
548
-
549
- ### Common Issues and Solutions
550
-
551
- #### ❓ **Action Not Found Errors**
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
- This gem is designed specifically for Rails 8 and takes advantage of:
656
-
657
- - βœ… **Propshaft** - Modern asset pipeline without compilation
658
- - βœ… **Importmap** - Native ES module support without bundling
659
- - βœ… **Rails 8 conventions** - Follows current Rails best practices
660
- - βœ… **Modern JavaScript** - ES6+ classes and async/await
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 improvements for ReactiveActions:
666
-
667
- * Security hooks - methods that run before actions for authentication and authorization checks
668
- * Rate limiting and throttling capabilities
669
- * Enhanced error handling with more granular error types
670
- * Action composition - ability to build complex workflows from smaller actions
671
- * Improved generators for common action patterns
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, run the following to install dependencies:
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
- The gem repository includes a dummy Rails application for development and testing purposes. To run the tests:
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 and is not included in the distributed gem.
806
+ **Note**: The dummy application is only available in the source repository.
707
807
 
708
808
  ## 🀝 Contributing
709
809