reactive-actions 0.1.0.pre.alpha.1 → 0.1.0.pre.alpha.3

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.
@@ -1,9 +1,267 @@
1
1
  // ReactiveActions JavaScript Client for Rails 8
2
- // ES Module compatible version
2
+ // ES Module compatible version with manual initialization
3
3
 
4
4
  class ReactiveActionsClient {
5
- constructor() {
6
- this.baseUrl = '/reactive_actions/execute';
5
+ constructor(options = {}) {
6
+ // Default configuration
7
+ this.config = {
8
+ baseUrl: '/reactive_actions/execute',
9
+ enableAutoBinding: true,
10
+ enableMutationObserver: true,
11
+ defaultHttpMethod: 'POST',
12
+ ...options
13
+ };
14
+
15
+ this.boundElements = new WeakSet();
16
+ this.initialized = false;
17
+ }
18
+
19
+ // Update configuration
20
+ configure(options = {}) {
21
+ this.config = { ...this.config, ...options };
22
+
23
+ // If baseUrl changed, update it
24
+ if (options.baseUrl) {
25
+ this.baseUrl = options.baseUrl;
26
+ }
27
+
28
+ return this;
29
+ }
30
+
31
+ // Get current configuration
32
+ getConfig() {
33
+ return { ...this.config };
34
+ }
35
+
36
+ // Initialize DOM bindings when called - must be called manually
37
+ initialize(options = {}) {
38
+ // Update configuration if options provided
39
+ if (Object.keys(options).length > 0) {
40
+ this.configure(options);
41
+ }
42
+
43
+ // Skip if already initialized
44
+ if (this.initialized) return this;
45
+
46
+ // Only bind if auto-binding is enabled
47
+ if (this.config.enableAutoBinding) {
48
+ this.bindExistingElements();
49
+
50
+ if (this.config.enableMutationObserver) {
51
+ this.setupMutationObserver();
52
+ }
53
+ }
54
+
55
+ this.initialized = true;
56
+ return this;
57
+ }
58
+
59
+ // Force re-initialization (useful after configuration changes)
60
+ reinitialize() {
61
+ this.initialized = false;
62
+ this.boundElements = new WeakSet();
63
+ this.initialize();
64
+ }
65
+
66
+ // Bind all existing elements with reactive-action attributes
67
+ bindExistingElements() {
68
+ const elements = document.querySelectorAll('[reactive-action]');
69
+ elements.forEach(element => this.bindElement(element));
70
+ }
71
+
72
+ // Set up mutation observer to handle dynamically added elements
73
+ setupMutationObserver() {
74
+ const observer = new MutationObserver(mutations => {
75
+ mutations.forEach(mutation => {
76
+ mutation.addedNodes.forEach(node => {
77
+ if (node.nodeType === Node.ELEMENT_NODE) {
78
+ // Check if the node itself has reactive-action
79
+ if (node.hasAttribute && node.hasAttribute('reactive-action')) {
80
+ this.bindElement(node);
81
+ }
82
+ // Check for child elements with reactive-action
83
+ const children = node.querySelectorAll ? node.querySelectorAll('[reactive-action]') : [];
84
+ children.forEach(child => this.bindElement(child));
85
+ }
86
+ });
87
+ });
88
+ });
89
+
90
+ observer.observe(document.body, {
91
+ childList: true,
92
+ subtree: true
93
+ });
94
+ }
95
+
96
+ // Bind a single element to reactive actions
97
+ bindElement(element) {
98
+ // Skip if already bound
99
+ if (this.boundElements.has(element)) return;
100
+
101
+ const actionValue = element.getAttribute('reactive-action');
102
+ if (!actionValue) return;
103
+
104
+ // Parse actions - support multiple actions separated by spaces
105
+ const actions = this.parseActions(actionValue);
106
+
107
+ actions.forEach(({ event, httpMethod, actionName }) => {
108
+ const listener = this.createActionListener(element, actionName, httpMethod);
109
+
110
+ // Handle special cases for form submission
111
+ if (event === 'submit' && element.tagName.toLowerCase() === 'form') {
112
+ element.addEventListener('submit', (e) => {
113
+ e.preventDefault();
114
+ listener(e);
115
+ });
116
+ } else {
117
+ element.addEventListener(event, listener);
118
+ }
119
+ });
120
+
121
+ // Mark as bound
122
+ this.boundElements.add(element);
123
+ }
124
+
125
+ // Parse action string(s) - supports "click->action_name", "click->post#action_name", or multiple actions
126
+ parseActions(actionValue) {
127
+ const actions = [];
128
+ const actionPairs = actionValue.trim().split(/\s+/);
129
+
130
+ actionPairs.forEach(pair => {
131
+ // Match patterns: "event->action", "event->method#action"
132
+ const match = pair.match(/^(\w+)->((?:(\w+)#)?(.+))$/);
133
+ if (match) {
134
+ const [, event, , httpMethod, actionName] = match;
135
+ actions.push({
136
+ event,
137
+ httpMethod: httpMethod ? httpMethod.toUpperCase() : this.config.defaultHttpMethod,
138
+ actionName
139
+ });
140
+ }
141
+ });
142
+
143
+ return actions;
144
+ }
145
+
146
+ // Create event listener for an action
147
+ createActionListener(element, actionName, httpMethod = null) {
148
+ return async (event) => {
149
+ try {
150
+ // Add loading state
151
+ this.setLoadingState(element, true);
152
+
153
+ // Extract data attributes
154
+ const actionParams = this.extractDataAttributes(element);
155
+
156
+ // Add form data if it's a form element
157
+ if (element.tagName.toLowerCase() === 'form') {
158
+ const formData = new FormData(element);
159
+ for (const [key, value] of formData.entries()) {
160
+ actionParams[key] = value;
161
+ }
162
+ }
163
+
164
+ // Add input value for input elements on change events
165
+ if (['input', 'select', 'textarea'].includes(element.tagName.toLowerCase()) &&
166
+ ['change', 'input'].includes(event.type)) {
167
+ actionParams.value = element.value;
168
+ }
169
+
170
+ // Execute the action using the existing method with specified HTTP method
171
+ const method = httpMethod || this.config.defaultHttpMethod;
172
+ const response = await this.execute(actionName, actionParams, { method });
173
+
174
+ // Handle response
175
+ this.handleActionResponse(element, response, event);
176
+
177
+ } catch (error) {
178
+ this.handleActionError(element, error, event);
179
+ } finally {
180
+ this.setLoadingState(element, false);
181
+ }
182
+ };
183
+ }
184
+
185
+ // Extract data attributes from element
186
+ extractDataAttributes(element) {
187
+ const data = {};
188
+ const attributes = element.attributes;
189
+
190
+ for (const attr of attributes) {
191
+ if (attr.name.startsWith('reactive-action-') &&
192
+ !['reactive-action', 'reactive-action-success', 'reactive-action-error'].includes(attr.name)) {
193
+ const key = attr.name
194
+ .replace('reactive-action-', '')
195
+ .replace(/-/g, '_'); // Convert kebab-case to snake_case
196
+ data[key] = attr.value;
197
+ }
198
+ }
199
+
200
+ return data;
201
+ }
202
+
203
+ // Set loading state on element
204
+ setLoadingState(element, loading) {
205
+ if (loading) {
206
+ element.classList.add('reactive-loading');
207
+ if (element.tagName.toLowerCase() === 'button' || element.tagName.toLowerCase() === 'input') {
208
+ element.disabled = true;
209
+ }
210
+ // Store original text if it's a button or link
211
+ if (['button', 'a'].includes(element.tagName.toLowerCase())) {
212
+ element.dataset.originalText = element.textContent;
213
+ element.textContent = element.dataset.loadingText || 'Loading...';
214
+ }
215
+ } else {
216
+ element.classList.remove('reactive-loading');
217
+ if (element.tagName.toLowerCase() === 'button' || element.tagName.toLowerCase() === 'input') {
218
+ element.disabled = false;
219
+ }
220
+ // Restore original text
221
+ if (element.dataset.originalText) {
222
+ element.textContent = element.dataset.originalText;
223
+ delete element.dataset.originalText;
224
+ }
225
+ }
226
+ }
227
+
228
+ // Handle successful action response
229
+ handleActionResponse(element, response, event) {
230
+ // Dispatch custom event
231
+ const customEvent = new CustomEvent('reactive-action:success', {
232
+ detail: { response, element, originalEvent: event },
233
+ bubbles: true
234
+ });
235
+ element.dispatchEvent(customEvent);
236
+
237
+ // Handle success callback if specified
238
+ const successCallback = element.getAttribute('reactive-action-success');
239
+ if (successCallback && typeof window[successCallback] === 'function') {
240
+ window[successCallback](response, element, event);
241
+ }
242
+
243
+ // Log successful responses that aren't ok
244
+ if (!response.ok) {
245
+ console.warn('ReactiveActions response not ok:', response);
246
+ }
247
+ }
248
+
249
+ // Handle action errors
250
+ handleActionError(element, error, event) {
251
+ console.error('ReactiveActions error:', error);
252
+
253
+ // Dispatch custom event
254
+ const customEvent = new CustomEvent('reactive-action:error', {
255
+ detail: { error, element, originalEvent: event },
256
+ bubbles: true
257
+ });
258
+ element.dispatchEvent(customEvent);
259
+
260
+ // Handle error callback if specified
261
+ const errorCallback = element.getAttribute('reactive-action-error');
262
+ if (errorCallback && typeof window[errorCallback] === 'function') {
263
+ window[errorCallback](error, element, event);
264
+ }
7
265
  }
8
266
 
9
267
  // Get CSRF token from meta tag
@@ -14,9 +272,12 @@ class ReactiveActionsClient {
14
272
 
15
273
  // Execute an action with parameters
16
274
  async execute(actionName, actionParams = {}, options = {}) {
17
- const method = options.method || 'POST';
275
+ const method = options.method || this.config.defaultHttpMethod;
18
276
  const contentType = options.contentType || 'application/json';
19
277
 
278
+ // Use configured baseUrl
279
+ const baseUrl = this.config.baseUrl;
280
+
20
281
  // Build request options
21
282
  const requestOptions = {
22
283
  method: method,
@@ -34,7 +295,7 @@ class ReactiveActionsClient {
34
295
  }
35
296
 
36
297
  // Build URL or prepare body based on HTTP method
37
- let url = this.baseUrl;
298
+ let url = baseUrl;
38
299
  if (['GET', 'HEAD'].includes(method.toUpperCase())) {
39
300
  // For GET requests, append parameters to URL
40
301
  const params = new URLSearchParams({
@@ -88,13 +349,10 @@ class ReactiveActionsClient {
88
349
  }
89
350
  }
90
351
 
91
- // Create and export the instance
92
- const ReactiveActions = new ReactiveActionsClient();
352
+ // ES Module export
353
+ export default ReactiveActionsClient;
93
354
 
94
- // For backward compatibility, also expose as global
355
+ // Global export
95
356
  if (typeof window !== 'undefined') {
96
- window.ReactiveActions = ReactiveActions;
97
- }
98
-
99
- // ES Module export
100
- export default ReactiveActions;
357
+ window.ReactiveActionsClient = ReactiveActionsClient;
358
+ }
@@ -4,6 +4,9 @@ module ReactiveActions
4
4
  # Main controller for handling reactive action requests
5
5
  # Processes action execution and manages error handling for the ReactiveActions engine
6
6
  class ReactiveActionsController < ApplicationController
7
+ # Include rate limiting functionality
8
+ include ReactiveActions::Controller::RateLimiter
9
+
7
10
  # Allow this action to handle any HTTP method
8
11
  def execute
9
12
  ReactiveActions.logger.info "ReactiveActionsController#execute[#{request.method}]: #{params.inspect}"
@@ -118,7 +121,8 @@ module ReactiveActions
118
121
  'MissingParameterError' => [:bad_request, 'MISSING_PARAMETER'],
119
122
  'InvalidParametersError' => [:bad_request, 'INVALID_PARAMETERS'],
120
123
  'UnauthorizedError' => [:forbidden, 'UNAUTHORIZED'],
121
- 'ActionExecutionError' => [:unprocessable_entity, 'EXECUTION_ERROR']
124
+ 'ActionExecutionError' => [:unprocessable_entity, 'EXECUTION_ERROR'],
125
+ 'RateLimitExceededError' => [:too_many_requests, 'RATE_LIMIT_EXCEEDED']
122
126
  }
123
127
 
124
128
  error_type = exception.class.name.demodulize
@@ -20,9 +20,27 @@ module ReactiveActions
20
20
  desc: 'Skip generating example action'
21
21
  class_option :mount_path, type: :string, default: '/reactive_actions',
22
22
  desc: 'Custom mount path for ReactiveActions'
23
+ class_option :auto_initialize, type: :boolean, default: true,
24
+ desc: 'Auto-initialize ReactiveActions client on page load'
25
+ class_option :enable_dom_binding, type: :boolean, default: true,
26
+ desc: 'Enable automatic DOM binding'
27
+ class_option :enable_mutation_observer, type: :boolean, default: true,
28
+ desc: 'Enable mutation observer for dynamic content'
29
+ class_option :default_http_method, type: :string, default: 'POST',
30
+ desc: 'Default HTTP method for actions'
23
31
  class_option :quiet, type: :boolean, default: false,
24
32
  desc: 'Run with minimal output'
25
33
 
34
+ # Rate limiting options
35
+ class_option :enable_rate_limiting, type: :boolean, default: false,
36
+ desc: 'Enable rate limiting features'
37
+ class_option :enable_global_rate_limiting, type: :boolean, default: false,
38
+ desc: 'Enable global controller-level rate limiting'
39
+ class_option :global_rate_limit, type: :numeric, default: 600,
40
+ desc: 'Global rate limit (requests per window)'
41
+ class_option :global_rate_limit_window, type: :string, default: '1.minute',
42
+ desc: 'Global rate limit window (e.g., "1.minute", "5.minutes")'
43
+
26
44
  def welcome_message
27
45
  return if options[:quiet]
28
46
 
@@ -31,24 +49,25 @@ module ReactiveActions
31
49
  say ''
32
50
  end
33
51
 
52
+ def gather_configuration
53
+ # Gather all configuration before creating files
54
+ setup_rate_limiting_configuration_for_initializer
55
+ end
56
+
34
57
  def create_initializer
35
- if options[:quiet] || yes?('Create ReactiveActions initializer? (recommended)', :green)
36
- template 'initializer.rb', 'config/initializers/reactive_actions.rb'
37
- say '✓ Created initializer', :green unless options[:quiet]
38
- else
39
- say '✗ Skipped initializer creation', :yellow
40
- end
58
+ return unless options[:quiet] || yes?('Create ReactiveActions initializer? (recommended)', :green)
59
+
60
+ template 'initializer.rb', 'config/initializers/reactive_actions.rb'
61
+ say '✓ Created initializer', :green unless options[:quiet]
41
62
  end
42
63
 
43
64
  def create_actions_directory
44
- if options[:quiet] || yes?('Create app/reactive_actions directory?', :green)
45
- empty_directory 'app/reactive_actions'
46
- say '✓ Created actions directory', :green unless options[:quiet]
65
+ return unless options[:quiet] || yes?('Create app/reactive_actions directory?', :green)
47
66
 
48
- ask_for_example_action unless options[:skip_example]
49
- else
50
- say '✗ Skipped actions directory creation', :yellow
51
- end
67
+ empty_directory 'app/reactive_actions'
68
+ say '✓ Created actions directory', :green unless options[:quiet]
69
+
70
+ ask_for_example_action unless options[:skip_example]
52
71
  end
53
72
 
54
73
  def configure_routes
@@ -64,13 +83,31 @@ module ReactiveActions
64
83
  def configure_javascript
65
84
  return if options[:skip_javascript]
66
85
  return unless javascript_needed?
86
+ return unless options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
67
87
 
68
- if options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
69
- add_javascript_to_importmap
70
- say '✓ Added JavaScript client to importmap', :green unless options[:quiet]
71
- else
72
- say '✗ Skipped JavaScript configuration', :yellow
73
- end
88
+ add_javascript_to_importmap
89
+ add_javascript_initialization
90
+ say '✓ Added JavaScript client with initialization', :green unless options[:quiet]
91
+ end
92
+
93
+ def javascript_configuration_options
94
+ return if options[:skip_javascript] || options[:quiet]
95
+ return unless javascript_needed?
96
+
97
+ configure_javascript_options
98
+ end
99
+
100
+ def rate_limiting_configuration
101
+ # Show rate limiting configuration summary if it was configured
102
+ return unless @rate_limiting_config && @rate_limiting_config[:rate_limiting_enabled] && !options[:quiet]
103
+
104
+ say '', :green
105
+ say '✓ Rate limiting configured:', :green
106
+ say " - Rate limiting: #{@rate_limiting_config[:rate_limiting_enabled] ? 'ENABLED' : 'DISABLED'}", :blue
107
+ say " - Global rate limiting: #{@rate_limiting_config[:global_rate_limiting_enabled] ? 'ENABLED' : 'DISABLED'}", :blue
108
+ return unless @rate_limiting_config[:global_rate_limiting_enabled]
109
+
110
+ say " - Global limit: #{@rate_limiting_config[:global_rate_limit]} requests per #{@rate_limiting_config[:global_rate_limit_window]}", :blue
74
111
  end
75
112
 
76
113
  def configuration_options
@@ -94,6 +131,62 @@ module ReactiveActions
94
131
 
95
132
  private
96
133
 
134
+ def setup_rate_limiting_configuration_for_initializer
135
+ if rate_limiting_options_provided?
136
+ configure_rate_limiting_from_options_quietly
137
+ elsif !options[:quiet] && ask_about_rate_limiting?
138
+ configure_rate_limiting_interactively
139
+ else
140
+ @rate_limiting_config = {}
141
+ end
142
+ end
143
+
144
+ def rate_limiting_options_provided?
145
+ options[:enable_rate_limiting] || options[:enable_global_rate_limiting]
146
+ end
147
+
148
+ def ask_about_rate_limiting?
149
+ yes?('Configure rate limiting? (optional but recommended for production)', :green)
150
+ end
151
+
152
+ def configure_rate_limiting_from_options_quietly
153
+ @rate_limiting_config = {
154
+ rate_limiting_enabled: options[:enable_rate_limiting] || options[:enable_global_rate_limiting],
155
+ global_rate_limiting_enabled: options[:enable_global_rate_limiting]
156
+ }
157
+
158
+ return unless options[:enable_global_rate_limiting]
159
+
160
+ @rate_limiting_config[:global_rate_limit] = options[:global_rate_limit]
161
+ @rate_limiting_config[:global_rate_limit_window] = options[:global_rate_limit_window]
162
+ end
163
+
164
+ def configure_rate_limiting_interactively
165
+ @rate_limiting_config = {}
166
+
167
+ enable_rate_limiting = yes?('Enable rate limiting features?', :green)
168
+ @rate_limiting_config[:rate_limiting_enabled] = enable_rate_limiting
169
+ return unless enable_rate_limiting
170
+
171
+ enable_global = yes?('Enable global controller-level rate limiting? (recommended)', :green)
172
+ @rate_limiting_config[:global_rate_limiting_enabled] = enable_global
173
+
174
+ return unless enable_global
175
+
176
+ limit = ask('Global rate limit (requests per window):', default: 600)
177
+ @rate_limiting_config[:global_rate_limit] = limit.to_i
178
+
179
+ window = ask('Global rate limit window:',
180
+ limited_to: %w[30.seconds 1.minute 5.minutes 15.minutes 1.hour],
181
+ default: '1.minute')
182
+ @rate_limiting_config[:global_rate_limit_window] = window
183
+
184
+ return unless yes?('Configure custom rate limit key generator? (advanced)', :green)
185
+
186
+ say 'You can customize how rate limit keys are generated in the initializer.'
187
+ @rate_limiting_config[:custom_key_generator] = true
188
+ end
189
+
97
190
  def should_skip_example_action?
98
191
  return true if options[:skip_example]
99
192
  return true if !options[:quiet] && !yes?('Generate example action file?', :green)
@@ -164,12 +257,9 @@ module ReactiveActions
164
257
  IMPORTMAP
165
258
 
166
259
  append_to_file 'config/importmap.rb', importmap_content
167
-
168
- # Add the import to application.js to ensure it executes
169
- add_import_to_application_js
170
260
  end
171
261
 
172
- def add_import_to_application_js
262
+ def add_javascript_initialization
173
263
  app_js_paths = %w[
174
264
  app/javascript/application.js
175
265
  app/assets/javascripts/application.js
@@ -179,29 +269,100 @@ module ReactiveActions
179
269
  app_js_path = app_js_paths.find { |path| File.exist?(path) }
180
270
 
181
271
  if app_js_path
182
- # Check if import already exists
183
272
  app_js_content = File.read(app_js_path)
184
- return if app_js_content.include?('import "reactive_actions"')
273
+ return if app_js_content.include?('ReactiveActions') || app_js_content.include?('reactive_actions')
185
274
 
186
- import_line = <<~JAVASCRIPT
275
+ js_config = generate_javascript_config
276
+ js_code = build_javascript_code(js_config)
187
277
 
188
- // Import ReactiveActions to make it globally available
189
- import "reactive_actions"
190
- JAVASCRIPT
278
+ append_to_file app_js_path, js_code
279
+ say "✓ Added ReactiveActions initialization to #{app_js_path}", :green unless options[:quiet]
280
+ else
281
+ say '⚠ Could not find application.js file. Please manually add ReactiveActions initialization.', :yellow
282
+ end
283
+ end
284
+
285
+ def generate_javascript_config
286
+ config = {}
287
+ mount_path = determine_mount_path || options[:mount_path]
288
+ config[:baseUrl] = "#{mount_path}/execute" if mount_path != '/reactive_actions'
289
+
290
+ config[:enableAutoBinding] = javascript_option_value(:enable_dom_binding)
291
+ config[:enableMutationObserver] = javascript_option_value(:enable_mutation_observer)
292
+ config[:defaultHttpMethod] = javascript_option_value(:default_http_method, 'POST')
293
+
294
+ # Remove default values to keep config clean
295
+ config.delete(:enableAutoBinding) if config[:enableAutoBinding] == true
296
+ config.delete(:enableMutationObserver) if config[:enableMutationObserver] == true
297
+ config.delete(:defaultHttpMethod) if config[:defaultHttpMethod] == 'POST'
298
+
299
+ config.empty? ? '{}' : JSON.pretty_generate(config)
300
+ end
301
+
302
+ def build_javascript_code(js_config)
303
+ <<~JAVASCRIPT
304
+
305
+ // Import and initialize ReactiveActions
306
+ import ReactiveActionsClient from "reactive_actions"
307
+
308
+ // Create and configure ReactiveActions instance
309
+ const reactiveActions = new ReactiveActionsClient(#{js_config});
191
310
 
192
- append_to_file app_js_path, import_line
193
- say "✓ Added ReactiveActions import to #{app_js_path}", :green unless options[:quiet]
311
+ #{generate_initialization_code}
312
+
313
+ // Make ReactiveActions globally available
314
+ window.ReactiveActions = reactiveActions;
315
+ JAVASCRIPT
316
+ end
317
+
318
+ def generate_initialization_code
319
+ if javascript_option_value(:auto_initialize)
320
+ <<~JAVASCRIPT.strip
321
+ // Initialize on DOM content loaded and Turbo events
322
+ document.addEventListener('DOMContentLoaded', () => reactiveActions.initialize());
323
+ document.addEventListener('turbo:load', () => reactiveActions.initialize());
324
+ document.addEventListener('turbo:frame-load', () => reactiveActions.initialize());
325
+ JAVASCRIPT
194
326
  else
195
- say ' Could not find application.js file. Please manually add: import "reactive_actions"', :yellow
327
+ '// Manual initialization - call reactiveActions.initialize() when ready'
328
+ end
329
+ end
330
+
331
+ def javascript_option_value(option_key, default_value = nil)
332
+ return options[option_key] if options.key?(option_key)
333
+ return default_value unless default_value.nil?
334
+
335
+ case option_key
336
+ when :enable_dom_binding, :enable_mutation_observer, :auto_initialize
337
+ true
338
+ when :default_http_method
339
+ 'POST'
340
+ end
341
+ end
342
+
343
+ def configure_javascript_options
344
+ return unless yes?('Configure JavaScript client options?', :green)
345
+
346
+ @javascript_options ||= {}
347
+ @javascript_options[:auto_initialize] = yes?('Auto-initialize ReactiveActions on page load? (recommended)', :green)
348
+
349
+ enable_dom = yes?('Enable automatic DOM binding? (recommended)', :green)
350
+ @javascript_options[:enable_dom_binding] = enable_dom
351
+
352
+ if enable_dom
353
+ enable_observer = yes?('Enable mutation observer for dynamic content? (recommended)', :green)
354
+ @javascript_options[:enable_mutation_observer] = enable_observer
196
355
  end
356
+
357
+ http_methods = %w[POST GET PUT PATCH DELETE]
358
+ default_method = ask('Default HTTP method:', limited_to: http_methods, default: 'POST')
359
+ @javascript_options[:default_http_method] = default_method unless default_method == 'POST'
197
360
  end
198
361
 
199
362
  def add_to_sprockets_manifest
200
363
  return unless File.exist?('app/assets/config/manifest.js')
201
364
 
202
- append_to_file 'app/assets/config/manifest.js' do
203
- "\n//= link reactive_actions.js\n"
204
- end
365
+ append_to_file 'app/assets/config/manifest.js', "\n//= link reactive_actions.js\n"
205
366
  end
206
367
 
207
368
  def configure_delegated_methods
@@ -256,6 +417,11 @@ module ReactiveActions
256
417
  template 'example_action.rb', "app/reactive_actions/#{sanitized_name}.rb"
257
418
  say "✓ Created #{sanitized_name}.rb", :green unless options[:quiet]
258
419
  end
420
+
421
+ # Template method to access rate limiting config in templates
422
+ def rate_limiting_config
423
+ @rate_limiting_config || {}
424
+ end
259
425
  end
260
426
  end
261
427
  end