stimulus_reflex 3.3.0.pre4 → 3.4.0.pre1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of stimulus_reflex might be problematic. Click here for more details.

Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -4
  3. data/Gemfile.lock +72 -69
  4. data/README.md +5 -2
  5. data/{lib → app/channels}/stimulus_reflex/channel.rb +17 -7
  6. data/lib/generators/stimulus_reflex/config_generator.rb +14 -0
  7. data/lib/generators/{stimulus_reflex_generator.rb → stimulus_reflex/stimulus_reflex_generator.rb} +0 -0
  8. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/%file_name%_controller.js.tt +89 -0
  9. data/lib/generators/{templates → stimulus_reflex/templates}/app/javascript/controllers/application_controller.js.tt +11 -8
  10. data/lib/generators/{templates → stimulus_reflex/templates}/app/reflexes/%file_name%_reflex.rb.tt +0 -0
  11. data/lib/generators/{templates → stimulus_reflex/templates}/app/reflexes/application_reflex.rb.tt +0 -0
  12. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +9 -0
  13. data/lib/stimulus_reflex.rb +5 -2
  14. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +3 -7
  15. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +2 -2
  16. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +2 -2
  17. data/lib/stimulus_reflex/configuration.rb +24 -0
  18. data/lib/stimulus_reflex/element.rb +8 -0
  19. data/lib/stimulus_reflex/reflex.rb +21 -18
  20. data/lib/stimulus_reflex/sanity_checker.rb +98 -0
  21. data/lib/stimulus_reflex/version.rb +1 -1
  22. data/lib/tasks/stimulus_reflex/install.rake +23 -8
  23. data/package.json +63 -0
  24. data/stimulus_reflex.gemspec +40 -0
  25. data/tags +98 -0
  26. data/test/generators/stimulus_reflex_generator_test.rb +3 -2
  27. data/test/tmp/app/reflexes/application_reflex.rb +12 -0
  28. data/test/tmp/app/reflexes/user_reflex.rb +33 -0
  29. data/yarn.lock +6261 -0
  30. metadata +27 -15
  31. data/lib/generators/templates/app/javascript/controllers/%file_name%_controller.js.tt +0 -57
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module StimulusReflex
6
+ class ConfigGenerator < Rails::Generators::Base
7
+ desc "Creates an StimulusReflex configuration file in config/initializers"
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_config_file
11
+ copy_file "config/initializers/stimulus_reflex.rb"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,89 @@
1
+ import ApplicationController from './application_controller'
2
+
3
+ /* This is the custom StimulusReflex controller for the <%= class_name %> Reflex.
4
+ * Learn more at: https://docs.stimulusreflex.com
5
+ */
6
+ export default class extends ApplicationController {
7
+ /*
8
+ * Regular Stimulus lifecycle methods
9
+ * Learn more at: https://stimulusjs.org/reference/lifecycle-callbacks
10
+ *
11
+ * If you intend to use this controller as a regular stimulus controller as well,
12
+ * make sure any Stimulus lifecycle methods overridden in ApplicationController call super.
13
+ *
14
+ * Important:
15
+ * By default, StimulusReflex overrides the -connect- method so make sure you
16
+ * call super if you intend to do anything else when this controller connects.
17
+ */
18
+
19
+ connect () {
20
+ super.connect()
21
+ // add your code here, if applicable
22
+ }
23
+
24
+ /* Reflex specific lifecycle methods.
25
+ *
26
+ * For every method defined in your Reflex class, a matching set of lifecycle methods become available
27
+ * in this javascript controller. These are optional, so feel free to delete these stubs if you don't
28
+ * need them.
29
+ *
30
+ * Important:
31
+ * Make sure to add data-controller="<%= class_name.underscore.dasherize %>" to your markup alongside
32
+ * data-reflex="<%= class_name %>#dance" for the lifecycle methods to fire properly.
33
+ *
34
+ * Example:
35
+ *
36
+ * <a href="#" data-reflex="click-><%= class_name %>#dance" data-controller="<%= class_name.underscore.dasherize %>">Dance!</a>
37
+ *
38
+ * Arguments:
39
+ *
40
+ * element - the element that triggered the reflex
41
+ * may be different than the Stimulus controller's this.element
42
+ *
43
+ * reflex - the name of the reflex e.g. "<%= class_name %>#dance"
44
+ *
45
+ * error/noop - the error message (for reflexError), otherwise null
46
+ *
47
+ * reflexId - a UUID4 or developer-provided unique identifier for each Reflex
48
+ */
49
+
50
+ <% if actions.empty? -%>
51
+ // Assuming you create a "<%= class_name %>#dance" action in your Reflex class
52
+ // you'll be able to use the following lifecycle methods:
53
+
54
+ // beforeDance(element, reflex, noop, reflexId) {
55
+ // element.innerText = 'Putting dance shoes on...'
56
+ // }
57
+
58
+ // danceSuccess(element, reflex, noop, reflexId) {
59
+ // element.innerText = 'Danced like no one was watching! Was someone watching?'
60
+ // }
61
+
62
+ // danceError(element, reflex, error, reflexId) {
63
+ // console.error('danceError', error);
64
+ // element.innerText = "Couldn't dance!"
65
+ // }
66
+ <% end -%>
67
+ <% actions.each do |action| -%>
68
+ // <%= "before_#{action}".camelize(:lower) %>(element, reflex, noop, reflexId) {
69
+ // console.log("before <%= action %>", element, reflex, reflexId)
70
+ // }
71
+
72
+ // <%= "#{action}_success".camelize(:lower) %>(element, reflex, noop, reflexId) {
73
+ // console.log("<%= action %> success", element, reflex, reflexId)
74
+ // }
75
+
76
+ // <%= "#{action}_error".camelize(:lower) %>(element, reflex, error, reflexId) {
77
+ // console.error("<%= action %> error", element, reflex, error, reflexId)
78
+ // }
79
+
80
+ // <%= "#{action}_halted".camelize(:lower) %>(element, reflex, noop, reflexId) {
81
+ // console.warn("<%= action %> halted", element, reflex, reflexId)
82
+ // }
83
+
84
+ // <%= "after_#{action}".camelize(:lower) %>(element, reflex, noop, reflexId) {
85
+ // console.log("after <%= action %>", element, reflex, reflexId)
86
+ // }
87
+ <%= "\n" unless action == actions.last -%>
88
+ <% end -%>
89
+ }
@@ -1,7 +1,7 @@
1
1
  import { Controller } from 'stimulus'
2
2
  import StimulusReflex from 'stimulus_reflex'
3
3
 
4
- /* This is your application's ApplicationController.
4
+ /* This is your ApplicationController.
5
5
  * All StimulusReflex controllers should inherit from this class.
6
6
  *
7
7
  * Example:
@@ -17,7 +17,8 @@ export default class extends Controller {
17
17
  StimulusReflex.register(this)
18
18
  }
19
19
 
20
- /* Application wide lifecycle methods.
20
+ /* Application-wide lifecycle methods
21
+ *
21
22
  * Use these methods to handle lifecycle concerns for the entire application.
22
23
  * Using the lifecycle is optional, so feel free to delete these stubs if you don't need them.
23
24
  *
@@ -26,24 +27,26 @@ export default class extends Controller {
26
27
  * element - the element that triggered the reflex
27
28
  * may be different than the Stimulus controller's this.element
28
29
  *
29
- * reflex - the name of the reflex e.g. "ExampleReflex#demo"
30
+ * reflex - the name of the reflex e.g. "Example#demo"
31
+ *
32
+ * error/noop - the error message (for reflexError), otherwise null
30
33
  *
31
- * error - error message from the server
34
+ * reflexId - a UUID4 or developer-provided unique identifier for each Reflex
32
35
  */
33
36
 
34
- beforeReflex (element, reflex) {
37
+ beforeReflex (element, reflex, noop, reflexId) {
35
38
  // document.body.classList.add('wait')
36
39
  }
37
40
 
38
- reflexSuccess (element, reflex, error) {
41
+ reflexSuccess (element, reflex, noop, reflexId) {
39
42
  // show success message etc...
40
43
  }
41
44
 
42
- reflexError (element, reflex, error) {
45
+ reflexError (element, reflex, error, reflexId) {
43
46
  // show error message etc...
44
47
  }
45
48
 
46
- afterReflex (element, reflex) {
49
+ afterReflex (element, reflex, noop, reflexId) {
47
50
  // document.body.classList.remove('wait')
48
51
  }
49
52
  }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ StimulusReflex.configure do |config|
4
+ # Enable/disable whether startup should be aborted when the sanity checks fail
5
+ # config.exit_on_failed_sanity_checks = true
6
+
7
+ # Override the parent class that the StimulusReflex ActionCable channel inherits from
8
+ # config.parent_channel = "ApplicationCable::Channel"
9
+ end
@@ -9,16 +9,19 @@ require "action_cable"
9
9
  require "nokogiri"
10
10
  require "cable_ready"
11
11
  require "stimulus_reflex/version"
12
+ require "stimulus_reflex/configuration"
12
13
  require "stimulus_reflex/reflex"
13
14
  require "stimulus_reflex/element"
14
- require "stimulus_reflex/channel"
15
+ require "stimulus_reflex/sanity_checker"
15
16
  require "stimulus_reflex/broadcasters/broadcaster"
16
17
  require "stimulus_reflex/broadcasters/nothing_broadcaster"
17
18
  require "stimulus_reflex/broadcasters/page_broadcaster"
18
19
  require "stimulus_reflex/broadcasters/selector_broadcaster"
19
- require "generators/stimulus_reflex_generator"
20
20
 
21
21
  module StimulusReflex
22
22
  class Engine < Rails::Engine
23
+ initializer "stimulus_reflex.sanity_check" do
24
+ SanityChecker.check!
25
+ end
23
26
  end
24
27
  end
@@ -24,22 +24,18 @@ module StimulusReflex
24
24
  false
25
25
  end
26
26
 
27
- def enqueue_message(subject:, body: nil, data: {})
27
+ def broadcast_message(subject:, body: nil, data: {}, error: nil)
28
28
  logger.error "\e[31m#{body}\e[0m" if subject == "error"
29
29
  cable_ready[stream_name].dispatch_event(
30
30
  name: "stimulus-reflex:server-message",
31
31
  detail: {
32
32
  reflexId: data["reflexId"],
33
33
  stimulus_reflex: data.merge(
34
- broadcaster: to_sym,
35
- server_message: {subject: subject, body: body}
34
+ morph: to_sym,
35
+ server_message: {subject: subject, body: error&.to_s}
36
36
  )
37
37
  }
38
38
  )
39
- end
40
-
41
- def broadcast_message(subject:, body: nil, data: {})
42
- enqueue_message subject: subject, body: body, data: data
43
39
  cable_ready.broadcast
44
40
  end
45
41
 
@@ -3,7 +3,7 @@
3
3
  module StimulusReflex
4
4
  class PageBroadcaster < Broadcaster
5
5
  def broadcast(selectors, data)
6
- reflex.controller.process reflex.url_params[:action]
6
+ reflex.controller.process reflex.params[:action]
7
7
  page_html = reflex.controller.response.body
8
8
 
9
9
  return unless page_html.present?
@@ -18,7 +18,7 @@ module StimulusReflex
18
18
  children_only: true,
19
19
  permanent_attribute_name: permanent_attribute_name,
20
20
  stimulus_reflex: data.merge({
21
- broadast_type: to_sym
21
+ morph: to_sym
22
22
  })
23
23
  )
24
24
  end
@@ -17,7 +17,7 @@ module StimulusReflex
17
17
  children_only: true,
18
18
  permanent_attribute_name: permanent_attribute_name,
19
19
  stimulus_reflex: data.merge({
20
- broadast_type: to_sym
20
+ morph: to_sym
21
21
  })
22
22
  )
23
23
  else
@@ -25,7 +25,7 @@ module StimulusReflex
25
25
  selector: selector,
26
26
  html: fragment.to_html,
27
27
  stimulus_reflex: data.merge({
28
- broadast_type: to_sym
28
+ morph: to_sym
29
29
  })
30
30
  )
31
31
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex
4
+ class << self
5
+ def configure
6
+ yield configuration
7
+ end
8
+
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ alias_method :config, :configuration
14
+ end
15
+
16
+ class Configuration
17
+ attr_accessor :exit_on_failed_sanity_checks, :parent_channel
18
+
19
+ def initialize
20
+ @exit_on_failed_sanity_checks = true
21
+ @parent_channel = "ApplicationCable::Channel"
22
+ end
23
+ end
24
+ end
@@ -11,6 +11,14 @@ class StimulusReflex::Element < OpenStruct
11
11
  @data_attributes.transform_keys! { |key| key.delete_prefix "data-" }
12
12
  end
13
13
 
14
+ def signed
15
+ @signed ||= ->(accessor) { GlobalID::Locator.locate_signed(dataset[accessor]) }
16
+ end
17
+
18
+ def unsigned
19
+ @unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
20
+ end
21
+
14
22
  def dataset
15
23
  @dataset ||= OpenStruct.new(data_attributes.merge(data_attributes.transform_keys(&:underscore)))
16
24
  end
@@ -45,8 +45,10 @@ class StimulusReflex::Reflex
45
45
 
46
46
  attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :permanent_attribute_name
47
47
 
48
+ alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
49
+
48
50
  delegate :connection, :stream_name, to: :channel
49
- delegate :session, to: :request
51
+ delegate :flash, :session, to: :request
50
52
  delegate :broadcast, :broadcast_message, to: :broadcaster
51
53
 
52
54
  def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, permanent_attribute_name: nil, params: {})
@@ -66,21 +68,26 @@ class StimulusReflex::Reflex
66
68
  uri = URI.parse(url)
67
69
  path = ActionDispatch::Journey::Router::Utils.normalize_path(uri.path)
68
70
  query_hash = Rack::Utils.parse_nested_query(uri.query)
69
- req = ActionDispatch::Request.new(
70
- connection.env.merge(
71
- Rack::MockRequest.env_for(uri.to_s).merge(
72
- "rack.request.query_hash" => query_hash,
73
- "rack.request.query_string" => uri.query,
74
- "ORIGINAL_SCRIPT_NAME" => "",
75
- "ORIGINAL_FULLPATH" => path,
76
- Rack::SCRIPT_NAME => "",
77
- Rack::PATH_INFO => path,
78
- Rack::REQUEST_PATH => path,
79
- Rack::QUERY_STRING => uri.query
80
- )
81
- )
71
+ mock_env = Rack::MockRequest.env_for(uri.to_s)
72
+
73
+ mock_env.merge!(
74
+ "rack.request.query_hash" => query_hash,
75
+ "rack.request.query_string" => uri.query,
76
+ "ORIGINAL_SCRIPT_NAME" => "",
77
+ "ORIGINAL_FULLPATH" => path,
78
+ Rack::SCRIPT_NAME => "",
79
+ Rack::PATH_INFO => path,
80
+ Rack::REQUEST_PATH => path,
81
+ Rack::QUERY_STRING => uri.query
82
82
  )
83
+
84
+ env = connection.env.merge(mock_env)
85
+ req = ActionDispatch::Request.new(env)
86
+
83
87
  path_params = Rails.application.routes.recognize_path_with_request(req, url, req.env[:extras] || {})
88
+ path_params[:controller] = path_params[:controller].force_encoding("UTF-8")
89
+ path_params[:action] = path_params[:action].force_encoding("UTF-8")
90
+
84
91
  req.env.merge(ActionDispatch::Http::Parameters::PARAMETERS_KEY => path_params)
85
92
  req.env["action_dispatch.request.parameters"] = req.parameters.merge(@params)
86
93
  req.tap { |r| r.session.send :load! }
@@ -112,10 +119,6 @@ class StimulusReflex::Reflex
112
119
  end
113
120
  end
114
121
 
115
- def url_params
116
- @url_params ||= Rails.application.routes.recognize_path_with_request(request, request.path, request.env[:extras] || {})
117
- end
118
-
119
122
  def process(name, *args)
120
123
  reflex_invoked = false
121
124
  result = run_callbacks(:process) {
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StimulusReflex::SanityChecker
4
+ JSON_VERSION_FORMAT = /(\d+\.\d+\.\d+.*)"/
5
+
6
+ def self.check!
7
+ instance = new
8
+ instance.check_caching_enabled
9
+ instance.check_javascript_package_version
10
+ end
11
+
12
+ def check_caching_enabled
13
+ unless caching_enabled?
14
+ warn_and_exit <<~WARN
15
+ Stimulus Reflex requires caching to be enabled. Caching allows the session to be modified during ActionCable requests.
16
+ To enable caching in development, run:
17
+ rails dev:cache
18
+ WARN
19
+ end
20
+
21
+ unless not_null_store?
22
+ warn_and_exit <<~WARN
23
+ Stimulus Reflex requires caching to be enabled. Caching allows the session to be modified during ActionCable requests.
24
+ But your config.cache_store is set to :null_store, so it won't work.
25
+ WARN
26
+ end
27
+ end
28
+
29
+ def check_javascript_package_version
30
+ if javascript_package_version.nil?
31
+ warn_and_exit <<~WARN
32
+ Can't locate the stimulus_reflex NPM package.
33
+ Either add it to your package.json as a dependency or use "yarn link stimulus_reflex" if you are doing development.
34
+ WARN
35
+ end
36
+
37
+ unless javascript_version_matches?
38
+ warn_and_exit <<~WARN
39
+ The Stimulus Reflex javascript package version (#{javascript_package_version}) does not match the Rubygem version (#{gem_version}).
40
+ To update the Stimulus Reflex npm package:
41
+ yarn upgrade stimulus_reflex@#{gem_version}
42
+ WARN
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def caching_enabled?
49
+ Rails.application.config.action_controller.perform_caching
50
+ end
51
+
52
+ def not_null_store?
53
+ Rails.application.config.cache_store != :null_store
54
+ end
55
+
56
+ def javascript_version_matches?
57
+ javascript_package_version == gem_version
58
+ end
59
+
60
+ def gem_version
61
+ @_gem_version ||= StimulusReflex::VERSION.gsub(".pre", "-pre")
62
+ end
63
+
64
+ def javascript_package_version
65
+ @_js_version ||= find_javascript_package_version
66
+ end
67
+
68
+ def find_javascript_package_version
69
+ if (match = search_file(package_json_path, regex: /version/))
70
+ match[JSON_VERSION_FORMAT, 1]
71
+ end
72
+ end
73
+
74
+ def search_file(path, regex:)
75
+ return unless File.exist?(path)
76
+ File.foreach(path).grep(regex).first
77
+ end
78
+
79
+ def package_json_path
80
+ Rails.root.join("node_modules", "stimulus_reflex", "package.json")
81
+ end
82
+
83
+ def warn_and_exit(text)
84
+ puts "WARNING:"
85
+ puts text
86
+ exit_with_info if StimulusReflex.config.exit_on_failed_sanity_checks
87
+ end
88
+
89
+ def exit_with_info
90
+ puts
91
+ puts <<~INFO
92
+ If you know what you are doing and you want to start the application anyway,
93
+ you can add the following directive to an initializer:
94
+ StimulusReflex.config.exit_on_failed_sanity_checks = false
95
+ INFO
96
+ exit
97
+ end
98
+ end