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
@@ -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
|
-
|
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 ||
|
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 =
|
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
|
-
//
|
92
|
-
|
352
|
+
// ES Module export
|
353
|
+
export default ReactiveActionsClient;
|
93
354
|
|
94
|
-
//
|
355
|
+
// Global export
|
95
356
|
if (typeof window !== 'undefined') {
|
96
|
-
window.
|
97
|
-
}
|
98
|
-
|
99
|
-
// ES Module export
|
100
|
-
export default ReactiveActions;
|
357
|
+
window.ReactiveActionsClient = ReactiveActionsClient;
|
358
|
+
}
|
@@ -20,6 +20,14 @@ 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
|
|
@@ -67,12 +75,20 @@ module ReactiveActions
|
|
67
75
|
|
68
76
|
if options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
|
69
77
|
add_javascript_to_importmap
|
70
|
-
|
78
|
+
add_javascript_initialization
|
79
|
+
say '✓ Added JavaScript client with initialization', :green unless options[:quiet]
|
71
80
|
else
|
72
81
|
say '✗ Skipped JavaScript configuration', :yellow
|
73
82
|
end
|
74
83
|
end
|
75
84
|
|
85
|
+
def javascript_configuration_options
|
86
|
+
return if options[:skip_javascript] || options[:quiet]
|
87
|
+
return unless javascript_needed?
|
88
|
+
|
89
|
+
configure_javascript_options
|
90
|
+
end
|
91
|
+
|
76
92
|
def configuration_options
|
77
93
|
return if options[:quiet]
|
78
94
|
return unless yes?('Configure advanced options?', :green)
|
@@ -164,12 +180,9 @@ module ReactiveActions
|
|
164
180
|
IMPORTMAP
|
165
181
|
|
166
182
|
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
183
|
end
|
171
184
|
|
172
|
-
def
|
185
|
+
def add_javascript_initialization
|
173
186
|
app_js_paths = %w[
|
174
187
|
app/javascript/application.js
|
175
188
|
app/assets/javascripts/application.js
|
@@ -179,21 +192,104 @@ module ReactiveActions
|
|
179
192
|
app_js_path = app_js_paths.find { |path| File.exist?(path) }
|
180
193
|
|
181
194
|
if app_js_path
|
182
|
-
# Check if
|
195
|
+
# Check if ReactiveActions is already configured
|
183
196
|
app_js_content = File.read(app_js_path)
|
184
|
-
return if app_js_content.include?('
|
197
|
+
return if app_js_content.include?('ReactiveActions') || app_js_content.include?('reactive_actions')
|
198
|
+
|
199
|
+
# Generate JavaScript initialization code
|
200
|
+
js_config = generate_javascript_config
|
201
|
+
|
202
|
+
js_code = <<~JAVASCRIPT
|
203
|
+
|
204
|
+
// Import and initialize ReactiveActions
|
205
|
+
import ReactiveActionsClient from "reactive_actions"
|
185
206
|
|
186
|
-
|
207
|
+
// Create and configure ReactiveActions instance
|
208
|
+
const reactiveActions = new ReactiveActionsClient(#{js_config});
|
187
209
|
|
188
|
-
|
189
|
-
|
210
|
+
#{generate_initialization_code}
|
211
|
+
|
212
|
+
// Make ReactiveActions globally available
|
213
|
+
window.ReactiveActions = reactiveActions;
|
190
214
|
JAVASCRIPT
|
191
215
|
|
192
|
-
append_to_file app_js_path,
|
193
|
-
say "✓ Added ReactiveActions
|
216
|
+
append_to_file app_js_path, js_code
|
217
|
+
say "✓ Added ReactiveActions initialization to #{app_js_path}", :green unless options[:quiet]
|
218
|
+
else
|
219
|
+
say '⚠ Could not find application.js file. Please manually add ReactiveActions initialization.', :yellow
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def generate_javascript_config
|
224
|
+
config = {}
|
225
|
+
|
226
|
+
# Add mount path if different from default
|
227
|
+
mount_path = determine_mount_path || options[:mount_path]
|
228
|
+
config[:baseUrl] = "#{mount_path}/execute" if mount_path != '/reactive_actions'
|
229
|
+
|
230
|
+
# Add configuration options
|
231
|
+
config[:enableAutoBinding] = javascript_option_value(:enable_dom_binding)
|
232
|
+
config[:enableMutationObserver] = javascript_option_value(:enable_mutation_observer)
|
233
|
+
config[:defaultHttpMethod] = javascript_option_value(:default_http_method, 'POST')
|
234
|
+
|
235
|
+
# Remove default values to keep config clean
|
236
|
+
config.delete(:enableAutoBinding) if config[:enableAutoBinding] == true
|
237
|
+
config.delete(:enableMutationObserver) if config[:enableMutationObserver] == true
|
238
|
+
config.delete(:defaultHttpMethod) if config[:defaultHttpMethod] == 'POST'
|
239
|
+
|
240
|
+
config.empty? ? '{}' : JSON.pretty_generate(config)
|
241
|
+
end
|
242
|
+
|
243
|
+
def generate_initialization_code
|
244
|
+
if javascript_option_value(:auto_initialize)
|
245
|
+
<<~JAVASCRIPT.strip
|
246
|
+
// Initialize on DOM content loaded and Turbo events
|
247
|
+
document.addEventListener('DOMContentLoaded', () => reactiveActions.initialize());
|
248
|
+
document.addEventListener('turbo:load', () => reactiveActions.initialize());
|
249
|
+
document.addEventListener('turbo:frame-load', () => reactiveActions.initialize());
|
250
|
+
JAVASCRIPT
|
194
251
|
else
|
195
|
-
|
252
|
+
<<~JAVASCRIPT.strip
|
253
|
+
// Manual initialization - call reactiveActions.initialize() when ready
|
254
|
+
// Example: reactiveActions.initialize();
|
255
|
+
JAVASCRIPT
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def javascript_option_value(option_key, default_value = nil)
|
260
|
+
return options[option_key] if options.key?(option_key)
|
261
|
+
return default_value unless default_value.nil?
|
262
|
+
|
263
|
+
case option_key
|
264
|
+
when :enable_dom_binding, :enable_mutation_observer, :auto_initialize
|
265
|
+
true
|
266
|
+
when :default_http_method
|
267
|
+
'POST'
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def configure_javascript_options
|
272
|
+
return unless yes?('Configure JavaScript client options?', :green)
|
273
|
+
|
274
|
+
# Auto-initialize option
|
275
|
+
auto_init = yes?('Auto-initialize ReactiveActions on page load? (recommended)', :green)
|
276
|
+
@javascript_options ||= {}
|
277
|
+
@javascript_options[:auto_initialize] = auto_init
|
278
|
+
|
279
|
+
# DOM binding option
|
280
|
+
enable_dom = yes?('Enable automatic DOM binding? (recommended)', :green)
|
281
|
+
@javascript_options[:enable_dom_binding] = enable_dom
|
282
|
+
|
283
|
+
# Mutation observer option
|
284
|
+
if enable_dom
|
285
|
+
enable_observer = yes?('Enable mutation observer for dynamic content? (recommended)', :green)
|
286
|
+
@javascript_options[:enable_mutation_observer] = enable_observer
|
196
287
|
end
|
288
|
+
|
289
|
+
# Default HTTP method
|
290
|
+
http_methods = %w[POST GET PUT PATCH DELETE]
|
291
|
+
default_method = ask('Default HTTP method:', limited_to: http_methods, default: 'POST')
|
292
|
+
@javascript_options[:default_http_method] = default_method unless default_method == 'POST'
|
197
293
|
end
|
198
294
|
|
199
295
|
def add_to_sprockets_manifest
|
@@ -2,31 +2,107 @@
|
|
2
2
|
|
3
3
|
ReactiveActions has been installed successfully!
|
4
4
|
|
5
|
-
|
5
|
+
SETUP COMPLETED:
|
6
|
+
================
|
7
|
+
|
8
|
+
✓ Routes added to config/routes.rb:
|
6
9
|
mount ReactiveActions::Engine, at: '/reactive_actions'
|
7
10
|
|
8
|
-
|
9
|
-
|
11
|
+
✓ JavaScript client configured with automatic initialization
|
12
|
+
- Added to config/importmap.rb
|
13
|
+
- Initialized in app/javascript/application.js
|
14
|
+
- Available globally as window.ReactiveActions
|
10
15
|
|
11
|
-
|
16
|
+
✓ Sample action created:
|
12
17
|
app/reactive_actions/example_action.rb
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
19
|
+
QUICK START:
|
20
|
+
============
|
21
|
+
|
22
|
+
1. Test the API endpoint:
|
23
|
+
curl -X POST http://localhost:3000/reactive_actions/execute \
|
24
|
+
-H "Content-Type: application/json" \
|
25
|
+
-d '{"action_name": "example", "action_params": {"name": "YourName"}}'
|
26
|
+
|
27
|
+
2. Use JavaScript client:
|
28
|
+
ReactiveActions.execute('example', { name: 'YourName' })
|
29
|
+
.then(response => console.log(response))
|
30
|
+
|
31
|
+
3. Use DOM binding (no JavaScript required):
|
32
|
+
<button reactive-action="click->example"
|
33
|
+
reactive-action-name="YourName">
|
34
|
+
Test Action
|
35
|
+
</button>
|
36
|
+
|
37
|
+
JAVASCRIPT CLIENT:
|
38
|
+
==================
|
39
|
+
|
40
|
+
The ReactiveActions client is automatically initialized and available as:
|
41
|
+
- window.ReactiveActions (global)
|
42
|
+
- Supports all HTTP methods: get, post, put, patch, delete
|
43
|
+
- Automatic CSRF token handling
|
44
|
+
- DOM binding with mutation observer
|
45
|
+
|
46
|
+
Examples:
|
47
|
+
// Execute actions
|
48
|
+
ReactiveActions.execute('action_name', { param: 'value' })
|
49
|
+
ReactiveActions.post('create_user', { name: 'John' })
|
50
|
+
ReactiveActions.get('fetch_data', { id: 123 })
|
51
|
+
|
52
|
+
// Configuration (if needed)
|
53
|
+
ReactiveActions.configure({
|
54
|
+
baseUrl: '/custom/path/execute',
|
55
|
+
defaultHttpMethod: 'PUT'
|
56
|
+
}).reinitialize()
|
57
|
+
|
58
|
+
DOM BINDING:
|
59
|
+
============
|
60
|
+
|
61
|
+
Add reactive-action attributes to any element:
|
62
|
+
|
63
|
+
<!-- Basic actions -->
|
64
|
+
<button reactive-action="click->update_user">Update</button>
|
65
|
+
<input reactive-action="change->search" type="text">
|
66
|
+
<form reactive-action="submit->create_post">...</form>
|
67
|
+
|
68
|
+
<!-- With HTTP methods -->
|
69
|
+
<button reactive-action="click->put#update_user">Update (PUT)</button>
|
70
|
+
<button reactive-action="click->delete#remove_user">Delete</button>
|
71
|
+
|
72
|
+
<!-- Pass data via attributes -->
|
73
|
+
<button reactive-action="click->update_user"
|
74
|
+
reactive-action-user-id="123"
|
75
|
+
reactive-action-name="John">
|
76
|
+
Update User
|
77
|
+
</button>
|
78
|
+
|
79
|
+
CREATING ACTIONS:
|
80
|
+
=================
|
81
|
+
|
82
|
+
Add files to app/reactive_actions/:
|
83
|
+
|
84
|
+
# app/reactive_actions/update_user_action.rb
|
85
|
+
class UpdateUserAction < ReactiveActions::ReactiveAction
|
86
|
+
def action
|
87
|
+
user = User.find(action_params[:user_id])
|
88
|
+
user.update(name: action_params[:name])
|
89
|
+
@result = { success: true, user: user.as_json }
|
90
|
+
end
|
91
|
+
|
92
|
+
def response
|
93
|
+
render json: @result
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
DOCUMENTATION & SUPPORT:
|
98
|
+
========================
|
99
|
+
|
100
|
+
For complete documentation and examples:
|
30
101
|
https://github.com/IstvanMs/reactive-actions
|
31
102
|
|
103
|
+
For troubleshooting and configuration options:
|
104
|
+
Check config/initializers/reactive_actions.rb
|
105
|
+
|
106
|
+
Happy coding with ReactiveActions! 🚀
|
107
|
+
|
32
108
|
===============================================================================
|
@@ -11,3 +11,17 @@ end
|
|
11
11
|
|
12
12
|
# Set the logger for ReactiveActions
|
13
13
|
ReactiveActions.logger = Rails.logger
|
14
|
+
|
15
|
+
# JavaScript Client Configuration
|
16
|
+
# ================================
|
17
|
+
# The JavaScript client is automatically initialized in application.js
|
18
|
+
# You can reconfigure it at runtime if needed:
|
19
|
+
#
|
20
|
+
# ReactiveActions.configure({
|
21
|
+
# baseUrl: '/custom/path/execute',
|
22
|
+
# enableAutoBinding: true,
|
23
|
+
# enableMutationObserver: true,
|
24
|
+
# defaultHttpMethod: 'POST'
|
25
|
+
# }).reinitialize();
|
26
|
+
#
|
27
|
+
# Available globally as window.ReactiveActions
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reactive-actions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.pre.alpha.
|
4
|
+
version: 0.1.0.pre.alpha.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Istvan Meszaros
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|