stimulus_reflex 3.3.0 → 3.4.0.pre4

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +113 -8
  3. data/Gemfile.lock +70 -69
  4. data/README.md +6 -3
  5. data/{lib → app/channels}/stimulus_reflex/channel.rb +36 -21
  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/{templates → stimulus_reflex/templates}/app/javascript/controllers/%file_name%_controller.js.tt +0 -0
  9. data/lib/generators/{templates → stimulus_reflex/templates}/app/javascript/controllers/application_controller.js.tt +0 -0
  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 +20 -0
  13. data/lib/stimulus_reflex.rb +3 -2
  14. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +12 -9
  15. data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +4 -0
  16. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +8 -3
  17. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +12 -2
  18. data/lib/stimulus_reflex/configuration.rb +27 -0
  19. data/lib/stimulus_reflex/element.rb +8 -0
  20. data/lib/stimulus_reflex/logger.rb +106 -0
  21. data/lib/stimulus_reflex/reflex.rb +29 -22
  22. data/lib/stimulus_reflex/sanity_checker.rb +82 -22
  23. data/lib/stimulus_reflex/utils/colorize.rb +23 -0
  24. data/lib/stimulus_reflex/version.rb +1 -1
  25. data/lib/tasks/stimulus_reflex/install.rake +12 -8
  26. data/package.json +57 -0
  27. data/stimulus_reflex.gemspec +40 -0
  28. data/tags +98 -0
  29. data/test/broadcasters/broadcaster_test.rb +15 -0
  30. data/test/broadcasters/nothing_broadcaster_test.rb +34 -0
  31. data/test/broadcasters/page_broadcaster_test.rb +69 -0
  32. data/test/broadcasters/selector_broadcaster_test.rb +83 -0
  33. data/test/generators/stimulus_reflex_generator_test.rb +1 -0
  34. data/test/test_helper.rb +2 -0
  35. data/test/tmp/app/reflexes/application_reflex.rb +12 -0
  36. data/test/tmp/app/reflexes/posts_reflex.rb +24 -0
  37. data/yarn.lock +4685 -0
  38. metadata +42 -21
@@ -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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ StimulusReflex.configure do |config|
4
+ # Enable/disable exiting / warning when the sanity checks fail options:
5
+ # `:exit` or `:warn` or `:ignore`
6
+
7
+ # config.on_failed_sanity_checks = :exit
8
+
9
+ # Override the parent class that the StimulusReflex ActionCable channel inherits from
10
+
11
+ # config.parent_channel = "ApplicationCable::Channel"
12
+
13
+ # Customize server-side Reflex logging format, with optional colorization:
14
+ # Available tokens: session_id, session_id_full, reflex_info, operation, reflex_id, reflex_id_full, mode, selector, operation_counter, connection_id, connection_id_full, timestamp
15
+ # Available colors: green, yellow, blue, magenta, cyan, white
16
+ # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
17
+ # eg. if your connection is `identified_by :current_user` and your User model has an email attribute, you can access r.email (it will display `-` if the user isn't logged in)
18
+
19
+ # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
20
+ end
@@ -9,15 +9,16 @@ 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
15
  require "stimulus_reflex/sanity_checker"
16
16
  require "stimulus_reflex/broadcasters/broadcaster"
17
17
  require "stimulus_reflex/broadcasters/nothing_broadcaster"
18
18
  require "stimulus_reflex/broadcasters/page_broadcaster"
19
19
  require "stimulus_reflex/broadcasters/selector_broadcaster"
20
- require "generators/stimulus_reflex_generator"
20
+ require "stimulus_reflex/utils/colorize"
21
+ require "stimulus_reflex/logger"
21
22
 
22
23
  module StimulusReflex
23
24
  class Engine < Rails::Engine
@@ -4,12 +4,13 @@ module StimulusReflex
4
4
  class Broadcaster
5
5
  include CableReady::Broadcaster
6
6
 
7
- attr_reader :reflex, :logger
7
+ attr_reader :reflex, :logger, :operations
8
8
  delegate :permanent_attribute_name, :stream_name, to: :reflex
9
9
 
10
10
  def initialize(reflex)
11
11
  @reflex = reflex
12
- @logger = Rails.logger
12
+ @logger = Rails.logger if defined?(Rails.logger)
13
+ @operations = []
13
14
  end
14
15
 
15
16
  def nothing?
@@ -24,22 +25,19 @@ module StimulusReflex
24
25
  false
25
26
  end
26
27
 
27
- def enqueue_message(subject:, body: nil, data: {})
28
+ def broadcast_message(subject:, body: nil, data: {}, error: nil)
28
29
  logger.error "\e[31m#{body}\e[0m" if subject == "error"
30
+ @operations << ["document", :dispatch_event]
29
31
  cable_ready[stream_name].dispatch_event(
30
32
  name: "stimulus-reflex:server-message",
31
33
  detail: {
32
34
  reflexId: data["reflexId"],
33
35
  stimulus_reflex: data.merge(
34
- broadcaster: to_sym,
35
- server_message: {subject: subject, body: body}
36
+ morph: to_sym,
37
+ server_message: {subject: subject, body: error&.to_s}
36
38
  )
37
39
  }
38
40
  )
39
- end
40
-
41
- def broadcast_message(subject:, body: nil, data: {})
42
- enqueue_message subject: subject, body: body, data: data
43
41
  cable_ready.broadcast
44
42
  end
45
43
 
@@ -52,5 +50,10 @@ module StimulusReflex
52
50
  def to_sym
53
51
  raise NotImplementedError
54
52
  end
53
+
54
+ # abstract method to be implemented by subclasses
55
+ def to_s
56
+ raise NotImplementedError
57
+ end
55
58
  end
56
59
  end
@@ -13,5 +13,9 @@ module StimulusReflex
13
13
  def to_sym
14
14
  :nothing
15
15
  end
16
+
17
+ def to_s
18
+ "Nothing"
19
+ end
16
20
  end
17
21
  end
@@ -3,14 +3,15 @@
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?
10
10
 
11
- document = Nokogiri::HTML(page_html)
11
+ document = Nokogiri::HTML.parse(page_html)
12
12
  selectors = selectors.select { |s| document.css(s).present? }
13
13
  selectors.each do |selector|
14
+ @operations << [selector, :morph]
14
15
  html = document.css(selector).inner_html
15
16
  cable_ready[stream_name].morph(
16
17
  selector: selector,
@@ -18,7 +19,7 @@ module StimulusReflex
18
19
  children_only: true,
19
20
  permanent_attribute_name: permanent_attribute_name,
20
21
  stimulus_reflex: data.merge({
21
- broadcaster: to_sym
22
+ morph: to_sym
22
23
  })
23
24
  )
24
25
  end
@@ -32,5 +33,9 @@ module StimulusReflex
32
33
  def page?
33
34
  true
34
35
  end
36
+
37
+ def to_s
38
+ "Page"
39
+ end
35
40
  end
36
41
  end
@@ -11,21 +11,23 @@ module StimulusReflex
11
11
  fragment = Nokogiri::HTML.fragment(html)
12
12
  match = fragment.at_css(selector)
13
13
  if match.present?
14
+ @operations << [selector, :morph]
14
15
  cable_ready[stream_name].morph(
15
16
  selector: selector,
16
17
  html: match.inner_html,
17
18
  children_only: true,
18
19
  permanent_attribute_name: permanent_attribute_name,
19
20
  stimulus_reflex: data.merge({
20
- broadcaster: to_sym
21
+ morph: to_sym
21
22
  })
22
23
  )
23
24
  else
25
+ @operations << [selector, :inner_html]
24
26
  cable_ready[stream_name].inner_html(
25
27
  selector: selector,
26
28
  html: fragment.to_html,
27
29
  stimulus_reflex: data.merge({
28
- broadcaster: to_sym
30
+ morph: to_sym
29
31
  })
30
32
  )
31
33
  end
@@ -40,6 +42,10 @@ module StimulusReflex
40
42
  @morphs ||= []
41
43
  end
42
44
 
45
+ def append_morph(selectors, html)
46
+ morphs << [selectors, html]
47
+ end
48
+
43
49
  def to_sym
44
50
  :selector
45
51
  end
@@ -47,5 +53,9 @@ module StimulusReflex
47
53
  def selector?
48
54
  true
49
55
  end
56
+
57
+ def to_s
58
+ "Selector"
59
+ end
50
60
  end
51
61
  end
@@ -0,0 +1,27 @@
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 :on_failed_sanity_checks, :parent_channel, :logging
18
+
19
+ DEFAULT_LOGGING = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
20
+
21
+ def initialize
22
+ @on_failed_sanity_checks = :exit
23
+ @parent_channel = "ApplicationCable::Channel"
24
+ @logging = DEFAULT_LOGGING
25
+ end
26
+ end
27
+ 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
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex
4
+ class Logger
5
+ attr_accessor :reflex, :current_operation
6
+
7
+ def initialize(reflex)
8
+ @reflex = reflex
9
+ @current_operation = 1
10
+ end
11
+
12
+ def print
13
+ return unless config_logging.instance_of?(Proc)
14
+
15
+ puts
16
+ reflex.broadcaster.operations.each do
17
+ puts instance_eval(&config_logging) + "\e[0m"
18
+ @current_operation += 1
19
+ end
20
+ puts
21
+ end
22
+
23
+ private
24
+
25
+ def config_logging
26
+ return @config_logging if @config_logging
27
+
28
+ StimulusReflex.config.logging.binding.eval("using StimulusReflex::Utils::Colorize")
29
+ @config_logging = StimulusReflex.config.logging
30
+ end
31
+
32
+ def session_id_full
33
+ session = reflex.request&.session
34
+ session.nil? ? "-" : session.id
35
+ end
36
+
37
+ def session_id
38
+ session_id_full.to_s[0..7]
39
+ end
40
+
41
+ def reflex_info
42
+ reflex.class.to_s + "#" + reflex.method_name
43
+ end
44
+
45
+ def reflex_id_full
46
+ reflex.reflex_id
47
+ end
48
+
49
+ def reflex_id
50
+ reflex_id_full[0..7]
51
+ end
52
+
53
+ def mode
54
+ reflex.broadcaster.to_s
55
+ end
56
+
57
+ def selector
58
+ reflex.broadcaster.operations[current_operation - 1][0]
59
+ end
60
+
61
+ def operation
62
+ reflex.broadcaster.operations[current_operation - 1][1].to_s
63
+ end
64
+
65
+ def operation_counter
66
+ current_operation.to_s + "/" + reflex.broadcaster.operations.size.to_s
67
+ end
68
+
69
+ def connection_id_full
70
+ identifier = reflex.connection&.connection_identifier
71
+ identifier.empty? ? "-" : identifier
72
+ end
73
+
74
+ def connection_id
75
+ connection_id_full[0..7]
76
+ end
77
+
78
+ def timestamp
79
+ Time.now.strftime("%Y-%m-%d %H:%M:%S")
80
+ end
81
+
82
+ def method_missing method
83
+ return send(method.to_sym) if private_instance_methods.include?(method.to_sym)
84
+
85
+ reflex.connection.identifiers.each do |identifier|
86
+ ident = reflex.connection.send(identifier)
87
+ return ident.send(method) if ident.respond_to?(:attributes) && ident.attributes.key?(method.to_s)
88
+ end
89
+ "-"
90
+ end
91
+
92
+ def respond_to_missing? method
93
+ return true if private_instance_methods.include?(method.to_sym)
94
+
95
+ reflex.connection.identifiers.each do |identifier|
96
+ ident = reflex.connection.send(identifier)
97
+ return true if ident.respond_to?(:attributes) && ident.attributes.key?(method.to_s)
98
+ end
99
+ false
100
+ end
101
+
102
+ def private_instance_methods
103
+ StimulusReflex::Logger.private_instance_methods(false)
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ClientAttributes = Struct.new(:reflex_id, :reflex_controller, :xpath, :c_xpath, :permanent_attribute_name, keyword_init: true)
4
+
3
5
  class StimulusReflex::Reflex
4
6
  include ActiveSupport::Rescuable
5
7
  include ActiveSupport::Callbacks
@@ -43,21 +45,25 @@ class StimulusReflex::Reflex
43
45
  end
44
46
  end
45
47
 
46
- attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :permanent_attribute_name
48
+ attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
49
+
50
+ alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
47
51
 
48
52
  delegate :connection, :stream_name, to: :channel
49
- delegate :session, to: :request
53
+ delegate :flash, :session, to: :request
50
54
  delegate :broadcast, :broadcast_message, to: :broadcaster
55
+ delegate :reflex_id, :reflex_controller, :xpath, :c_xpath, :permanent_attribute_name, to: :client_attributes
51
56
 
52
- def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, permanent_attribute_name: nil, params: {})
57
+ def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
53
58
  @channel = channel
54
59
  @url = url
55
60
  @element = element
56
61
  @selectors = selectors
57
62
  @method_name = method_name
58
63
  @params = params
59
- @permanent_attribute_name = permanent_attribute_name
60
64
  @broadcaster = StimulusReflex::PageBroadcaster.new(self)
65
+ @logger = StimulusReflex::Logger.new(self)
66
+ @client_attributes = ClientAttributes.new(client_attributes)
61
67
  self.params
62
68
  end
63
69
 
@@ -66,21 +72,26 @@ class StimulusReflex::Reflex
66
72
  uri = URI.parse(url)
67
73
  path = ActionDispatch::Journey::Router::Utils.normalize_path(uri.path)
68
74
  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
- )
75
+ mock_env = Rack::MockRequest.env_for(uri.to_s)
76
+
77
+ mock_env.merge!(
78
+ "rack.request.query_hash" => query_hash,
79
+ "rack.request.query_string" => uri.query,
80
+ "ORIGINAL_SCRIPT_NAME" => "",
81
+ "ORIGINAL_FULLPATH" => path,
82
+ Rack::SCRIPT_NAME => "",
83
+ Rack::PATH_INFO => path,
84
+ Rack::REQUEST_PATH => path,
85
+ Rack::QUERY_STRING => uri.query
82
86
  )
87
+
88
+ env = connection.env.merge(mock_env)
89
+ req = ActionDispatch::Request.new(env)
90
+
83
91
  path_params = Rails.application.routes.recognize_path_with_request(req, url, req.env[:extras] || {})
92
+ path_params[:controller] = path_params[:controller].force_encoding("UTF-8")
93
+ path_params[:action] = path_params[:action].force_encoding("UTF-8")
94
+
84
95
  req.env.merge(ActionDispatch::Http::Parameters::PARAMETERS_KEY => path_params)
85
96
  req.env["action_dispatch.request.parameters"] = req.parameters.merge(@params)
86
97
  req.tap { |r| r.session.send :load! }
@@ -97,7 +108,7 @@ class StimulusReflex::Reflex
97
108
  else
98
109
  raise StandardError.new("Cannot call :selector morph after :nothing morph") if broadcaster.nothing?
99
110
  @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
100
- broadcaster.morphs << [selectors, html]
111
+ broadcaster.append_morph(selectors, html)
101
112
  end
102
113
  end
103
114
 
@@ -112,10 +123,6 @@ class StimulusReflex::Reflex
112
123
  end
113
124
  end
114
125
 
115
- def url_params
116
- @url_params ||= Rails.application.routes.recognize_path_with_request(request, request.path, request.env[:extras] || {})
117
- end
118
-
119
126
  def process(name, *args)
120
127
  reflex_invoked = false
121
128
  result = run_callbacks(:process) {