rnx_rails 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +484 -0
- data/lib/rnx_rails/engine.rb +21 -0
- data/lib/rnx_rails/helpers.rb +158 -0
- data/lib/rnx_rails/version.rb +5 -0
- data/lib/rnx_rails.rb +38 -0
- data/rnx_rails.gemspec +35 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8500771a30b64cbac0e54aebd62985c64f9a8f90c716f552cdabee1faeac35b6
|
|
4
|
+
data.tar.gz: 52f1816f7dff0db762ccd50883fe00d907955260ee37c1ff9d3d02bb533cd8c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 379c5ce5f42fc081964f129132e05984d88e0d7204b0844c2815e1663b8bd3b6c6f1c633c29645bfd21b7653cfa4d142c28c7f7aa197224908cf6fcdfa470e55
|
|
7
|
+
data.tar.gz: 6b20a381a7f082903c0920f214f09c17167f29b1abe61e49af8d72a27e660578692767dfe7717d26c26b9dc467ec1ce1ec08151468d9bc0d0c2312fe1820edb9
|
data/README.md
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# rnx_rails
|
|
2
|
+
|
|
3
|
+
Rails gem providing view helpers and ERB directives for integrating [rnxJS](https://github.com/BaryoDev/rnxjs) reactive components into Rails applications.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MPL-2.0)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
rnx_rails provides Rails view helpers that make it easy to:
|
|
11
|
+
|
|
12
|
+
- Include rnxJS library and stylesheets in your ERB templates
|
|
13
|
+
- Create reactive state from Rails data
|
|
14
|
+
- Render rnxJS components with Rails variables
|
|
15
|
+
- Initialize rnxJS plugins (router, toast, storage)
|
|
16
|
+
- Bind Rails data to reactive components
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Add to Gemfile
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'rnx_rails', '~> 1.0'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Bundle
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The gem is automatically loaded by Rails' engine system.
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### 1. Include rnxJS in Your Base Layout
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
<%= rnx_scripts(cdn: true, theme: 'bootstrap') %>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Create Reactive State
|
|
43
|
+
|
|
44
|
+
Pass Rails data to rnxJS as reactive state:
|
|
45
|
+
|
|
46
|
+
```erb
|
|
47
|
+
<% user_data = {
|
|
48
|
+
name: current_user.name,
|
|
49
|
+
email: current_user.email,
|
|
50
|
+
role: current_user.role
|
|
51
|
+
} %>
|
|
52
|
+
|
|
53
|
+
<%= rnx_state(user_data, 'user') %>
|
|
54
|
+
|
|
55
|
+
<p>Welcome, <span data-bind="user.name"></span>!</p>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Render Components
|
|
59
|
+
|
|
60
|
+
Use view helpers to render rnxJS components:
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<%= rnx_component('Button', variant: 'primary', label: 'Save') %>
|
|
64
|
+
<%= rnx_component('Input', type: 'email', placeholder: 'user@example.com') %>
|
|
65
|
+
<%= rnx_component('DataTable', data: 'users', columns: 'columns', sortable: true) %>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## View Helpers Reference
|
|
69
|
+
|
|
70
|
+
### rnx_scripts
|
|
71
|
+
|
|
72
|
+
Include rnxJS library and stylesheets.
|
|
73
|
+
|
|
74
|
+
**Signature:**
|
|
75
|
+
```ruby
|
|
76
|
+
rnx_scripts(cdn: true, theme: 'bootstrap')
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `cdn` (Boolean, default: true) - Use CDN for resources
|
|
81
|
+
- `theme` (String, default: 'bootstrap') - Theme variant: 'bootstrap', 'm3', 'plugins', or nil
|
|
82
|
+
|
|
83
|
+
**Example:**
|
|
84
|
+
```erb
|
|
85
|
+
<head>
|
|
86
|
+
<%= rnx_scripts(cdn: true, theme: 'm3') %>
|
|
87
|
+
</head>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### rnx_state
|
|
91
|
+
|
|
92
|
+
Create reactive state from Rails data.
|
|
93
|
+
|
|
94
|
+
**Signature:**
|
|
95
|
+
```ruby
|
|
96
|
+
rnx_state(data, var_name = 'state')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
- `data` - Ruby hash or object to convert to reactive state
|
|
101
|
+
- `var_name` (String, optional, default: 'state') - Name of the global state variable
|
|
102
|
+
|
|
103
|
+
**Example:**
|
|
104
|
+
```erb
|
|
105
|
+
<% app_data = {
|
|
106
|
+
user: current_user,
|
|
107
|
+
notifications: current_user.notifications,
|
|
108
|
+
settings: {
|
|
109
|
+
theme: 'light',
|
|
110
|
+
language: 'en'
|
|
111
|
+
}
|
|
112
|
+
} %>
|
|
113
|
+
|
|
114
|
+
<%= rnx_state(app_data, 'appState') %>
|
|
115
|
+
|
|
116
|
+
<!-- Access reactive state in HTML -->
|
|
117
|
+
<span data-bind="appState.user.name"></span>
|
|
118
|
+
<span data-bind="appState.user.email"></span>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### rnx_component
|
|
122
|
+
|
|
123
|
+
Render rnxJS components with props.
|
|
124
|
+
|
|
125
|
+
**Signature:**
|
|
126
|
+
```ruby
|
|
127
|
+
rnx_component(name, props = {})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Parameters:**
|
|
131
|
+
- Component name (String)
|
|
132
|
+
- Hash of props (optional)
|
|
133
|
+
|
|
134
|
+
**Features:**
|
|
135
|
+
- Automatic string escaping for security
|
|
136
|
+
- Support for data binding expressions (state.*)
|
|
137
|
+
- Boolean attributes without values
|
|
138
|
+
- Numeric attribute values without quotes
|
|
139
|
+
- Snake_case to kebab-case conversion
|
|
140
|
+
|
|
141
|
+
**Examples:**
|
|
142
|
+
|
|
143
|
+
```erb
|
|
144
|
+
<!-- Button component -->
|
|
145
|
+
<%= rnx_component('Button', variant: 'primary', label: 'Save') %>
|
|
146
|
+
|
|
147
|
+
<!-- Input with data binding -->
|
|
148
|
+
<%= rnx_component('Input',
|
|
149
|
+
type: 'email',
|
|
150
|
+
placeholder: 'user@example.com',
|
|
151
|
+
data_bind: 'state.email'
|
|
152
|
+
) %>
|
|
153
|
+
|
|
154
|
+
<!-- DataTable with reactive data -->
|
|
155
|
+
<%= rnx_component('DataTable',
|
|
156
|
+
data: 'state.users',
|
|
157
|
+
columns: 'state.columns',
|
|
158
|
+
sortable: true,
|
|
159
|
+
pageable: true
|
|
160
|
+
) %>
|
|
161
|
+
|
|
162
|
+
<!-- Loop example -->
|
|
163
|
+
<% @items.each do |item| %>
|
|
164
|
+
<%= rnx_component('Card',
|
|
165
|
+
title: item.title,
|
|
166
|
+
content: item.content,
|
|
167
|
+
id: item.id
|
|
168
|
+
) %>
|
|
169
|
+
<% end %>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### rnx_plugin
|
|
173
|
+
|
|
174
|
+
Initialize rnxJS plugins.
|
|
175
|
+
|
|
176
|
+
**Signature:**
|
|
177
|
+
```ruby
|
|
178
|
+
rnx_plugin(name, options = {})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Parameters:**
|
|
182
|
+
- Plugin name: 'router', 'toast', or 'storage'
|
|
183
|
+
- Plugin-specific configuration options
|
|
184
|
+
|
|
185
|
+
**Available Plugins:**
|
|
186
|
+
|
|
187
|
+
#### Router Plugin
|
|
188
|
+
Navigate between pages with hash-based routing.
|
|
189
|
+
|
|
190
|
+
```erb
|
|
191
|
+
<%= rnx_plugin('router', mode: 'hash', default_route: '/') %>
|
|
192
|
+
|
|
193
|
+
<!-- Route-specific content visibility -->
|
|
194
|
+
<div data-route="/">Home Page</div>
|
|
195
|
+
<div data-route="/users">Users Page</div>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Toast Plugin
|
|
199
|
+
Display notifications.
|
|
200
|
+
|
|
201
|
+
```erb
|
|
202
|
+
<%= rnx_plugin('toast',
|
|
203
|
+
position: 'top-right',
|
|
204
|
+
duration: 3000,
|
|
205
|
+
max_toasts: 5
|
|
206
|
+
) %>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
window.rnx.toast.success('Operation completed!');
|
|
210
|
+
window.rnx.toast.error('An error occurred');
|
|
211
|
+
window.rnx.toast.warning('Warning message');
|
|
212
|
+
window.rnx.toast.info('Information');
|
|
213
|
+
</script>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Storage Plugin
|
|
217
|
+
Persist state to localStorage/sessionStorage.
|
|
218
|
+
|
|
219
|
+
```erb
|
|
220
|
+
<%= rnx_plugin('storage',
|
|
221
|
+
prefix: 'myapp_',
|
|
222
|
+
storage: 'localStorage'
|
|
223
|
+
) %>
|
|
224
|
+
|
|
225
|
+
<script>
|
|
226
|
+
// Persist state
|
|
227
|
+
window.rnx.storage.persist(state, 'user_prefs', ['theme', 'language']);
|
|
228
|
+
|
|
229
|
+
// Retrieve
|
|
230
|
+
const theme = window.rnx.storage.get('user_prefs_theme');
|
|
231
|
+
</script>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### data_bind
|
|
235
|
+
|
|
236
|
+
Create a data binding attribute.
|
|
237
|
+
|
|
238
|
+
**Signature:**
|
|
239
|
+
```ruby
|
|
240
|
+
data_bind(path)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Example:**
|
|
244
|
+
```erb
|
|
245
|
+
<span <%= data_bind('user.name') %>></span>
|
|
246
|
+
<!-- Output: data-bind="user.name" -->
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### data_rule
|
|
250
|
+
|
|
251
|
+
Create a validation rule attribute.
|
|
252
|
+
|
|
253
|
+
**Signature:**
|
|
254
|
+
```ruby
|
|
255
|
+
data_rule(rules)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Example:**
|
|
259
|
+
```erb
|
|
260
|
+
<input <%= data_rule('required|email|max:100') %> />
|
|
261
|
+
<!-- Output: data-rule="required|email|max:100" -->
|
|
262
|
+
|
|
263
|
+
<input <%= data_rule(['required', 'email']) %> />
|
|
264
|
+
<!-- Output: data-rule="required|email" -->
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Configuration
|
|
268
|
+
|
|
269
|
+
Create an initializer to configure rnx_rails:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# config/initializers/rnx.rb
|
|
273
|
+
RnxRails.configure do |config|
|
|
274
|
+
config.cdn = ENV.fetch('RNX_CDN', true)
|
|
275
|
+
config.theme = ENV.fetch('RNX_THEME', 'bootstrap')
|
|
276
|
+
config.storage_prefix = ENV.fetch('RNX_STORAGE_PREFIX', 'rnx_')
|
|
277
|
+
config.router_mode = ENV.fetch('RNX_ROUTER_MODE', 'hash')
|
|
278
|
+
config.toast_position = ENV.fetch('RNX_TOAST_POSITION', 'top-right')
|
|
279
|
+
config.toast_duration = ENV.fetch('RNX_TOAST_DURATION', 3000).to_i
|
|
280
|
+
config.toast_max = ENV.fetch('RNX_TOAST_MAX', 5).to_i
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Environment Variables
|
|
285
|
+
|
|
286
|
+
- `RNX_CDN` - Use CDN (default: true)
|
|
287
|
+
- `RNX_THEME` - Default theme (default: 'bootstrap')
|
|
288
|
+
- `RNX_STORAGE_PREFIX` - Storage key prefix (default: 'rnx_')
|
|
289
|
+
- `RNX_ROUTER_MODE` - Router mode (default: 'hash')
|
|
290
|
+
- `RNX_TOAST_POSITION` - Toast position (default: 'top-right')
|
|
291
|
+
- `RNX_TOAST_DURATION` - Toast duration in ms (default: 3000)
|
|
292
|
+
- `RNX_TOAST_MAX` - Max toasts on screen (default: 5)
|
|
293
|
+
|
|
294
|
+
## Complete Example
|
|
295
|
+
|
|
296
|
+
### app/controllers/profiles_controller.rb
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class ProfilesController < ApplicationController
|
|
300
|
+
before_action :authenticate_user!
|
|
301
|
+
|
|
302
|
+
def show
|
|
303
|
+
@user = current_user
|
|
304
|
+
@notifications = current_user.notifications.recent.limit(10)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### app/views/profiles/show.html.erb
|
|
310
|
+
|
|
311
|
+
```erb
|
|
312
|
+
<% app_state = {
|
|
313
|
+
user: {
|
|
314
|
+
name: @user.name,
|
|
315
|
+
email: @user.email,
|
|
316
|
+
role: @user.role,
|
|
317
|
+
avatar: @user.avatar_url
|
|
318
|
+
},
|
|
319
|
+
notifications: @notifications.map { |n| n.as_json },
|
|
320
|
+
routes: {
|
|
321
|
+
home: '/',
|
|
322
|
+
profile: '/profile',
|
|
323
|
+
settings: '/settings'
|
|
324
|
+
}
|
|
325
|
+
} %>
|
|
326
|
+
|
|
327
|
+
<%= rnx_state(app_state, 'appState') %>
|
|
328
|
+
<%= rnx_plugin('toast', position: 'top-right', duration: 3000) %>
|
|
329
|
+
|
|
330
|
+
<div class="profile-container">
|
|
331
|
+
<header>
|
|
332
|
+
<h1 data-bind="appState.user.name"></h1>
|
|
333
|
+
<p data-bind="appState.user.email"></p>
|
|
334
|
+
</header>
|
|
335
|
+
|
|
336
|
+
<section class="profile-actions">
|
|
337
|
+
<%= rnx_component('Button',
|
|
338
|
+
variant: 'primary',
|
|
339
|
+
label: 'Edit Profile',
|
|
340
|
+
onclick: 'editProfile()'
|
|
341
|
+
) %>
|
|
342
|
+
|
|
343
|
+
<%= rnx_component('Button',
|
|
344
|
+
variant: 'danger',
|
|
345
|
+
label: 'Logout',
|
|
346
|
+
onclick: 'logout()'
|
|
347
|
+
) %>
|
|
348
|
+
</section>
|
|
349
|
+
|
|
350
|
+
<section class="notifications">
|
|
351
|
+
<h2>Recent Notifications</h2>
|
|
352
|
+
<% @notifications.each do |notification| %>
|
|
353
|
+
<%= rnx_component('Card',
|
|
354
|
+
title: notification.title,
|
|
355
|
+
content: notification.message,
|
|
356
|
+
timestamp: notification.created_at,
|
|
357
|
+
dismissible: true
|
|
358
|
+
) %>
|
|
359
|
+
<% end %>
|
|
360
|
+
</section>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<script>
|
|
364
|
+
function editProfile() {
|
|
365
|
+
window.rnx.toast.info('Opening edit dialog');
|
|
366
|
+
// Navigate to edit page
|
|
367
|
+
window.location.href = appState.routes.profile + '/edit';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function logout() {
|
|
371
|
+
window.rnx.toast.info('Logging out...');
|
|
372
|
+
window.location.href = '/logout';
|
|
373
|
+
}
|
|
374
|
+
</script>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### app/views/layouts/application.html.erb
|
|
378
|
+
|
|
379
|
+
```erb
|
|
380
|
+
<!DOCTYPE html>
|
|
381
|
+
<html>
|
|
382
|
+
<head>
|
|
383
|
+
<title>My Rails App</title>
|
|
384
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
385
|
+
<%= csp_meta_tag %>
|
|
386
|
+
<%= csrf_meta_tags %>
|
|
387
|
+
|
|
388
|
+
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
|
389
|
+
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
|
390
|
+
|
|
391
|
+
<!-- rnxJS -->
|
|
392
|
+
<%= rnx_scripts(cdn: true, theme: 'bootstrap') %>
|
|
393
|
+
</head>
|
|
394
|
+
|
|
395
|
+
<body>
|
|
396
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
397
|
+
<div class="container-fluid">
|
|
398
|
+
<%= link_to 'My App', root_path, class: 'navbar-brand' %>
|
|
399
|
+
<div class="collapse navbar-collapse">
|
|
400
|
+
<ul class="navbar-nav ms-auto">
|
|
401
|
+
<% if user_signed_in? %>
|
|
402
|
+
<li class="nav-item">
|
|
403
|
+
<%= link_to current_user.name, profile_path, class: 'nav-link' %>
|
|
404
|
+
</li>
|
|
405
|
+
<li class="nav-item">
|
|
406
|
+
<%= link_to 'Logout', destroy_user_session_path, method: :delete, class: 'nav-link' %>
|
|
407
|
+
</li>
|
|
408
|
+
<% else %>
|
|
409
|
+
<li class="nav-item">
|
|
410
|
+
<%= link_to 'Login', new_user_session_path, class: 'nav-link' %>
|
|
411
|
+
</li>
|
|
412
|
+
<% end %>
|
|
413
|
+
</ul>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</nav>
|
|
417
|
+
|
|
418
|
+
<main>
|
|
419
|
+
<%= yield %>
|
|
420
|
+
</main>
|
|
421
|
+
|
|
422
|
+
<%= rnx_plugin('toast', RnxRails.configuration.toast.to_hash) if RnxRails.configuration.toast %>
|
|
423
|
+
</body>
|
|
424
|
+
</html>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Security Considerations
|
|
428
|
+
|
|
429
|
+
### HTML Escaping
|
|
430
|
+
|
|
431
|
+
All component props are automatically HTML-escaped to prevent XSS attacks:
|
|
432
|
+
|
|
433
|
+
```erb
|
|
434
|
+
<!-- Safe: Will escape < > & quotes -->
|
|
435
|
+
<%= rnx_component('Button', label: user_input) %>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Data Binding Expressions
|
|
439
|
+
|
|
440
|
+
String values starting with `state.`, `{`, or `[` are preserved without escaping for data binding:
|
|
441
|
+
|
|
442
|
+
```erb
|
|
443
|
+
<!-- Treated as data binding expression -->
|
|
444
|
+
<%= rnx_component('Div', data_content: 'state.userMessage') %>
|
|
445
|
+
|
|
446
|
+
<!-- Regular string, will be escaped -->
|
|
447
|
+
<%= rnx_component('Div', title: 'Hello World') %>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Testing
|
|
451
|
+
|
|
452
|
+
Tests are included in the `spec` directory. Run them with RSpec:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
bundle exec rspec
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Contributing
|
|
459
|
+
|
|
460
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
|
|
461
|
+
|
|
462
|
+
## License
|
|
463
|
+
|
|
464
|
+
MPL-2.0 - See [LICENSE](../../LICENSE) for details.
|
|
465
|
+
|
|
466
|
+
## Support
|
|
467
|
+
|
|
468
|
+
- Documentation: [rnxJS Documentation](https://github.com/BaryoDev/rnxjs)
|
|
469
|
+
- Issues: [GitHub Issues](https://github.com/BaryoDev/rnxjs/issues)
|
|
470
|
+
- Discussions: [GitHub Discussions](https://github.com/BaryoDev/rnxjs/discussions)
|
|
471
|
+
|
|
472
|
+
## Changelog
|
|
473
|
+
|
|
474
|
+
### 1.0.0 (2024)
|
|
475
|
+
|
|
476
|
+
- Initial release
|
|
477
|
+
- Rails view helpers: rnx_scripts, rnx_state, rnx_component, rnx_plugin
|
|
478
|
+
- Helper methods: data_bind, data_rule
|
|
479
|
+
- Bootstrap 5.3+ support
|
|
480
|
+
- Plugin integration (router, toast, storage)
|
|
481
|
+
- Configuration via initializers
|
|
482
|
+
- Full test coverage
|
|
483
|
+
- Type-safe component rendering
|
|
484
|
+
- HTML escaping and XSS prevention
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RnxRails
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace RnxRails
|
|
6
|
+
|
|
7
|
+
config.generators do |g|
|
|
8
|
+
g.test_framework :rspec
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer 'rnx_rails.view_helpers' do |app|
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
13
|
+
include RnxRails::Helpers
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer 'rnx_rails.asset_pipeline' do |app|
|
|
18
|
+
app.config.assets.paths << File.join(root, 'app', 'assets')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RnxRails
|
|
4
|
+
# Rails view helpers for rnxJS integration
|
|
5
|
+
module Helpers
|
|
6
|
+
# Generate rnxJS script includes
|
|
7
|
+
#
|
|
8
|
+
# @param cdn [Boolean] Use CDN (default: true)
|
|
9
|
+
# @param theme [String] Theme to include ('bootstrap', 'm3', 'plugins', or nil)
|
|
10
|
+
# @return [String] HTML script tags
|
|
11
|
+
def rnx_scripts(cdn: RnxRails.configuration.cdn, theme: RnxRails.configuration.theme)
|
|
12
|
+
if cdn
|
|
13
|
+
scripts = <<~HTML
|
|
14
|
+
<!-- rnxJS from CDN -->
|
|
15
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
16
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/dist/rnx.global.js"></script>
|
|
18
|
+
HTML
|
|
19
|
+
|
|
20
|
+
case theme
|
|
21
|
+
when 'm3'
|
|
22
|
+
scripts += '<link href="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/css/bootstrap-m3-theme.css" rel="stylesheet">'
|
|
23
|
+
when 'plugins'
|
|
24
|
+
scripts += '<link href="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/css/plugins.css" rel="stylesheet">'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
scripts.html_safe
|
|
28
|
+
else
|
|
29
|
+
# Local file serving
|
|
30
|
+
scripts = <<~HTML
|
|
31
|
+
<!-- rnxJS from local files -->
|
|
32
|
+
<link href="#{asset_path('bootstrap.min.css')}" rel="stylesheet">
|
|
33
|
+
<link href="#{asset_path('bootstrap-icons.min.css')}" rel="stylesheet">
|
|
34
|
+
<script src="#{asset_path('rnx.global.js')}"></script>
|
|
35
|
+
HTML
|
|
36
|
+
|
|
37
|
+
case theme
|
|
38
|
+
when 'm3'
|
|
39
|
+
scripts += "<link href=\"#{asset_path('bootstrap-m3-theme.css')}\" rel=\"stylesheet\">"
|
|
40
|
+
when 'plugins'
|
|
41
|
+
scripts += "<link href=\"#{asset_path('plugins.css')}\" rel=\"stylesheet\">"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
scripts.html_safe
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Create reactive state from Rails data
|
|
49
|
+
#
|
|
50
|
+
# @param data [Object] Data to convert to state
|
|
51
|
+
# @param var_name [String] Name of the state variable (default: 'state')
|
|
52
|
+
# @return [String] JavaScript code to initialize state
|
|
53
|
+
def rnx_state(data, var_name = 'state')
|
|
54
|
+
begin
|
|
55
|
+
json_data = data.to_json
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
return tag.script("console.error('rnxJS: Failed to serialize state: #{e.message}')")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
script = <<~JAVASCRIPT
|
|
61
|
+
<script>
|
|
62
|
+
// Initialize reactive state from Rails context
|
|
63
|
+
const #{var_name} = rnx.createReactiveState(#{json_data});
|
|
64
|
+
rnx.loadComponents(document.body, #{var_name});
|
|
65
|
+
</script>
|
|
66
|
+
JAVASCRIPT
|
|
67
|
+
|
|
68
|
+
script.html_safe
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Render an rnxJS component with props
|
|
72
|
+
#
|
|
73
|
+
# @param name [String] Component name
|
|
74
|
+
# @param props [Hash] Component properties
|
|
75
|
+
# @return [String] HTML component tag
|
|
76
|
+
def rnx_component(name, props = {})
|
|
77
|
+
attrs = []
|
|
78
|
+
|
|
79
|
+
props.each do |key, value|
|
|
80
|
+
# Convert snake_case to kebab-case
|
|
81
|
+
attr_name = key.to_s.gsub(/_/, '-')
|
|
82
|
+
|
|
83
|
+
if value.is_a?(TrueClass)
|
|
84
|
+
attrs << attr_name
|
|
85
|
+
elsif value.is_a?(FalseClass)
|
|
86
|
+
# Skip false boolean attributes
|
|
87
|
+
next
|
|
88
|
+
elsif value.is_a?(String) && (
|
|
89
|
+
value.start_with?('state.') ||
|
|
90
|
+
value.start_with?('{') ||
|
|
91
|
+
value.start_with?('[')
|
|
92
|
+
)
|
|
93
|
+
# Preserve data binding expressions
|
|
94
|
+
attrs << "#{attr_name}=\"#{value}\""
|
|
95
|
+
elsif value.is_a?(Numeric)
|
|
96
|
+
attrs << "#{attr_name}=#{value}"
|
|
97
|
+
else
|
|
98
|
+
# Escape and quote string values
|
|
99
|
+
escaped = ERB::Util.html_escape(value.to_s)
|
|
100
|
+
attrs << "#{attr_name}=\"#{escaped}\""
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
attrs_str = attrs.any? ? " #{attrs.join(' ')}" : ''
|
|
105
|
+
"<#{name}#{attrs_str}></#{name}>".html_safe
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Initialize an rnxJS plugin
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] Plugin name ('router', 'toast', 'storage')
|
|
111
|
+
# @param options [Hash] Plugin configuration options
|
|
112
|
+
# @return [String] JavaScript code to initialize plugin
|
|
113
|
+
def rnx_plugin(name, options = {})
|
|
114
|
+
begin
|
|
115
|
+
options_json = options.to_json
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
return tag.script("console.error('rnxJS: Failed to serialize plugin options: #{e.message}')")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
camel_name = name.to_s.camelize(:lower)
|
|
121
|
+
|
|
122
|
+
script = <<~JAVASCRIPT
|
|
123
|
+
<script>
|
|
124
|
+
// Initialize rnxJS plugin: #{name}
|
|
125
|
+
if (window.rnx && window.rnx.plugins) {
|
|
126
|
+
try {
|
|
127
|
+
const plugin = rnx.#{camel_name}Plugin ? rnx.#{camel_name}Plugin(#{options_json}) : null;
|
|
128
|
+
if (plugin) {
|
|
129
|
+
rnx.plugins.use(plugin);
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error("[rnxJS] Failed to initialize #{name} plugin:", e);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
JAVASCRIPT
|
|
137
|
+
|
|
138
|
+
script.html_safe
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create a data binding attribute
|
|
142
|
+
#
|
|
143
|
+
# @param path [String] Data path (e.g., 'user.name')
|
|
144
|
+
# @return [String] data-bind attribute
|
|
145
|
+
def data_bind(path)
|
|
146
|
+
"data-bind=\"#{ERB::Util.html_escape(path)}\"".html_safe
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Create a validation rule attribute
|
|
150
|
+
#
|
|
151
|
+
# @param rules [String, Array] Validation rules
|
|
152
|
+
# @return [String] data-rule attribute
|
|
153
|
+
def data_rule(rules)
|
|
154
|
+
rules_str = rules.is_a?(Array) ? rules.join('|') : rules.to_s
|
|
155
|
+
"data-rule=\"#{ERB::Util.html_escape(rules_str)}\"".html_safe
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/rnx_rails.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rnx_rails/version'
|
|
4
|
+
require_relative 'rnx_rails/helpers'
|
|
5
|
+
require_relative 'rnx_rails/engine'
|
|
6
|
+
|
|
7
|
+
module RnxRails
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Configuration for rnxRails
|
|
11
|
+
class << self
|
|
12
|
+
attr_accessor :configuration
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
self.configuration ||= Configuration.new
|
|
16
|
+
yield(configuration)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Configuration class
|
|
25
|
+
class Configuration
|
|
26
|
+
attr_accessor :cdn, :theme, :storage_prefix, :router_mode, :toast_position, :toast_duration, :toast_max
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@cdn = ENV.fetch('RNX_CDN', true)
|
|
30
|
+
@theme = ENV.fetch('RNX_THEME', 'bootstrap')
|
|
31
|
+
@storage_prefix = ENV.fetch('RNX_STORAGE_PREFIX', 'rnx_')
|
|
32
|
+
@router_mode = ENV.fetch('RNX_ROUTER_MODE', 'hash')
|
|
33
|
+
@toast_position = ENV.fetch('RNX_TOAST_POSITION', 'top-right')
|
|
34
|
+
@toast_duration = ENV.fetch('RNX_TOAST_DURATION', 3000).to_i
|
|
35
|
+
@toast_max = ENV.fetch('RNX_TOAST_MAX', 5).to_i
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/rnx_rails.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/rnx_rails/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'rnx_rails'
|
|
7
|
+
spec.version = RnxRails::VERSION
|
|
8
|
+
spec.authors = ['Arnel Irobles']
|
|
9
|
+
spec.email = ['arnel@arnelirobles.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'rnxJS integration for Rails - View helpers and directives for reactive components'
|
|
12
|
+
spec.description = 'Rails gem providing view helpers, ERB directives, and configuration for integrating rnxJS reactive components into Rails applications'
|
|
13
|
+
spec.homepage = 'https://github.com/BaryoDev/rnxjs'
|
|
14
|
+
spec.license = 'MPL-2.0'
|
|
15
|
+
spec.required_ruby_version = '>= 2.7'
|
|
16
|
+
|
|
17
|
+
spec.metadata = {
|
|
18
|
+
'homepage_uri' => spec.homepage,
|
|
19
|
+
'source_code_uri' => spec.homepage,
|
|
20
|
+
'changelog_uri' => "#{spec.homepage}/releases",
|
|
21
|
+
'bug_tracker_uri' => "#{spec.homepage}/issues",
|
|
22
|
+
'documentation_uri' => "#{spec.homepage}/tree/main/packages/rails-rnx",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|features)/}) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
|
|
31
|
+
spec.add_runtime_dependency 'rails', '>= 6.0'
|
|
32
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
33
|
+
spec.add_development_dependency 'rake', '>= 10.0'
|
|
34
|
+
spec.add_development_dependency 'rspec-rails', '>= 5.0'
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rnx_rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Arnel Irobles
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '10.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '10.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec-rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
description: Rails gem providing view helpers, ERB directives, and configuration for
|
|
70
|
+
integrating rnxJS reactive components into Rails applications
|
|
71
|
+
email:
|
|
72
|
+
- arnel@arnelirobles.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- README.md
|
|
78
|
+
- lib/rnx_rails.rb
|
|
79
|
+
- lib/rnx_rails/engine.rb
|
|
80
|
+
- lib/rnx_rails/helpers.rb
|
|
81
|
+
- lib/rnx_rails/version.rb
|
|
82
|
+
- rnx_rails.gemspec
|
|
83
|
+
homepage: https://github.com/BaryoDev/rnxjs
|
|
84
|
+
licenses:
|
|
85
|
+
- MPL-2.0
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://github.com/BaryoDev/rnxjs
|
|
88
|
+
source_code_uri: https://github.com/BaryoDev/rnxjs
|
|
89
|
+
changelog_uri: https://github.com/BaryoDev/rnxjs/releases
|
|
90
|
+
bug_tracker_uri: https://github.com/BaryoDev/rnxjs/issues
|
|
91
|
+
documentation_uri: https://github.com/BaryoDev/rnxjs/tree/main/packages/rails-rnx
|
|
92
|
+
post_install_message:
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '2.7'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 3.0.3.1
|
|
108
|
+
signing_key:
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: rnxJS integration for Rails - View helpers and directives for reactive components
|
|
111
|
+
test_files: []
|