stimulus_reflex 3.3.0 → 3.4.0.pre4

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 (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) {