opal-vite 0.2.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4bc96b96e9f545db30dd5ca2d6b0fb0aea32f1a03e9a733c6e3c802dc616506
4
- data.tar.gz: d753caf10cfca04a2709bfe446f3dde148eafc3da279d5fbb38797ad17b000da
3
+ metadata.gz: d02e4907cfae995a3c6496d788bf45bee0815c506be5685bc43caa417572f864
4
+ data.tar.gz: da33d83bca5ccc6eab82467d639c209911283a55bda3d9ce89d522639f1a2347
5
5
  SHA512:
6
- metadata.gz: 7a660f33fceef3720e67a7530cbda406c6218978557056cefc782e7dce014e0c73784ea46397091cfff1a130e2d0edc9003f051694f88f52c4f0f568748cba7c
7
- data.tar.gz: 017e824fd2eefd1ecc9cdae4dd7677bd74e6dd14082a0c5fc755f22f7cf879d61d0a519e55988f02d93bdf1d74aeb37ffdc5f8822f88d0a3e99b1d6cf229f047
6
+ metadata.gz: bd7612a8e8b417ede04c9dcc8f71a7238e170f0d4d909e8fb44015211aa3ef59253e62cea003e5050d0d75052ea54da3f2c77fbf7211147987a566c97677b9cf
7
+ data.tar.gz: 0f325e40dd3785586ca1c39912602b4553fe2b8842d3d22504378c1887d9026b41ab85f70562c17025905879bacc44cf4295a7d3abf799a75137b459634dbdf6
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.2.0"
3
+ VERSION = "0.2.2"
4
4
  end
5
5
  end
data/lib/opal-vite.rb CHANGED
@@ -21,9 +21,17 @@ module Opal
21
21
  yield(config) if block_given?
22
22
  end
23
23
 
24
+ # Returns the path to the opal/ directory in this gem
25
+ # Contains built-in concerns like StimulusHelpers
26
+ def opal_lib_path
27
+ File.expand_path('../../opal', __dir__)
28
+ end
29
+
24
30
  # CLI entry point for compilation
25
- def compile_for_vite(file_path)
26
- compiler = Compiler.new
31
+ # @param file_path [String] The path to the Ruby file to compile
32
+ # @param include_concerns [Boolean] Whether to include built-in concerns
33
+ def compile_for_vite(file_path, include_concerns: true)
34
+ compiler = Compiler.new(include_concerns: include_concerns)
27
35
  result = compiler.compile_file(file_path)
28
36
 
29
37
  # Output JSON to stdout for the Vite plugin to consume
@@ -0,0 +1,315 @@
1
+ # backtick_javascript: true
2
+
3
+ # ReactHelpers - DSL helpers for React applications with Opal
4
+ # Reduces backtick JavaScript usage in React components
5
+ module ReactHelpers
6
+ # ===================
7
+ # React Access
8
+ # ===================
9
+
10
+ # Get React from window
11
+ def react
12
+ Native(`window.React`)
13
+ end
14
+
15
+ # Get ReactDOM from window
16
+ def react_dom
17
+ Native(`window.ReactDOM`)
18
+ end
19
+
20
+ # ===================
21
+ # Window/Global Access
22
+ # ===================
23
+
24
+ # Get a property from window
25
+ def window_get(key)
26
+ `window[#{key}]`
27
+ end
28
+
29
+ # Set a property on window
30
+ def window_set(key, value)
31
+ `window[#{key}] = #{value}`
32
+ end
33
+
34
+ # Delete a property from window
35
+ def window_delete(key)
36
+ `delete window[#{key}]`
37
+ end
38
+
39
+ # ===================
40
+ # Console
41
+ # ===================
42
+
43
+ # Console log
44
+ def console_log(*args)
45
+ `console.log(...#{args})`
46
+ end
47
+
48
+ # Console warn
49
+ def console_warn(*args)
50
+ `console.warn(...#{args})`
51
+ end
52
+
53
+ # Console error
54
+ def console_error(*args)
55
+ `console.error(...#{args})`
56
+ end
57
+
58
+ # ===================
59
+ # Alerts/Dialogs
60
+ # ===================
61
+
62
+ # Show alert dialog
63
+ def alert_message(message)
64
+ `alert(#{message})`
65
+ end
66
+
67
+ # Show confirm dialog
68
+ def confirm_message(message)
69
+ `confirm(#{message})`
70
+ end
71
+
72
+ # Show prompt dialog
73
+ def prompt_message(message, default_value = '')
74
+ `prompt(#{message}, #{default_value})`
75
+ end
76
+
77
+ # ===================
78
+ # DOM Events
79
+ # ===================
80
+
81
+ # Execute block when DOM is ready
82
+ def on_dom_ready(&block)
83
+ `document.addEventListener('DOMContentLoaded', #{block})`
84
+ end
85
+
86
+ # Add event listener to window
87
+ def on_window_event(event_name, &block)
88
+ `window.addEventListener(#{event_name}, #{block})`
89
+ end
90
+
91
+ # Remove event listener from window
92
+ def off_window_event(event_name, handler)
93
+ `window.removeEventListener(#{event_name}, #{handler})`
94
+ end
95
+
96
+ # ===================
97
+ # DOM Query
98
+ # ===================
99
+
100
+ # Query single element
101
+ def query(selector)
102
+ `document.querySelector(#{selector})`
103
+ end
104
+
105
+ # Query all elements
106
+ def query_all(selector)
107
+ `Array.from(document.querySelectorAll(#{selector}))`
108
+ end
109
+
110
+ # Get element by ID
111
+ def get_element_by_id(id)
112
+ `document.getElementById(#{id})`
113
+ end
114
+
115
+ # ===================
116
+ # DOM Manipulation
117
+ # ===================
118
+
119
+ # Create element
120
+ def create_element(tag)
121
+ `document.createElement(#{tag})`
122
+ end
123
+
124
+ # Set innerHTML
125
+ def set_html(element, html)
126
+ `#{element}.innerHTML = #{html}`
127
+ end
128
+
129
+ # Set textContent
130
+ def set_text(element, text)
131
+ `#{element}.textContent = #{text}`
132
+ end
133
+
134
+ # Add class to element
135
+ def add_class(element, *classes)
136
+ `#{element}.classList.add(...#{classes})`
137
+ end
138
+
139
+ # Remove class from element
140
+ def remove_class(element, *classes)
141
+ `#{element}.classList.remove(...#{classes})`
142
+ end
143
+
144
+ # ===================
145
+ # React Element Creation Helpers
146
+ # ===================
147
+
148
+ # Create React element (shorthand)
149
+ def el(type, props = nil, *children)
150
+ react.createElement(type, props, *children)
151
+ end
152
+
153
+ # Create div element
154
+ def div(props = nil, *children, &block)
155
+ if block_given?
156
+ react.createElement('div', props, block.call)
157
+ else
158
+ react.createElement('div', props, *children)
159
+ end
160
+ end
161
+
162
+ # Create span element
163
+ def span(props = nil, *children, &block)
164
+ if block_given?
165
+ react.createElement('span', props, block.call)
166
+ else
167
+ react.createElement('span', props, *children)
168
+ end
169
+ end
170
+
171
+ # Create button element
172
+ def button(props = nil, *children, &block)
173
+ if block_given?
174
+ react.createElement('button', props, block.call)
175
+ else
176
+ react.createElement('button', props, *children)
177
+ end
178
+ end
179
+
180
+ # Create p element
181
+ def paragraph(props = nil, *children, &block)
182
+ if block_given?
183
+ react.createElement('p', props, block.call)
184
+ else
185
+ react.createElement('p', props, *children)
186
+ end
187
+ end
188
+
189
+ # Create heading elements
190
+ def h1(props = nil, *children)
191
+ react.createElement('h1', props, *children)
192
+ end
193
+
194
+ def h2(props = nil, *children)
195
+ react.createElement('h2', props, *children)
196
+ end
197
+
198
+ def h3(props = nil, *children)
199
+ react.createElement('h3', props, *children)
200
+ end
201
+
202
+ # ===================
203
+ # Timing
204
+ # ===================
205
+
206
+ # Set timeout
207
+ def set_timeout(delay_ms, &block)
208
+ `setTimeout(#{block}, #{delay_ms})`
209
+ end
210
+
211
+ # Set interval
212
+ def set_interval(interval_ms, &block)
213
+ `setInterval(#{block}, #{interval_ms})`
214
+ end
215
+
216
+ # Clear timeout
217
+ def clear_timeout(timeout_id)
218
+ `clearTimeout(#{timeout_id})`
219
+ end
220
+
221
+ # Clear interval
222
+ def clear_interval(interval_id)
223
+ `clearInterval(#{interval_id})`
224
+ end
225
+
226
+ # ===================
227
+ # LocalStorage
228
+ # ===================
229
+
230
+ # Get from localStorage
231
+ def storage_get(key)
232
+ `localStorage.getItem(#{key})`
233
+ end
234
+
235
+ # Set to localStorage
236
+ def storage_set(key, value)
237
+ `localStorage.setItem(#{key}, #{value})`
238
+ end
239
+
240
+ # Remove from localStorage
241
+ def storage_remove(key)
242
+ `localStorage.removeItem(#{key})`
243
+ end
244
+
245
+ # ===================
246
+ # Fetch API
247
+ # ===================
248
+
249
+ # Fetch with promise (returns Native promise)
250
+ def fetch_url(url, options = nil)
251
+ if options
252
+ Native(`fetch(#{url}, #{options.to_n})`)
253
+ else
254
+ Native(`fetch(#{url})`)
255
+ end
256
+ end
257
+
258
+ # ===================
259
+ # JSON
260
+ # ===================
261
+
262
+ # Parse JSON string
263
+ def parse_json(json_string)
264
+ `JSON.parse(#{json_string})`
265
+ end
266
+
267
+ # Stringify to JSON
268
+ def to_json(object)
269
+ `JSON.stringify(#{object})`
270
+ end
271
+
272
+ # ===================
273
+ # Type Conversion
274
+ # ===================
275
+
276
+ # Parse string to integer (wrapper for JavaScript parseInt)
277
+ # @param value [String, Number] Value to parse
278
+ # @param radix [Integer] Radix (default: 10)
279
+ # @return [Integer, NaN] Parsed integer
280
+ def parse_int(value, radix = 10)
281
+ `parseInt(#{value}, #{radix})`
282
+ end
283
+
284
+ # Parse string to float (wrapper for JavaScript parseFloat)
285
+ # @param value [String, Number] Value to parse
286
+ # @return [Float, NaN] Parsed float
287
+ def parse_float(value)
288
+ `parseFloat(#{value})`
289
+ end
290
+
291
+ # Check if value is NaN
292
+ # @param value [Number] Value to check
293
+ # @return [Boolean] true if NaN
294
+ def is_nan?(value)
295
+ `Number.isNaN(#{value})`
296
+ end
297
+
298
+ # Parse integer with default value (returns default if NaN)
299
+ # @param value [String, Number] Value to parse
300
+ # @param default_value [Integer] Default value if parsing fails
301
+ # @return [Integer] Parsed integer or default
302
+ def parse_int_or(value, default_value = 0)
303
+ result = parse_int(value)
304
+ is_nan?(result) ? default_value : result
305
+ end
306
+
307
+ # Parse float with default value (returns default if NaN)
308
+ # @param value [String, Number] Value to parse
309
+ # @param default_value [Float] Default value if parsing fails
310
+ # @return [Float] Parsed float or default
311
+ def parse_float_or(value, default_value = 0.0)
312
+ result = parse_float(value)
313
+ is_nan?(result) ? default_value : result
314
+ end
315
+ end
@@ -339,6 +339,391 @@ module OpalVite
339
339
  `#{array}[#{index}]`
340
340
  end
341
341
 
342
+ # ===== LocalStorage Methods =====
343
+
344
+ # Get item from localStorage
345
+ # @param key [String] Storage key
346
+ # @return [String, nil] Stored value or nil
347
+ def storage_get(key)
348
+ `localStorage.getItem(#{key})`
349
+ end
350
+
351
+ # Set item in localStorage
352
+ # @param key [String] Storage key
353
+ # @param value [String] Value to store
354
+ def storage_set(key, value)
355
+ `localStorage.setItem(#{key}, #{value})`
356
+ end
357
+
358
+ # Remove item from localStorage
359
+ # @param key [String] Storage key
360
+ def storage_remove(key)
361
+ `localStorage.removeItem(#{key})`
362
+ end
363
+
364
+ # Get JSON-parsed value from localStorage
365
+ # @param key [String] Storage key
366
+ # @param default [Object] Default value if key doesn't exist
367
+ # @return [Object] Parsed value or default
368
+ def storage_get_json(key, default = nil)
369
+ stored = storage_get(key)
370
+ return default if `#{stored} === null`
371
+ `JSON.parse(#{stored})`
372
+ end
373
+
374
+ # Set JSON-stringified value in localStorage
375
+ # @param key [String] Storage key
376
+ # @param value [Object] Value to store (will be JSON-stringified)
377
+ def storage_set_json(key, value)
378
+ `localStorage.setItem(#{key}, JSON.stringify(#{value.to_n}))`
379
+ end
380
+
381
+ # ===== Event Methods =====
382
+
383
+ # Dispatch a custom event on window
384
+ # @param name [String] Event name
385
+ # @param detail [Hash] Event detail data
386
+ def dispatch_window_event(name, detail = {})
387
+ native_detail = detail.is_a?(Hash) ? detail.to_n : detail
388
+ `
389
+ const event = new CustomEvent(#{name}, { detail: #{native_detail} });
390
+ window.dispatchEvent(event);
391
+ `
392
+ end
393
+
394
+ # Dispatch a custom event on the controller element
395
+ # @param name [String] Event name
396
+ # @param detail [Hash] Event detail data
397
+ def dispatch_event(name, detail = {})
398
+ native_detail = detail.is_a?(Hash) ? detail.to_n : detail
399
+ `
400
+ const event = new CustomEvent(#{name}, { detail: #{native_detail} });
401
+ this.element.dispatchEvent(event);
402
+ `
403
+ end
404
+
405
+ # Add window event listener
406
+ # @param name [String] Event name
407
+ # @yield Block to execute when event fires
408
+ def on_window_event(name, &block)
409
+ `window.addEventListener(#{name}, #{block})`
410
+ end
411
+
412
+ # Remove window event listener
413
+ # @param name [String] Event name
414
+ # @param handler [Native] Handler function to remove
415
+ def off_window_event(name, handler)
416
+ `window.removeEventListener(#{name}, #{handler})`
417
+ end
418
+
419
+ # Add document event listener
420
+ # @param name [String] Event name
421
+ # @yield Block to execute when event fires
422
+ def on_document_event(name, &block)
423
+ `document.addEventListener(#{name}, #{block})`
424
+ end
425
+
426
+ # Add event listener when DOM is ready
427
+ # @yield Block to execute when DOM is ready
428
+ def on_dom_ready(&block)
429
+ `document.addEventListener('DOMContentLoaded', #{block})`
430
+ end
431
+
432
+ # Add event listener to any element
433
+ # @param element [Native] DOM element
434
+ # @param name [String] Event name
435
+ # @yield Block to execute when event fires
436
+ def on_element_event(element, name, &block)
437
+ `#{element}.addEventListener(#{name}, #{block})`
438
+ end
439
+
440
+ # Remove event listener from element
441
+ # @param element [Native] DOM element
442
+ # @param name [String] Event name
443
+ # @param handler [Native] Handler function to remove
444
+ def off_element_event(element, name, handler)
445
+ `#{element}.removeEventListener(#{name}, #{handler})`
446
+ end
447
+
448
+ # Add event listener to controller's element (this.element)
449
+ # @param name [String] Event name
450
+ # @yield Block to execute when event fires
451
+ def on_controller_event(name, &block)
452
+ `
453
+ const handler = #{block};
454
+ this.element.addEventListener(#{name}, function(e) {
455
+ handler(e);
456
+ });
457
+ `
458
+ end
459
+
460
+ # Get the current event's target element
461
+ # @return [Native] Event target element
462
+ def event_target
463
+ `event.currentTarget`
464
+ end
465
+
466
+ # Get data attribute from current event target
467
+ # @param attr [String] Data attribute name (without 'data-' prefix)
468
+ # @return [String, nil] Attribute value
469
+ def event_data(attr)
470
+ `event.currentTarget.getAttribute('data-' + #{attr})`
471
+ end
472
+
473
+ # Get integer data attribute from current event target
474
+ # @param attr [String] Data attribute name (without 'data-' prefix)
475
+ # @return [Integer, nil] Parsed integer value
476
+ def event_data_int(attr)
477
+ parse_int(event_data(attr))
478
+ end
479
+
480
+ # Prevent default event behavior
481
+ def prevent_default
482
+ `event.preventDefault()`
483
+ end
484
+
485
+ # Get event key (for keyboard events)
486
+ # @return [String] Key name
487
+ def event_key
488
+ `event.key`
489
+ end
490
+
491
+ # ===== Element Methods =====
492
+
493
+ # Add class to element
494
+ # @param element [Native] DOM element
495
+ # @param class_name [String] CSS class to add
496
+ def add_class(element, class_name)
497
+ `#{element}.classList.add(#{class_name})`
498
+ end
499
+
500
+ # Remove class from element
501
+ # @param element [Native] DOM element
502
+ # @param class_name [String] CSS class to remove
503
+ def remove_class(element, class_name)
504
+ `#{element}.classList.remove(#{class_name})`
505
+ end
506
+
507
+ # Toggle class on element
508
+ # @param element [Native] DOM element
509
+ # @param class_name [String] CSS class to toggle
510
+ def toggle_class(element, class_name)
511
+ `#{element}.classList.toggle(#{class_name})`
512
+ end
513
+
514
+ # Check if element has class
515
+ # @param element [Native] DOM element
516
+ # @param class_name [String] CSS class to check
517
+ # @return [Boolean]
518
+ def has_class?(element, class_name)
519
+ `#{element}.classList.contains(#{class_name})`
520
+ end
521
+
522
+ # Set element attribute
523
+ # @param element [Native] DOM element
524
+ # @param attr [String] Attribute name
525
+ # @param value [String] Attribute value
526
+ def set_attr(element, attr, value)
527
+ `#{element}.setAttribute(#{attr}, #{value})`
528
+ end
529
+
530
+ # Get element attribute
531
+ # @param element [Native] DOM element
532
+ # @param attr [String] Attribute name
533
+ # @return [String, nil] Attribute value
534
+ def get_attr(element, attr)
535
+ `#{element}.getAttribute(#{attr})`
536
+ end
537
+
538
+ # Remove element attribute
539
+ # @param element [Native] DOM element
540
+ # @param attr [String] Attribute name
541
+ def remove_attr(element, attr)
542
+ `#{element}.removeAttribute(#{attr})`
543
+ end
544
+
545
+ # Set element style
546
+ # @param element [Native] DOM element
547
+ # @param property [String] CSS property
548
+ # @param value [String] CSS value
549
+ def set_style(element, property, value)
550
+ `#{element}.style[#{property}] = #{value}`
551
+ end
552
+
553
+ # Set element innerHTML
554
+ # @param element [Native] DOM element
555
+ # @param html [String] HTML content
556
+ def set_html(element, html)
557
+ `#{element}.innerHTML = #{html}`
558
+ end
559
+
560
+ # Set element textContent
561
+ # @param element [Native] DOM element
562
+ # @param text [String] Text content
563
+ def set_text(element, text)
564
+ `#{element}.textContent = #{text}`
565
+ end
566
+
567
+ # Get element value (for inputs)
568
+ # @param element [Native] DOM element
569
+ # @return [String] Element value
570
+ def get_value(element)
571
+ `#{element}.value`
572
+ end
573
+
574
+ # Set element value (for inputs)
575
+ # @param element [Native] DOM element
576
+ # @param value [String] Value to set
577
+ def set_value(element, value)
578
+ `#{element}.value = #{value}`
579
+ end
580
+
581
+ # Focus element
582
+ # @param element [Native] DOM element
583
+ def focus(element)
584
+ `#{element}.focus()`
585
+ end
586
+
587
+ # Check if element has attribute
588
+ # @param element [Native] DOM element
589
+ # @param attr [String] Attribute name
590
+ # @return [Boolean]
591
+ def has_attr?(element, attr)
592
+ `#{element}.hasAttribute(#{attr})`
593
+ end
594
+
595
+ # ===== DOM Creation Methods =====
596
+
597
+ # Create a new DOM element
598
+ # @param tag [String] HTML tag name
599
+ # @return [Native] Created element
600
+ def create_element(tag)
601
+ `document.createElement(#{tag})`
602
+ end
603
+
604
+ # Append child to element
605
+ # @param parent [Native] Parent element
606
+ # @param child [Native] Child element to append
607
+ def append_child(parent, child)
608
+ `#{parent}.appendChild(#{child})`
609
+ end
610
+
611
+ # Remove element from DOM
612
+ # @param element [Native] Element to remove
613
+ def remove_element(element)
614
+ `#{element}.remove()`
615
+ end
616
+
617
+ # Get next element sibling
618
+ # @param element [Native] DOM element
619
+ # @return [Native, nil] Next sibling element
620
+ def next_sibling(element)
621
+ `#{element}.nextElementSibling`
622
+ end
623
+
624
+ # Get previous element sibling
625
+ # @param element [Native] DOM element
626
+ # @return [Native, nil] Previous sibling element
627
+ def prev_sibling(element)
628
+ `#{element}.previousElementSibling`
629
+ end
630
+
631
+ # Get parent element
632
+ # @param element [Native] DOM element
633
+ # @return [Native, nil] Parent element
634
+ def parent(element)
635
+ `#{element}.parentElement`
636
+ end
637
+
638
+ # ===== DOM Query Methods =====
639
+
640
+ # Query selector on document
641
+ # @param selector [String] CSS selector
642
+ # @return [Native, nil] Element or nil
643
+ def query(selector)
644
+ `document.querySelector(#{selector})`
645
+ end
646
+
647
+ # Query selector all on document
648
+ # @param selector [String] CSS selector
649
+ # @return [Array] Array of elements
650
+ def query_all(selector)
651
+ `Array.from(document.querySelectorAll(#{selector}))`
652
+ end
653
+
654
+ # Query selector on controller element
655
+ # @param selector [String] CSS selector
656
+ # @return [Native, nil] Element or nil
657
+ def query_element(selector)
658
+ `this.element.querySelector(#{selector})`
659
+ end
660
+
661
+ # Query selector all on controller element
662
+ # @param selector [String] CSS selector
663
+ # @return [Array] Array of elements
664
+ def query_all_element(selector)
665
+ `Array.from(this.element.querySelectorAll(#{selector}))`
666
+ end
667
+
668
+ # ===== Document Methods =====
669
+
670
+ # Get document root element (html)
671
+ # @return [Native] HTML element
672
+ def doc_root
673
+ `document.documentElement`
674
+ end
675
+
676
+ # Set attribute on document root
677
+ # @param attr [String] Attribute name
678
+ # @param value [String] Attribute value
679
+ def set_root_attr(attr, value)
680
+ `document.documentElement.setAttribute(#{attr}, #{value})`
681
+ end
682
+
683
+ # Get attribute from document root
684
+ # @param attr [String] Attribute name
685
+ # @return [String, nil] Attribute value
686
+ def get_root_attr(attr)
687
+ `document.documentElement.getAttribute(#{attr})`
688
+ end
689
+
690
+ # ===== Template Methods =====
691
+
692
+ # Clone a template target's content
693
+ # @param name [Symbol, String] Template target name
694
+ # @return [Native] Cloned content
695
+ def clone_template(name)
696
+ method_name = "#{camelize(name, false)}Target"
697
+ `this[#{method_name}].content.cloneNode(true)`
698
+ end
699
+
700
+ # Get first element child from cloned template
701
+ # @param clone [Native] Cloned template content
702
+ # @return [Native, nil] First element child
703
+ def template_first_child(clone)
704
+ `#{clone}.firstElementChild`
705
+ end
706
+
707
+ # ===== Stimulus Controller Element Methods =====
708
+
709
+ # Add class to controller element
710
+ # @param class_name [String] CSS class to add
711
+ def element_add_class(class_name)
712
+ `this.element.classList.add(#{class_name})`
713
+ end
714
+
715
+ # Remove class from controller element
716
+ # @param class_name [String] CSS class to remove
717
+ def element_remove_class(class_name)
718
+ `this.element.classList.remove(#{class_name})`
719
+ end
720
+
721
+ # Toggle class on controller element
722
+ # @param class_name [String] CSS class to toggle
723
+ def element_toggle_class(class_name)
724
+ `this.element.classList.toggle(#{class_name})`
725
+ end
726
+
342
727
  # ===== Utility Methods =====
343
728
 
344
729
  # Generate a unique ID based on timestamp
@@ -363,6 +748,530 @@ module OpalVite
363
748
  `this[#{method_name}].blur()`
364
749
  end
365
750
 
751
+ # ===== Type Conversion Methods =====
752
+
753
+ # Parse string to integer (wrapper for JavaScript parseInt)
754
+ # @param value [String, Number] Value to parse
755
+ # @param radix [Integer] Radix (default: 10)
756
+ # @return [Integer, NaN] Parsed integer
757
+ def parse_int(value, radix = 10)
758
+ `parseInt(#{value}, #{radix})`
759
+ end
760
+
761
+ # Parse string to float (wrapper for JavaScript parseFloat)
762
+ # @param value [String, Number] Value to parse
763
+ # @return [Float, NaN] Parsed float
764
+ def parse_float(value)
765
+ `parseFloat(#{value})`
766
+ end
767
+
768
+ # Check if value is NaN
769
+ # @param value [Number] Value to check
770
+ # @return [Boolean] true if NaN
771
+ def is_nan?(value)
772
+ `Number.isNaN(#{value})`
773
+ end
774
+
775
+ # Parse integer with default value (returns default if NaN)
776
+ # @param value [String, Number] Value to parse
777
+ # @param default_value [Integer] Default value if parsing fails
778
+ # @return [Integer] Parsed integer or default
779
+ def parse_int_or(value, default_value = 0)
780
+ result = parse_int(value)
781
+ is_nan?(result) ? default_value : result
782
+ end
783
+
784
+ # Parse float with default value (returns default if NaN)
785
+ # @param value [String, Number] Value to parse
786
+ # @param default_value [Float] Default value if parsing fails
787
+ # @return [Float] Parsed float or default
788
+ def parse_float_or(value, default_value = 0.0)
789
+ result = parse_float(value)
790
+ is_nan?(result) ? default_value : result
791
+ end
792
+
793
+ # ===== JavaScript Property Access Methods =====
794
+
795
+ # Get a JavaScript property from the controller (this[name])
796
+ # @param name [Symbol, String] Property name
797
+ # @return [Native] Property value
798
+ def js_prop(name)
799
+ `this[#{name.to_s}]`
800
+ end
801
+
802
+ # Set a JavaScript property on the controller (this[name] = value)
803
+ # @param name [Symbol, String] Property name
804
+ # @param value [Object] Value to set
805
+ def js_set_prop(name, value)
806
+ `this[#{name.to_s}] = #{value}`
807
+ end
808
+
809
+ # Check if controller has a JavaScript property
810
+ # @param name [Symbol, String] Property name
811
+ # @return [Boolean] true if property exists and is truthy
812
+ def js_has_prop?(name)
813
+ `!!this[#{name.to_s}]`
814
+ end
815
+
816
+ # Call a JavaScript method on the controller (this[name](...args))
817
+ # @param name [Symbol, String] Method name
818
+ # @param args [Array] Arguments to pass
819
+ # @return [Native] Method return value
820
+ def js_call(name, *args)
821
+ if args.empty?
822
+ `this[#{name.to_s}]()`
823
+ else
824
+ native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
825
+ `this[#{name.to_s}].apply(this, #{native_args})`
826
+ end
827
+ end
828
+
829
+ # Call a method on a JavaScript object
830
+ # @param obj [Native] JavaScript object
831
+ # @param method [Symbol, String] Method name
832
+ # @param args [Array] Arguments to pass
833
+ # @return [Native] Method return value
834
+ def js_call_on(obj, method, *args)
835
+ if args.empty?
836
+ `#{obj}[#{method.to_s}]()`
837
+ else
838
+ native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
839
+ `#{obj}[#{method.to_s}].apply(#{obj}, #{native_args})`
840
+ end
841
+ end
842
+
843
+ # Get a property from a JavaScript object
844
+ # @param obj [Native] JavaScript object
845
+ # @param prop [Symbol, String] Property name
846
+ # @return [Native] Property value
847
+ def js_get(obj, prop)
848
+ `#{obj}[#{prop.to_s}]`
849
+ end
850
+
851
+ # Set a property on a JavaScript object
852
+ # @param obj [Native] JavaScript object
853
+ # @param prop [Symbol, String] Property name
854
+ # @param value [Object] Value to set
855
+ def js_set(obj, prop, value)
856
+ `#{obj}[#{prop.to_s}] = #{value}`
857
+ end
858
+
859
+ # ===== JSON Methods =====
860
+
861
+ # Parse JSON string to JavaScript object
862
+ # @param json_string [String] JSON string
863
+ # @return [Native] Parsed JavaScript object
864
+ def json_parse(json_string)
865
+ `JSON.parse(#{json_string})`
866
+ end
867
+
868
+ # Stringify JavaScript object to JSON
869
+ # @param obj [Object] Object to stringify
870
+ # @return [String] JSON string
871
+ def json_stringify(obj)
872
+ native_obj = obj.respond_to?(:to_n) ? obj.to_n : obj
873
+ `JSON.stringify(#{native_obj})`
874
+ end
875
+
876
+ # ===== Console Methods =====
877
+
878
+ # Log to console
879
+ # @param args [Array] Arguments to log
880
+ def console_log(*args)
881
+ `console.log.apply(console, #{args})`
882
+ end
883
+
884
+ # Log warning to console
885
+ # @param args [Array] Arguments to log
886
+ def console_warn(*args)
887
+ `console.warn.apply(console, #{args})`
888
+ end
889
+
890
+ # Log error to console
891
+ # @param args [Array] Arguments to log
892
+ def console_error(*args)
893
+ `console.error.apply(console, #{args})`
894
+ end
895
+
896
+ # ===== String Methods =====
897
+
898
+ # Get character at index from string
899
+ # @param str [String] JavaScript string
900
+ # @param index [Integer] Character index
901
+ # @return [String] Character at index
902
+ def js_string_char_at(str, index)
903
+ `#{str}.charAt(#{index})`
904
+ end
905
+
906
+ # Get substring
907
+ # @param str [String] JavaScript string
908
+ # @param start [Integer] Start index
909
+ # @param end_idx [Integer, nil] End index (optional)
910
+ # @return [String] Substring
911
+ def js_substring(str, start, end_idx = nil)
912
+ if end_idx
913
+ `#{str}.substring(#{start}, #{end_idx})`
914
+ else
915
+ `#{str}.substring(#{start})`
916
+ end
917
+ end
918
+
919
+ # Split string
920
+ # @param str [String] JavaScript string
921
+ # @param separator [String] Separator
922
+ # @return [Native] Array of substrings
923
+ def js_split(str, separator)
924
+ `#{str}.split(#{separator})`
925
+ end
926
+
927
+ # Trim whitespace from string
928
+ # @param str [String] JavaScript string
929
+ # @return [String] Trimmed string
930
+ def js_trim(str)
931
+ `#{str}.trim()`
932
+ end
933
+
934
+ # Check if string includes substring
935
+ # @param str [String] JavaScript string
936
+ # @param search [String] Substring to search for
937
+ # @return [Boolean] true if includes
938
+ def js_includes?(str, search)
939
+ `#{str}.includes(#{search})`
940
+ end
941
+
942
+ # ===== Comparison Methods =====
943
+
944
+ # Check strict equality (===) between two JavaScript values
945
+ # @param a [Native] First value
946
+ # @param b [Native] Second value
947
+ # @return [Boolean] true if strictly equal
948
+ def js_equals?(a, b)
949
+ `#{a} === #{b}`
950
+ end
951
+
952
+ # Check loose equality (==) between two JavaScript values
953
+ # @param a [Native] First value
954
+ # @param b [Native] Second value
955
+ # @return [Boolean] true if loosely equal
956
+ def js_loose_equals?(a, b)
957
+ `#{a} == #{b}`
958
+ end
959
+
960
+ # ===== Math Methods =====
961
+
962
+ # Generate random number between 0 and 1
963
+ # @return [Float] Random number
964
+ def js_random
965
+ `Math.random()`
966
+ end
967
+
968
+ # Get minimum of two numbers
969
+ # @param a [Number] First number
970
+ # @param b [Number] Second number
971
+ # @return [Number] Minimum value
972
+ def js_min(a, b)
973
+ `Math.min(#{a}, #{b})`
974
+ end
975
+
976
+ # Get maximum of two numbers
977
+ # @param a [Number] First number
978
+ # @param b [Number] Second number
979
+ # @return [Number] Maximum value
980
+ def js_max(a, b)
981
+ `Math.max(#{a}, #{b})`
982
+ end
983
+
984
+ # Get absolute value
985
+ # @param num [Number] Number
986
+ # @return [Number] Absolute value
987
+ def js_abs(num)
988
+ `Math.abs(#{num})`
989
+ end
990
+
991
+ # Round number
992
+ # @param num [Number] Number
993
+ # @return [Integer] Rounded number
994
+ def js_round(num)
995
+ `Math.round(#{num})`
996
+ end
997
+
998
+ # Ceiling of number
999
+ # @param num [Number] Number
1000
+ # @return [Integer] Ceiling value
1001
+ def js_ceil(num)
1002
+ `Math.ceil(#{num})`
1003
+ end
1004
+
1005
+ # Format number with fixed decimal places
1006
+ # @param num [Number] Number to format
1007
+ # @param digits [Integer] Number of decimal places
1008
+ # @return [String] Formatted number string
1009
+ def js_to_fixed(num, digits)
1010
+ `#{num}.toFixed(#{digits})`
1011
+ end
1012
+
1013
+ # Generate random integer between 0 and max (exclusive)
1014
+ # @param max [Integer] Maximum value (exclusive)
1015
+ # @return [Integer] Random integer
1016
+ def random_int(max)
1017
+ `Math.floor(Math.random() * #{max})`
1018
+ end
1019
+
1020
+ # Floor a number
1021
+ # @param num [Number] Number to floor
1022
+ # @return [Integer] Floored number
1023
+ def js_floor(num)
1024
+ `Math.floor(#{num})`
1025
+ end
1026
+
1027
+ # ===== Global Object Access =====
1028
+
1029
+ # Check if a global JavaScript object/class exists
1030
+ # @param name [String] Global name (e.g., 'Chart', 'React')
1031
+ # @return [Boolean] true if exists
1032
+ def js_global_exists?(name)
1033
+ `typeof window[#{name}] !== 'undefined'`
1034
+ end
1035
+
1036
+ # Get a global JavaScript object/class
1037
+ # @param name [String] Global name
1038
+ # @return [Native] Global object
1039
+ def js_global(name)
1040
+ `window[#{name}]`
1041
+ end
1042
+
1043
+ # Create new instance of a JavaScript class
1044
+ # @param klass [Native] JavaScript class/constructor
1045
+ # @param args [Array] Constructor arguments
1046
+ # @return [Native] New instance
1047
+ def js_new(klass, *args)
1048
+ if args.empty?
1049
+ `new klass()`
1050
+ else
1051
+ # Convert Ruby objects to native, pass JS objects as-is
1052
+ native_args = args.map { |a| `#{a} != null && typeof #{a}.$to_n === 'function' ? #{a}.$to_n() : #{a}` }
1053
+ # Use Reflect.construct for dynamic argument passing
1054
+ `Reflect.construct(#{klass}, #{native_args})`
1055
+ end
1056
+ end
1057
+
1058
+ # Define a JavaScript function on the controller (this[name] = function)
1059
+ # @param name [Symbol, String] Function name
1060
+ # @yield Block that becomes the function body
1061
+ def js_define_method(name, &block)
1062
+ `this[#{name.to_s}] = #{block}`
1063
+ end
1064
+
1065
+ # Define a JavaScript function on an object (obj[name] = function)
1066
+ # @param obj [Native] JavaScript object
1067
+ # @param name [Symbol, String] Function name
1068
+ # @yield Block that becomes the function body
1069
+ def js_define_method_on(obj, name, &block)
1070
+ `#{obj}[#{name.to_s}] = #{block}`
1071
+ end
1072
+
1073
+ # ===== Array Methods =====
1074
+
1075
+ # Get array length
1076
+ # @param arr [Native] JavaScript array
1077
+ # @return [Integer] Array length
1078
+ def js_length(arr)
1079
+ `#{arr}.length`
1080
+ end
1081
+
1082
+ # Map over array with block
1083
+ # @param arr [Native] JavaScript array
1084
+ # @yield [item] Block to execute for each item
1085
+ # @return [Native] New array with mapped values
1086
+ def js_map(arr, &block)
1087
+ `#{arr}.map(#{block})`
1088
+ end
1089
+
1090
+ # Filter array with block
1091
+ # @param arr [Native] JavaScript array
1092
+ # @yield [item] Block to execute for each item
1093
+ # @return [Native] Filtered array
1094
+ def js_filter(arr, &block)
1095
+ `#{arr}.filter(#{block})`
1096
+ end
1097
+
1098
+ # Reduce array with block
1099
+ # @param arr [Native] JavaScript array
1100
+ # @param initial [Object] Initial value
1101
+ # @yield [acc, item] Block to execute for each item
1102
+ # @return [Object] Reduced value
1103
+ def js_reduce(arr, initial, &block)
1104
+ `#{arr}.reduce(#{block}, #{initial})`
1105
+ end
1106
+
1107
+ # ForEach over array with block
1108
+ # @param arr [Native] JavaScript array
1109
+ # @yield [item, index] Block to execute for each item
1110
+ def js_each(arr, &block)
1111
+ `#{arr}.forEach(#{block})`
1112
+ end
1113
+
1114
+ # Slice array
1115
+ # @param arr [Native] JavaScript array
1116
+ # @param start [Integer] Start index
1117
+ # @param end_idx [Integer, nil] End index (optional)
1118
+ # @return [Native] Sliced array
1119
+ def js_slice(arr, start, end_idx = nil)
1120
+ if end_idx
1121
+ `#{arr}.slice(#{start}, #{end_idx})`
1122
+ else
1123
+ `#{arr}.slice(#{start})`
1124
+ end
1125
+ end
1126
+
1127
+ # ===== Object Methods =====
1128
+
1129
+ # Create empty JavaScript object
1130
+ # @return [Native] Empty JavaScript object
1131
+ def js_object
1132
+ `{}`
1133
+ end
1134
+
1135
+ # Get object keys
1136
+ # @param obj [Native] JavaScript object
1137
+ # @return [Native] Array of keys
1138
+ def js_keys(obj)
1139
+ `Object.keys(#{obj})`
1140
+ end
1141
+
1142
+ # Get object values
1143
+ # @param obj [Native] JavaScript object
1144
+ # @return [Native] Array of values
1145
+ def js_values(obj)
1146
+ `Object.values(#{obj})`
1147
+ end
1148
+
1149
+ # Get object entries
1150
+ # @param obj [Native] JavaScript object
1151
+ # @return [Native] Array of [key, value] pairs
1152
+ def js_entries(obj)
1153
+ `Object.entries(#{obj})`
1154
+ end
1155
+
1156
+ # Create Set from array and get size
1157
+ # @param arr [Native] JavaScript array
1158
+ # @return [Integer] Number of unique elements
1159
+ def js_unique_count(arr)
1160
+ `new Set(#{arr}).size`
1161
+ end
1162
+
1163
+ # ===== Fetch API =====
1164
+
1165
+ # Simple fetch that returns Promise-wrapped response
1166
+ # @param url [String] URL to fetch
1167
+ # @return [Native] Promise
1168
+ def js_fetch(url)
1169
+ `fetch(#{url})`
1170
+ end
1171
+
1172
+ # Fetch JSON from URL with callback
1173
+ # @param url [String] URL to fetch
1174
+ # @yield [data] Block to handle response data
1175
+ def fetch_json(url, &success_block)
1176
+ `
1177
+ fetch(#{url})
1178
+ .then(response => response.json())
1179
+ .then(data => #{success_block}.$call(data))
1180
+ .catch(error => console.error('Fetch error:', error))
1181
+ `
1182
+ end
1183
+
1184
+ # Fetch JSON from URL returning a Promise (for chaining)
1185
+ # @param url [String] URL to fetch
1186
+ # @return [Native] Promise that resolves to JSON data
1187
+ def fetch_json_promise(url)
1188
+ `fetch(#{url}).then(response => response.json())`
1189
+ end
1190
+
1191
+ # Fetch JSON with response validation
1192
+ # @param url [String] URL to fetch
1193
+ # @return [Native] Promise that resolves to JSON data or rejects on error
1194
+ def fetch_json_safe(url)
1195
+ `fetch(#{url}).then(function(response) { if (!response.ok) { throw new Error('Network response was not ok: ' + response.status); } return response.json(); })`
1196
+ end
1197
+
1198
+ # Fetch multiple URLs in parallel and get JSON results
1199
+ # @param urls [Array<String>] URLs to fetch
1200
+ # @return [Native] Promise that resolves to array of JSON results
1201
+ def fetch_all_json(urls)
1202
+ promises = urls.map { |url| fetch_json_promise(url) }
1203
+ `Promise.all(#{promises})`
1204
+ end
1205
+
1206
+ # Fetch JSON with success and error callbacks
1207
+ # @param url [String] URL to fetch
1208
+ # @param on_success [Proc] Success callback receiving data
1209
+ # @param on_error [Proc] Error callback receiving error
1210
+ def fetch_json_with_handlers(url, on_success:, on_error: nil)
1211
+ promise = fetch_json_safe(url)
1212
+ promise = js_then(promise) { |data| on_success.call(data) }
1213
+ if on_error
1214
+ js_catch(promise) { |error| on_error.call(error) }
1215
+ else
1216
+ js_catch(promise) { |error| console_error('Fetch error:', error) }
1217
+ end
1218
+ end
1219
+
1220
+ # ===== Promise Methods =====
1221
+
1222
+ # Create Promise.all from array of promises
1223
+ # @param promises [Array<Native>] Array of promises
1224
+ # @return [Native] Promise that resolves when all complete
1225
+ def promise_all(promises)
1226
+ `Promise.all(#{promises})`
1227
+ end
1228
+
1229
+ # Create Promise.race from array of promises
1230
+ # @param promises [Array<Native>] Array of promises
1231
+ # @return [Native] Promise that resolves when first completes
1232
+ def promise_race(promises)
1233
+ `Promise.race(#{promises})`
1234
+ end
1235
+
1236
+ # Create a resolved Promise with value
1237
+ # @param value [Object] Value to resolve with
1238
+ # @return [Native] Resolved Promise
1239
+ def promise_resolve(value)
1240
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
1241
+ `Promise.resolve(#{native_value})`
1242
+ end
1243
+
1244
+ # Create a rejected Promise with error
1245
+ # @param error [Object] Error to reject with
1246
+ # @return [Native] Rejected Promise
1247
+ def promise_reject(error)
1248
+ `Promise.reject(#{error})`
1249
+ end
1250
+
1251
+ # Add then handler to promise
1252
+ # @param promise [Native] JavaScript Promise
1253
+ # @yield [value] Block to handle resolved value
1254
+ # @return [Native] New Promise
1255
+ def js_then(promise, &block)
1256
+ `#{promise}.then(#{block})`
1257
+ end
1258
+
1259
+ # Add catch handler to promise
1260
+ # @param promise [Native] JavaScript Promise
1261
+ # @yield [error] Block to handle rejection
1262
+ # @return [Native] New Promise
1263
+ def js_catch(promise, &block)
1264
+ `#{promise}.catch(#{block})`
1265
+ end
1266
+
1267
+ # Add finally handler to promise
1268
+ # @param promise [Native] JavaScript Promise
1269
+ # @yield Block to execute regardless of outcome
1270
+ # @return [Native] New Promise
1271
+ def js_finally(promise, &block)
1272
+ `#{promise}.finally(#{block})`
1273
+ end
1274
+
366
1275
  private
367
1276
 
368
1277
  # Convert snake_case to camelCase
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal-vite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - stofu1234
@@ -114,6 +114,7 @@ files:
114
114
  - opal/opal_vite/concerns.rb
115
115
  - opal/opal_vite/concerns/dom_helpers.rb
116
116
  - opal/opal_vite/concerns/js_proxy_ex.rb
117
+ - opal/opal_vite/concerns/react_helpers.rb
117
118
  - opal/opal_vite/concerns/stimulus_helpers.rb
118
119
  - opal/opal_vite/concerns/storable.rb
119
120
  - opal/opal_vite/concerns/toastable.rb