opal_stimulus 0.1.2 → 0.1.4

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: 300ef0df34590b96644f09bec7f618c308189700c8b91c909bccb4bd7cb48760
4
- data.tar.gz: 4d405702dc56dcd9a0fd980c6c834dce308303a4e73f950239ed33c58493bd56
3
+ metadata.gz: cb159569bc40b9490b5433925d2b07e0a4cbd05d8c5f8c1ad86547efedd38361
4
+ data.tar.gz: 2d06de0ea3b604943301fd118de40c41b010378d7bf505878b0a99c1af51cffe
5
5
  SHA512:
6
- metadata.gz: 320be88eef00e84f254a120dd0c3029f6027daacba7aeab96c74af79de26b1150034de44200d299fad77d1817e9f298e1f1111d7c608fb076cf4db9ef571831b
7
- data.tar.gz: 548d689d02602637832156e6e88be3ff29224ce12dd5cba5de985d04c1567eabc1ad539120b9f6cd7dee41c687ec86324ac3179c4ffac04ed5cc15f79f5fb524
6
+ metadata.gz: d543dccccdb4c2fa6c0a1422681ac5181547e4f28e00f0fa7085015f8abefeaa37a96839bf9001335bd1fb2ff90ed3a91404eb515fb0e907e969324824914e34
7
+ data.tar.gz: 0c8822447330dbeef2dd40bd43bba1110dd458e5269675a9f6c08ca96c4177a5b2caa77dd307c3ece0665f260008111a3bab31a52bbfdaf9588c565fdbec4cfc
data/CHANGELOG.md CHANGED
@@ -11,3 +11,18 @@
11
11
  ## [0.1.2] - 2025-07-29
12
12
 
13
13
  - Implement Opal Source maps https://opalrb.com/docs/guides/v1.4.1/source_maps.html
14
+
15
+ ## [0.1.3] - 2025-07-29
16
+
17
+ - Add missing ignore for app/assets/builds/opal.js.map
18
+ - Replace opal-browser with opal_proxy, check it out here.
19
+
20
+ ## [0.1.4] - 2025-08-03
21
+
22
+ Fix Targets
23
+ Fix Outlets
24
+ Fix Values
25
+ Fix `element`
26
+ Fix Classes
27
+ Updare readme
28
+ Add `document` and `window` private methods
data/README.md CHANGED
@@ -25,7 +25,7 @@ bin/dev
25
25
 
26
26
  ## Basic Example
27
27
 
28
- Here's a Hello World example with OpalStimulus. Compare with the [original JavaScript example](https://stimulus.hotwired.dev/handbook/hello-stimulus):
28
+ Here's a Hello World example with OpalStimulus. Compare with the [original JavaScript example](https://stimulus.hotwired.dev/#:~:text=%2F%2F%20hello_controller.js%0Aimport%20%7B%20Controller%20%7D%20from%20%22stimulus%22%0A%0Aexport%20default%20class%20extends%20Controller%20%7B%0A%20%20static%20targets%20%3D%20%5B%20%22name%22%2C%20%22output%22%20%5D%0A%0A%20%20greet()%20%7B%0A%20%20%20%20this.outputTarget.textContent%20%3D%0A%20%20%20%20%20%20%60Hello%2C%20%24%7Bthis.nameTarget.value%7D!%60%0A%20%20%7D%0A%7D):
29
29
 
30
30
  **Ruby Controller:**
31
31
 
@@ -37,7 +37,7 @@ class HelloController < StimulusController
37
37
  self.targets = ["name", "output"]
38
38
 
39
39
  def greet
40
- output_target.content = "Hello, #{name_target.value}!"
40
+ output_target.text_content = "Hello, #{name_target.value}!"
41
41
  end
42
42
  end
43
43
  ```
@@ -61,9 +61,123 @@ end
61
61
 
62
62
  https://github.com/user-attachments/assets/c51ed28c-13d2-4e06-b882-1cc997e9627b
63
63
 
64
+ **Notes:**
64
65
 
66
+ - In general any reference method is snake cased, so `container` target will produce `container_target` and not ~`containerTarget`~
67
+ - Any `target`, `element`, `document`, `window` and `event` is a `JS::Proxy` instance, that provides a dynamic interface to JavaScript objects in Opal
68
+ - The frontend definition part will remain the same
65
69
 
66
70
 
71
+ ## Some examples based on [Stimulus Reference](https://stimulus.hotwired.dev/reference/controllers)
72
+
73
+ ### Lifecycle Callbacks
74
+ ```ruby
75
+ class AlertController < StimulusController
76
+ def initialize; end
77
+ def connect; end
78
+ def disconnect; end
79
+ end
80
+ ```
81
+
82
+ ### Actions
83
+ ```ruby
84
+ class WindowResizeController < StimulusController
85
+ def resized(event)
86
+ if !@resized && event.target.inner_width >= 1080
87
+ puts "Full HD? Nice!"
88
+ @resized = true
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### Targets
95
+ ```ruby
96
+ class ContainerController < StimulusController
97
+ self.targets = ["container"]
98
+
99
+ def container_target_connected
100
+ container_target.inner_html = <<~HTML
101
+ <h1>Test connected!</h1>
102
+ HTML
103
+ end
104
+
105
+ def container_target_disconnected
106
+ puts "Container disconnected!"
107
+ end
108
+ end
109
+ ```
110
+
111
+ ### Outlets
112
+ ```ruby
113
+ class ChatController < StimulusController
114
+ self.outlets = [ "user-status" ]
115
+
116
+ def connect
117
+ user_status_outlets.each do |status|
118
+ puts status
119
+ end
120
+ end
121
+ end
122
+ ```
123
+
124
+ ### Values
125
+ ```ruby
126
+ class LoaderController < StimulusController
127
+ self.values = { url: :string }
128
+
129
+ def connect
130
+ window.fetch(url_value).then do |response|
131
+ response.json().then do |data|
132
+ load_data(data)
133
+ end
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def load_data(data)
140
+ # ...
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### CSS Classes
146
+ ```ruby
147
+ class SearchController < StimulusController
148
+ self.classes = [ "loading" ]
149
+
150
+ def loadResults
151
+ element.class_list.add loading_class
152
+ end
153
+ end
154
+ ```
155
+
156
+ ## Extra tools
157
+ ### Window
158
+ ```ruby
159
+ class WindowController < StimulusController
160
+ def connect
161
+ window.alert "Hello world!"
162
+ window.set_timeout(-> {
163
+ puts "1. Timeout test OK (1s delay)"
164
+ }, 1000)
165
+ end
166
+ end
167
+ ```
168
+
169
+ ### Document
170
+ ```ruby
171
+ class DocumentController < StimulusController
172
+ def connect
173
+ headers = document.querySelectorAll("h1")
174
+ headers.each do |h1|
175
+ h1.text_content = "Opal is great!"
176
+ end
177
+ end
178
+ end
179
+ ```
180
+
67
181
  ## Contributing
68
182
 
69
183
  Bug reports and pull requests are welcome on GitHub at https://github.com/josephschito/opal_stimulus.
@@ -40,6 +40,7 @@ window.Controller = Controller;
40
40
  end
41
41
  append_to_file ".gitignore", "/.opal-cache\n"
42
42
  append_to_file ".gitignore", "app/assets/builds/opal.js\n"
43
+ append_to_file ".gitignore", "app/assets/builds/opal.js.map\n"
43
44
  empty_directory APPLICATION_OPAL_STIMULUS_BIN_PATH
44
45
  empty_directory APPLICATION_OPAL_STIMULUS_PATH
45
46
  empty_directory "#{APPLICATION_OPAL_STIMULUS_PATH}/controllers"
data/lib/install/opal CHANGED
@@ -4,11 +4,12 @@
4
4
  require "bundler/setup"
5
5
  require "listen"
6
6
  require "opal"
7
- require "opal-browser"
8
7
 
9
- GEM_NAME = "opal_stimulus"
8
+ GEMS = ["opal_proxy", "opal_stimulus"]
10
9
 
11
- Opal.use_gem(GEM_NAME) rescue Opal.append_path(File.expand_path("lib", Bundler.rubygems.find_name(GEM_NAME).first.full_gem_path))
10
+ GEMS.each do |gem_name|
11
+ Opal.use_gem(gem_name) rescue Opal.append_path(File.expand_path("lib", Bundler.rubygems.find_name(gem_name).first.full_gem_path))
12
+ end
12
13
 
13
14
  Opal.append_path("app/opal")
14
15
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  require "opal"
4
4
  require "native"
5
- require "promise"
6
- require "browser/setup/full"
5
+ require "js/proxy"
7
6
 
8
7
  class StimulusController < `Controller`
9
8
  include Native::Wrapper
@@ -31,7 +30,13 @@ class StimulusController < `Controller`
31
30
  %x{
32
31
  #{self.stimulus_controller}.prototype[name] = function (...args) {
33
32
  try {
34
- return this['$' + name].apply(this, args);
33
+ var wrappedArgs = args.map(function(arg) {
34
+ if (arg && typeof arg === "object" && !arg.$$class) {
35
+ return Opal.JS.Proxy.$new(arg);
36
+ }
37
+ return arg;
38
+ });
39
+ return this['$' + name].apply(this, wrappedArgs);
35
40
  } catch (e) {
36
41
  console.error("Uncaught", e);
37
42
  }
@@ -43,22 +48,25 @@ class StimulusController < `Controller`
43
48
  `#{self.stimulus_controller}.targets = targets`
44
49
 
45
50
  targets.each do |target|
46
- define_method(target + "_target") do
47
- Browser::DOM::Element.new(`this[#{target + "Target"}]`)
51
+ js_name = target.to_s
52
+ ruby_name = self.to_ruby_name(target)
53
+
54
+ define_method(ruby_name + "_target") do
55
+ JS::Proxy.new(`this[#{js_name + "Target"}]`)
48
56
  end
49
57
 
50
- define_method(target + "_targets") do
51
- `this[#{target + "Targets"}]`.map do |el|
52
- Browser::DOM::Element.new(el)
58
+ define_method(ruby_name + "_targets") do
59
+ `this[#{js_name + "Targets"}]`.map do |el|
60
+ JS::Proxy.new(el)
53
61
  end
54
62
  end
55
63
 
56
- define_method("has_" + target + "_target") do
57
- `return this[#{"has" + target.capitalize + "Target"}]`
64
+ define_method("has_" + ruby_name + "_target") do
65
+ `this[#{"has" + js_name.capitalize + "Target"}]`
58
66
  end
59
67
 
60
- snake_case_connected = target + "_target_connected"
61
- camel_case_connected = target + "TargetConnected"
68
+ snake_case_connected = ruby_name + "_target_connected"
69
+ camel_case_connected = js_name + "TargetConnected"
62
70
  %x{
63
71
  #{self.stimulus_controller}.prototype[#{camel_case_connected}] = function() {
64
72
  if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_connected})) {
@@ -67,8 +75,8 @@ class StimulusController < `Controller`
67
75
  }
68
76
  }
69
77
 
70
- snake_case_disconnected = target + "_target_disconnected"
71
- camel_case_disconnected = target + "TargetDisconnected"
78
+ snake_case_disconnected = ruby_name + "_target_disconnected"
79
+ camel_case_disconnected = js_name + "TargetDisconnected"
72
80
  %x{
73
81
  #{self.stimulus_controller}.prototype[#{camel_case_disconnected}] = function() {
74
82
  if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_disconnected})) {
@@ -83,22 +91,23 @@ class StimulusController < `Controller`
83
91
  `#{self.stimulus_controller}.outlets = outlets`
84
92
 
85
93
  outlets.each do |outlet|
86
- define_method(outlet + "_outlet") do
87
- Browser::DOM::Element.new(`this[#{outlet + "Outlet"}]`)
94
+ js_name = outlet.to_s
95
+ ruby_name = self.to_ruby_name(outlet)
96
+
97
+ define_method(ruby_name + "_outlet") do
98
+ `return this[#{js_name + "Outlet"}]`
88
99
  end
89
100
 
90
- define_method(outlet + "_outlets") do
91
- `this[#{outlet + "Outlets"}]`.map do |outlet|
92
- Browser::DOM::Element.new(outlet)
93
- end
101
+ define_method(ruby_name + "_outlets") do
102
+ `this[#{js_name + "Outlets"}]`
94
103
  end
95
104
 
96
- define_method("has_" + outlet + "_outlet") do
97
- `return this[#{"has" + outlet.capitalize + "Outlet"}]`
105
+ define_method("has_" + ruby_name + "_outlet") do
106
+ `return this[#{"has" + js_name.capitalize + "Outlet"}]`
98
107
  end
99
108
 
100
- snake_case_connected = outlet + "_outlet_connected"
101
- camel_case_connected = outlet + "OutletConnected"
109
+ snake_case_connected = ruby_name + "_outlet_connected"
110
+ camel_case_connected = js_name + "OutletConnected"
102
111
  %x{
103
112
  #{self.stimulus_controller}.prototype[#{camel_case_connected}] = function() {
104
113
  if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_connected})) {
@@ -107,8 +116,8 @@ class StimulusController < `Controller`
107
116
  }
108
117
  }
109
118
 
110
- snake_case_disconnected = outlet + "_outlet_disconnected"
111
- camel_case_disconnected = outlet + "OutletDisconnected"
119
+ snake_case_disconnected = ruby_name + "_outlet_disconnected"
120
+ camel_case_disconnected = js_name + "OutletDisconnected"
112
121
  %x{
113
122
  #{self.stimulus_controller}.prototype[#{camel_case_disconnected}] = function() {
114
123
  if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_disconnected})) {
@@ -123,37 +132,29 @@ class StimulusController < `Controller`
123
132
  js_values = {}
124
133
 
125
134
  values_hash.each do |name, type|
135
+ name = self.to_ruby_name(name)
136
+
126
137
  js_type = case type
127
- when String then "String"
128
- when Integer, Float, Numeric then "Number"
129
- when TrueClass, FalseClass, Boolean then "Boolean"
130
- when Array then "Array"
131
- when Hash, Object then "Object"
132
- else "String" # Default to String for unknown types
138
+ when :string then `String`
139
+ when :number then `Number`
140
+ when :boolean then `Boolean`
141
+ when :array then `Array`
142
+ when :object then `Object`
143
+ else
144
+ raise ArgumentError,
145
+ "Unsupported value type: #{type}, please use :string, :number, :boolean, :array, or :object"
133
146
  end
134
147
 
135
148
  js_values[name] = js_type
136
149
 
137
- # Define value getter method (snake_case)
138
- define_method(name.to_s) do
139
- # Convert JavaScript value to appropriate Ruby type
140
- js_value = `this[#{name + "Value"}]`
141
- case type
142
- when String
143
- js_value.to_s
144
- when Integer
145
- js_value.to_i
146
- when Float
147
- js_value.to_f
148
- when TrueClass, FalseClass, Boolean
149
- !!js_value
150
- when Array
151
- Native::Array.new(js_value)
152
- when Hash, Object
153
- Native::Object.new(js_value)
154
- else
155
- js_value
156
- end
150
+ `#{self.stimulus_controller}.values = #{js_values.to_n}`
151
+
152
+ define_method(name + "_value") do
153
+ `return this[#{name + "Value"}]`
154
+ end
155
+
156
+ define_method(name + "_value=") do |value|
157
+ `this[#{name + "Value"}]= #{value.to_n}`
157
158
  end
158
159
 
159
160
  define_method("has_#{name}") do
@@ -170,69 +171,47 @@ class StimulusController < `Controller`
170
171
  }
171
172
  }
172
173
  end
173
-
174
- `#{self.stimulus_controller}.values = #{js_values.to_n}`
175
174
  end
176
175
 
177
176
  def self.classes=(class_names = [])
178
- `#{self.stimulus_controller}.classes = #{class_names.to_n}`
177
+ `#{self.stimulus_controller}.classes = class_names`
179
178
 
180
179
  class_names.each do |class_name|
181
- define_method("add_#{class_name}_class") do
182
- `this.#{class_name}Classes.add()`
183
- end
180
+ js_name = class_name.to_s
181
+ ruby_name = self.to_ruby_name(class_name)
184
182
 
185
- define_method("remove_#{class_name}_class") do
186
- `this.#{class_name}Classes.remove()`
183
+ define_method("#{ruby_name}_class") do
184
+ `return this[#{js_name + "Class"}]`
187
185
  end
188
186
 
189
- define_method("has_#{class_name}_class?") do
190
- `return this.#{class_name}Classes.has()`
187
+ define_method("#{ruby_name}_classes") do
188
+ `return this[#{js_name + "Classes"}]`
191
189
  end
192
190
 
193
- define_method("toggle_#{class_name}_class") do
194
- `this.#{class_name}Classes.toggle()`
191
+ define_method("has_#{ruby_name}_class") do
192
+ `return this[#{"has" + js_name.capitalize + "Class"}]`
195
193
  end
196
194
  end
197
195
  end
198
196
 
199
- def add_class(class_name, element = nil)
200
- if element
201
- `this.addClass(#{class_name}, #{element})`
202
- else
203
- `this.addClass(#{class_name})`
204
- end
197
+ def element
198
+ JS::Proxy.new(`this.element`)
205
199
  end
206
200
 
207
- def remove_class(class_name, element = nil)
208
- if element
209
- `this.removeClass(#{class_name}, #{element})`
210
- else
211
- `this.removeClass(#{class_name})`
212
- end
213
- end
201
+ private
214
202
 
215
- def has_class?(class_name, element = nil)
216
- if element
217
- `return this.hasClass(#{class_name}, #{element})`
218
- else
219
- `return this.hasClass(#{class_name})`
220
- end
203
+ def self.to_ruby_name(name)
204
+ name
205
+ .to_s
206
+ .gsub(/([A-Z]+)/) { "_#{$1.downcase}" }
207
+ .sub(/^_/, '')
221
208
  end
222
209
 
223
- def toggle_class(class_name, force = nil, element = nil)
224
- if element && force != nil
225
- `this.toggleClass(#{class_name}, #{force}, #{element})`
226
- elsif element
227
- `this.toggleClass(#{class_name}, #{element})`
228
- elsif force != nil
229
- `this.toggleClass(#{class_name}, #{force})`
230
- else
231
- `this.toggleClass(#{class_name})`
232
- end
210
+ def window
211
+ @window ||= JS::Proxy.new($$.window)
233
212
  end
234
213
 
235
- def element
236
- `this.element`
214
+ def document
215
+ @document ||= JS::Proxy.new($$.document)
237
216
  end
238
217
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpalStimulus
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal_stimulus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Schito
@@ -38,19 +38,19 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: 3.9.0
40
40
  - !ruby/object:Gem::Dependency
41
- name: opal-browser
41
+ name: opal_proxy
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.3.5
46
+ version: 0.1.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 0.3.5
53
+ version: 0.1.0
54
54
  description: Opal Stimulus provides a way to write Stimulus controllers in Ruby, leveraging
55
55
  the Opal compiler to convert Ruby code into JavaScript. This allows developers familiar
56
56
  with Ruby to use the Stimulus framework without needing to write JavaScript directly.