opal_stimulus 0.1.4 → 0.1.6

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: cb159569bc40b9490b5433925d2b07e0a4cbd05d8c5f8c1ad86547efedd38361
4
- data.tar.gz: 2d06de0ea3b604943301fd118de40c41b010378d7bf505878b0a99c1af51cffe
3
+ metadata.gz: 669d38bd3eaad485f44b8347ca5008ee6d6001a7d622f305ad78a413f2fbee43
4
+ data.tar.gz: 36bd60dc686ef27c23e1db07a3359679f1b6de0fc9ef28e11ef79f891bc79303
5
5
  SHA512:
6
- metadata.gz: d543dccccdb4c2fa6c0a1422681ac5181547e4f28e00f0fa7085015f8abefeaa37a96839bf9001335bd1fb2ff90ed3a91404eb515fb0e907e969324824914e34
7
- data.tar.gz: 0c8822447330dbeef2dd40bd43bba1110dd458e5269675a9f6c08ca96c4177a5b2caa77dd307c3ece0665f260008111a3bab31a52bbfdaf9588c565fdbec4cfc
6
+ metadata.gz: 5ff23a23826bb81e640c712da250e950053376312a5af7bd6dc10e5175ab2480257ec859868e8c24dc8f7ba7d343f9147bf119852e9475d927e44fd893a3da90
7
+ data.tar.gz: 422ab1172b497502b34b1369118ed10669e59b8a830ae9539fc7a7c289ae743bc9ed68919fab0eb666a3a88c679a13dbe43a9c8f6c3514e462e58607f07e629f
data/.rspec-opal ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/CHANGELOG.md CHANGED
@@ -19,10 +19,22 @@
19
19
 
20
20
  ## [0.1.4] - 2025-08-03
21
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
22
+ - Fix Targets
23
+ - Fix Outlets
24
+ - Fix Values
25
+ - Fix `element`
26
+ - Fix Classes
27
+ - Add `document` and `window` private methods
28
+
29
+ ## [0.1.5] - 2025-08-14
30
+
31
+ - Add `StimulusController` unit and system tests
32
+ - Reduce installed new lines for Rails
33
+ - Skip importmap in production
34
+ - Add `bin/rails generate opal_stimulus <CONTROLLER_NAME>`, usage: `bin/rails g opal_stimulus -h`
35
+
36
+ ## [0.1.6] - 2025-08-17
37
+
38
+ - Simplify installation importing `application` and `Controller` with ESM
39
+ Now `app/javascript/controllers/application.js` will be not modified,
40
+ and only compiled code will be imported inside application layout.
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Opal Stimulus** is a Stimulus wrapper made with Opal (a source-to-source Ruby to JavaScript compiler) that allows you to write Stimulus controllers in Ruby instead of JavaScript (It works only with Rails).
4
4
 
5
+ [![Ruby](https://github.com/josephschito/opal_stimulus/actions/workflows/main.yml/badge.svg)](https://github.com/josephschito/opal_stimulus/actions/workflows/main.yml)
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your Gemfile:
@@ -23,6 +25,13 @@ Start application:
23
25
  bin/dev
24
26
  ```
25
27
 
28
+ Create a new controller:
29
+
30
+ ```bash
31
+ bin/rails opal_stimulus hello
32
+ ```
33
+ This command will create `app/opal/controllers/hello_controller.rb`
34
+
26
35
  ## Basic Example
27
36
 
28
37
  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):
@@ -31,8 +40,6 @@ Here's a Hello World example with OpalStimulus. Compare with the [original JavaS
31
40
 
32
41
  ```ruby
33
42
  # app/opal/controllers/hello_controller.rb
34
- # new controllers will be automatically added to app/opal/controllers_requires.rb
35
- # (ordered files load is not supported yet)
36
43
  class HelloController < StimulusController
37
44
  self.targets = ["name", "output"]
38
45
 
@@ -57,10 +64,6 @@ end
57
64
  </div>
58
65
  ```
59
66
 
60
- **Result**
61
-
62
- https://github.com/user-attachments/assets/c51ed28c-13d2-4e06-b882-1cc997e9627b
63
-
64
67
  **Notes:**
65
68
 
66
69
  - In general any reference method is snake cased, so `container` target will produce `container_target` and not ~`containerTarget`~
@@ -178,6 +181,9 @@ class DocumentController < StimulusController
178
181
  end
179
182
  ```
180
183
 
184
+ ## Run tests
185
+ `bundle exec rake`
186
+
181
187
  ## Contributing
182
188
 
183
189
  Bug reports and pull requests are welcome on GitHub at https://github.com/josephschito/opal_stimulus.
data/Rakefile CHANGED
@@ -9,4 +9,15 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[test rubocop]
12
+ require 'opal/rspec/rake_task'
13
+ require "opal"
14
+
15
+ Opal.use_gem("opal_proxy")
16
+ Opal.append_path File.expand_path('../lib', __FILE__)
17
+ Opal.append_path File.expand_path('../shared_fixtures', __FILE__)
18
+
19
+ Opal::RSpec::RakeTask.new("rspec-opal") do |server, task|
20
+ task.runner = :chrome
21
+ end
22
+
23
+ task default: %i[test rspec-opal]
@@ -0,0 +1,14 @@
1
+ Description:
2
+ ============
3
+ Generates a new Opal Stimulus controller at the passed path.
4
+
5
+ Examples:
6
+ =========
7
+ bin/rails generate opal_stimulus chat
8
+
9
+ creates: app/opal/controllers/chat_controller.rb
10
+
11
+
12
+ bin/rails generate stimulus nested/chat
13
+
14
+ creates: app/opal/controllers/nested/chat_controller.rb
@@ -0,0 +1,9 @@
1
+ require "rails/generators/named_base"
2
+
3
+ class OpalStimulusGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def create_controller_file
7
+ template "controller.rb.tt", File.join("app/opal/controllers", class_path, "#{file_name}_controller.rb")
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # Connects to data-controller="<%= file_name.dasherize %>"
2
+ <% constants = class_name.split("::") -%>
3
+ <% modules = constants[0..-2] -%>
4
+ <% indentation = " " * (modules.length) -%>
5
+ <% modules.each do |constant| -%>
6
+ module <%= constant %>
7
+ <% end -%>
8
+ <%= indentation %>class <%= constants.last %>Controller < StimulusController
9
+ <%= indentation %> def connect
10
+ <%= indentation %> end
11
+ <%= indentation %>end
12
+ <% modules.each.with_index do |constant, i| -%>
13
+ <%= " " * i %>end
14
+ <% end -%>
@@ -1,2 +1,2 @@
1
1
  web: bin/rails server
2
- opal: bin/opal --watch
2
+ opal: bin/rails opal_stimulus:watch
@@ -1,9 +1,4 @@
1
1
  require "opal_stimulus/stimulus_controller"
2
- require "controllers_requires"
2
+ require_tree "controllers"
3
3
 
4
- StimulusController.subclasses.each do |controller|
5
- controller.define_method(:dummy) { }
6
-
7
- return if `Stimulus.controllers`.include?(`#{controller.stimulus_name}`)
8
- `Stimulus.register(#{controller.stimulus_name}, #{controller.stimulus_controller})`
9
- end
4
+ StimulusController.register_all!
@@ -7,22 +7,14 @@ if File.exist? APPLICATION_LAYOUT_PATH
7
7
  insert_into_file APPLICATION_LAYOUT_PATH, after: "<%= javascript_importmap_tags %>\n" do
8
8
  <<-ERB
9
9
  <script type="module">
10
- import "application"
11
10
  import "<%= javascript_path("opal") %>"
12
11
  </script>
13
12
  ERB
14
13
  end
15
14
 
16
- insert_into_file APPLICATION_LAYOUT_PATH, after: "<body>\n" do
17
- say "Adding `my-opal` to the application layout", :green
18
- <<-ERB
19
- <span data-controller="my-opal"></span>
20
- ERB
21
- end
22
-
23
15
  say "Creating Opal Stimulus files", :green
24
16
  if Rails.root.join("Procfile.dev").exist?
25
- append_to_file "Procfile.dev", "opal: bin/opal --watch\n"
17
+ append_to_file "Procfile.dev", "opal: bin/rails opal_stimulus:watch\n"
26
18
  else
27
19
  say "Add default Procfile.dev"
28
20
  copy_file "#{__dir__}/Procfile.dev", "Procfile.dev"
@@ -30,27 +22,15 @@ if File.exist? APPLICATION_LAYOUT_PATH
30
22
  say "Ensure foreman is installed"
31
23
  run "gem install foreman"
32
24
  end
33
- insert_into_file Rails.root.join("app/javascript/controllers/application.js") do
34
- <<-JS
35
- import { Controller } from "@hotwired/stimulus";
36
-
37
- window.application = application;
38
- window.Controller = Controller;
39
- JS
40
- end
41
25
  append_to_file ".gitignore", "/.opal-cache\n"
42
26
  append_to_file ".gitignore", "app/assets/builds/opal.js\n"
43
27
  append_to_file ".gitignore", "app/assets/builds/opal.js.map\n"
44
28
  empty_directory APPLICATION_OPAL_STIMULUS_BIN_PATH
45
29
  empty_directory APPLICATION_OPAL_STIMULUS_PATH
46
30
  empty_directory "#{APPLICATION_OPAL_STIMULUS_PATH}/controllers"
47
- empty_directory "#{APPLICATION_OPAL_STIMULUS_PATH}/app/assets/builds"
31
+ create_file "#{APPLICATION_OPAL_STIMULUS_PATH}/controllers/.keep"
48
32
  create_file "app/assets/builds/.keep"
49
- copy_file "#{__dir__}/opal", "#{APPLICATION_OPAL_STIMULUS_BIN_PATH}/opal"
50
- FileUtils.chmod("+x", "#{APPLICATION_OPAL_STIMULUS_BIN_PATH}/opal")
51
33
  copy_file "#{__dir__}/dev", "#{APPLICATION_OPAL_STIMULUS_BIN_PATH}/dev"
52
34
  FileUtils.chmod("+x", "#{APPLICATION_OPAL_STIMULUS_BIN_PATH}/dev")
53
35
  copy_file "#{__dir__}/application.rb", "#{APPLICATION_OPAL_STIMULUS_PATH}/application.rb"
54
- copy_file "#{__dir__}/controllers_requires.rb", "#{APPLICATION_OPAL_STIMULUS_PATH}/controllers_requires.rb"
55
- copy_file "#{__dir__}/controllers/my_opal_controller.rb", "#{APPLICATION_OPAL_STIMULUS_PATH}/controllers/my_opal_controller.rb"
56
36
  end
@@ -5,8 +5,6 @@ require "native"
5
5
  require "js/proxy"
6
6
 
7
7
  class StimulusController < `Controller`
8
- include Native::Wrapper
9
-
10
8
  DEFAULT_METHODS = %i[initialize connect disconnect dispatch]
11
9
  DEFAULT_GETTERS = %i[element]
12
10
 
@@ -44,6 +42,22 @@ class StimulusController < `Controller`
44
42
  }
45
43
  end
46
44
 
45
+ def self.to_ruby_name(name)
46
+ name
47
+ .to_s
48
+ .gsub(/([A-Z]+)/) { "_#{$1.downcase}" }
49
+ .sub(/^_/, '')
50
+ end
51
+
52
+ def self.register_all!
53
+ subclasses.each do |controller|
54
+ controller.define_method(:dummy) {}
55
+
56
+ return if `application.controllers`.include?(`#{controller.stimulus_name}`)
57
+ `application.register(#{controller.stimulus_name}, #{controller.stimulus_controller})`
58
+ end
59
+ end
60
+
47
61
  def self.targets=(targets = [])
48
62
  `#{self.stimulus_controller}.targets = targets`
49
63
 
@@ -150,11 +164,11 @@ class StimulusController < `Controller`
150
164
  `#{self.stimulus_controller}.values = #{js_values.to_n}`
151
165
 
152
166
  define_method(name + "_value") do
153
- `return this[#{name + "Value"}]`
167
+ Native(`this[#{name + "Value"}]`)
154
168
  end
155
169
 
156
170
  define_method(name + "_value=") do |value|
157
- `this[#{name + "Value"}]= #{value.to_n}`
171
+ Native(`this[#{name + "Value"}]= #{value}`)
158
172
  end
159
173
 
160
174
  define_method("has_#{name}") do
@@ -165,6 +179,10 @@ class StimulusController < `Controller`
165
179
  camel_case_changed = "#{name}ValueChanged"
166
180
  %x{
167
181
  #{self.stimulus_controller}.prototype[#{camel_case_changed}] = function(value, previousValue) {
182
+ if (#{type == :object}) {
183
+ value = JSON.stringify(value)
184
+ previousValue = JSON.stringify(previousValue)
185
+ }
168
186
  if (this['$respond_to?'] && this['$respond_to?'](#{snake_case_changed})) {
169
187
  return this['$' + #{snake_case_changed}](value, previousValue);
170
188
  }
@@ -198,15 +216,6 @@ class StimulusController < `Controller`
198
216
  JS::Proxy.new(`this.element`)
199
217
  end
200
218
 
201
- private
202
-
203
- def self.to_ruby_name(name)
204
- name
205
- .to_s
206
- .gsub(/([A-Z]+)/) { "_#{$1.downcase}" }
207
- .sub(/^_/, '')
208
- end
209
-
210
219
  def window
211
220
  @window ||= JS::Proxy.new($$.window)
212
221
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpalStimulus
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -0,0 +1,58 @@
1
+ require "bundler/setup"
2
+ require "listen"
3
+ require "opal"
4
+
5
+ namespace :opal_stimulus do
6
+ def prepare_paths
7
+ [ "opal_proxy", "opal_stimulus" ].each do |gem_name|
8
+ Opal.use_gem(gem_name) rescue Opal.append_path(File.expand_path("lib", Bundler.rubygems.find_name(gem_name).first.full_gem_path))
9
+ end
10
+
11
+ Opal.append_path(Rails.root.join("app/opal"))
12
+ end
13
+
14
+ def compile
15
+ puts "🔨 Compiling Opal..."
16
+
17
+ Opal::Config.esm = true
18
+ builder = Opal::Builder.new
19
+ result = builder.build("application")
20
+ output_path = Rails.root.join("app/assets/builds/opal.js")
21
+ code = [
22
+ "import { application } from 'controllers/application';",
23
+ "import { Controller } from '@hotwired/stimulus';",
24
+ result.to_s
25
+ ].join("\n")
26
+
27
+ if Rails.env.development?
28
+ code += "//# sourceMappingURL=/assets/opal.js.map"
29
+ sourcemap_path = "#{output_path}.map"
30
+ source_map_json = result.source_map.to_json
31
+ File.write(sourcemap_path, source_map_json)
32
+ end
33
+
34
+ File.write(output_path, code)
35
+
36
+ puts "✅ Compiled to #{output_path}"
37
+ end
38
+
39
+ desc "Build Opal Stimulus controllers"
40
+ task build: [:environment] do
41
+ prepare_paths
42
+ compile
43
+ end
44
+
45
+ desc "Watch and build Opal Stimulus controllers"
46
+ task watch: [:environment] do
47
+ prepare_paths
48
+
49
+ compile
50
+
51
+ listen = Listen.to(Rails.root.join("app/opal")) { compile }
52
+
53
+ puts "👀 Watching app/opal for changes..."
54
+ listen.start
55
+ Signal.trap("INT") { listen.stop }
56
+ sleep
57
+ end
58
+ end
@@ -1,6 +1,6 @@
1
1
  namespace :opal_stimulus do
2
2
  desc "Install Opal Stimulus into the app"
3
- task :install do
3
+ task install: [:environment] do
4
4
  system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/install_opal_stimulus.rb", __dir__)}"
5
5
  end
6
6
  end