stimulus_reflex 3.4.0.pre1 → 3.4.0.pre6

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.

Potentially problematic release.


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

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -2
  3. data/Gemfile.lock +15 -15
  4. data/README.md +6 -10
  5. data/Rakefile +5 -5
  6. data/app/channels/stimulus_reflex/channel.rb +20 -4
  7. data/bin/console +1 -0
  8. data/bin/standardize +1 -1
  9. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/%file_name%_controller.js.tt +14 -2
  10. data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/application_controller.js.tt +10 -2
  11. data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +18 -9
  12. data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +2 -2
  13. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +13 -2
  14. data/lib/stimulus_reflex.rb +3 -0
  15. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +11 -6
  16. data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +4 -0
  17. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +7 -2
  18. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +12 -2
  19. data/lib/stimulus_reflex/cable_ready_channels.rb +18 -0
  20. data/lib/stimulus_reflex/configuration.rb +5 -2
  21. data/lib/stimulus_reflex/logger.rb +106 -0
  22. data/lib/stimulus_reflex/reflex.rb +9 -5
  23. data/lib/stimulus_reflex/sanity_checker.rb +60 -11
  24. data/lib/stimulus_reflex/utils/colorize.rb +23 -0
  25. data/lib/stimulus_reflex/version.rb +1 -1
  26. data/lib/tasks/stimulus_reflex/install.rake +3 -2
  27. data/package.json +11 -17
  28. data/stimulus_reflex.gemspec +1 -1
  29. data/tags +82 -41
  30. data/test/broadcasters/broadcaster_test.rb +10 -0
  31. data/test/broadcasters/broadcaster_test_case.rb +13 -0
  32. data/test/broadcasters/nothing_broadcaster_test.rb +33 -0
  33. data/test/broadcasters/page_broadcaster_test.rb +73 -0
  34. data/test/broadcasters/selector_broadcaster_test.rb +55 -0
  35. data/test/test_helper.rb +37 -0
  36. data/test/tmp/app/reflexes/application_reflex.rb +2 -2
  37. data/test/tmp/app/reflexes/demo_reflex.rb +34 -0
  38. data/yarn.lock +248 -1798
  39. metadata +23 -10
  40. data/test/tmp/app/reflexes/user_reflex.rb +0 -33
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ApplicationReflex < StimulusReflex::Reflex
4
- # Put application wide Reflex behavior in this file.
4
+ # Put application-wide Reflex behavior and callbacks in this file.
5
5
  #
6
6
  # Example:
7
7
  #
8
8
  # # If your ActionCable connection is: `identified_by :current_user`
9
9
  # delegate :current_user, to: :connection
10
10
  #
11
- # Learn more at: https://docs.stimulusreflex.com
11
+ # Learn more at: https://docs.stimulusreflex.com/reflexes#reflex-classes
12
12
  end
@@ -1,9 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
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
6
8
 
7
9
  # Override the parent class that the StimulusReflex ActionCable channel inherits from
10
+
8
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})" }
9
20
  end
@@ -9,6 +9,7 @@ require "action_cable"
9
9
  require "nokogiri"
10
10
  require "cable_ready"
11
11
  require "stimulus_reflex/version"
12
+ require "stimulus_reflex/cable_ready_channels"
12
13
  require "stimulus_reflex/configuration"
13
14
  require "stimulus_reflex/reflex"
14
15
  require "stimulus_reflex/element"
@@ -17,6 +18,8 @@ require "stimulus_reflex/broadcasters/broadcaster"
17
18
  require "stimulus_reflex/broadcasters/nothing_broadcaster"
18
19
  require "stimulus_reflex/broadcasters/page_broadcaster"
19
20
  require "stimulus_reflex/broadcasters/selector_broadcaster"
21
+ require "stimulus_reflex/utils/colorize"
22
+ require "stimulus_reflex/logger"
20
23
 
21
24
  module StimulusReflex
22
25
  class Engine < Rails::Engine
@@ -2,14 +2,13 @@
2
2
 
3
3
  module StimulusReflex
4
4
  class Broadcaster
5
- include CableReady::Broadcaster
6
-
7
- attr_reader :reflex, :logger
8
- delegate :permanent_attribute_name, :stream_name, to: :reflex
5
+ attr_reader :reflex, :logger, :operations
6
+ delegate :cable_ready, :permanent_attribute_name, to: :reflex
9
7
 
10
8
  def initialize(reflex)
11
9
  @reflex = reflex
12
- @logger = Rails.logger
10
+ @logger = Rails.logger if defined?(Rails.logger)
11
+ @operations = []
13
12
  end
14
13
 
15
14
  def nothing?
@@ -26,7 +25,8 @@ module StimulusReflex
26
25
 
27
26
  def broadcast_message(subject:, body: nil, data: {}, error: nil)
28
27
  logger.error "\e[31m#{body}\e[0m" if subject == "error"
29
- cable_ready[stream_name].dispatch_event(
28
+ operations << ["document", :dispatch_event]
29
+ cable_ready.dispatch_event(
30
30
  name: "stimulus-reflex:server-message",
31
31
  detail: {
32
32
  reflexId: data["reflexId"],
@@ -48,5 +48,10 @@ module StimulusReflex
48
48
  def to_sym
49
49
  raise NotImplementedError
50
50
  end
51
+
52
+ # abstract method to be implemented by subclasses
53
+ def to_s
54
+ raise NotImplementedError
55
+ end
51
56
  end
52
57
  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
@@ -8,11 +8,12 @@ module StimulusReflex
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
- cable_ready[stream_name].morph(
16
+ cable_ready.morph(
16
17
  selector: selector,
17
18
  html: html,
18
19
  children_only: true,
@@ -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,7 +11,8 @@ module StimulusReflex
11
11
  fragment = Nokogiri::HTML.fragment(html)
12
12
  match = fragment.at_css(selector)
13
13
  if match.present?
14
- cable_ready[stream_name].morph(
14
+ operations << [selector, :morph]
15
+ cable_ready.morph(
15
16
  selector: selector,
16
17
  html: match.inner_html,
17
18
  children_only: true,
@@ -21,7 +22,8 @@ module StimulusReflex
21
22
  })
22
23
  )
23
24
  else
24
- cable_ready[stream_name].inner_html(
25
+ operations << [selector, :inner_html]
26
+ cable_ready.inner_html(
25
27
  selector: selector,
26
28
  html: fragment.to_html,
27
29
  stimulus_reflex: data.merge({
@@ -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,18 @@
1
+ module StimulusReflex
2
+ class CableReadyChannels
3
+ def initialize(stream_name)
4
+ @cable_ready_channels = CableReady::Channels.instance
5
+ @stimulus_reflex_channel = @cable_ready_channels[stream_name]
6
+ end
7
+
8
+ def method_missing(name, *args)
9
+ return @stimulus_reflex_channel.send(name, *args) if @stimulus_reflex_channel.respond_to?(name)
10
+ @cable_ready_channels.send(name, *args)
11
+ end
12
+
13
+ def respond_to_missing?(name, include_all)
14
+ @stimulus_reflex_channel.respond_to?(name, include_all) ||
15
+ @cable_ready_channels.respond_to?(name, include_all)
16
+ end
17
+ end
18
+ end
@@ -14,11 +14,14 @@ module StimulusReflex
14
14
  end
15
15
 
16
16
  class Configuration
17
- attr_accessor :exit_on_failed_sanity_checks, :parent_channel
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})" }
18
20
 
19
21
  def initialize
20
- @exit_on_failed_sanity_checks = true
22
+ @on_failed_sanity_checks = :exit
21
23
  @parent_channel = "ApplicationCable::Channel"
24
+ @logging = DEFAULT_LOGGING
22
25
  end
23
26
  end
24
27
  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,9 +1,10 @@
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
6
- include CableReady::Broadcaster
7
8
 
8
9
  define_callbacks :process, skip_after_callbacks_if_terminated: true
9
10
 
@@ -43,23 +44,26 @@ class StimulusReflex::Reflex
43
44
  end
44
45
  end
45
46
 
46
- attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :permanent_attribute_name
47
+ attr_reader :cable_ready, :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
47
48
 
48
49
  alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
49
50
 
50
51
  delegate :connection, :stream_name, to: :channel
51
52
  delegate :flash, :session, to: :request
52
53
  delegate :broadcast, :broadcast_message, to: :broadcaster
54
+ delegate :reflex_id, :reflex_controller, :xpath, :c_xpath, :permanent_attribute_name, to: :client_attributes
53
55
 
54
- def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, permanent_attribute_name: nil, params: {})
56
+ def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
55
57
  @channel = channel
56
58
  @url = url
57
59
  @element = element
58
60
  @selectors = selectors
59
61
  @method_name = method_name
60
62
  @params = params
61
- @permanent_attribute_name = permanent_attribute_name
62
63
  @broadcaster = StimulusReflex::PageBroadcaster.new(self)
64
+ @logger = StimulusReflex::Logger.new(self)
65
+ @client_attributes = ClientAttributes.new(client_attributes)
66
+ @cable_ready = StimulusReflex::CableReadyChannels.new(stream_name)
63
67
  self.params
64
68
  end
65
69
 
@@ -104,7 +108,7 @@ class StimulusReflex::Reflex
104
108
  else
105
109
  raise StandardError.new("Cannot call :selector morph after :nothing morph") if broadcaster.nothing?
106
110
  @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
107
- broadcaster.morphs << [selectors, html]
111
+ broadcaster.append_morph(selectors, html)
108
112
  end
109
113
  end
110
114
 
@@ -3,10 +3,28 @@
3
3
  class StimulusReflex::SanityChecker
4
4
  JSON_VERSION_FORMAT = /(\d+\.\d+\.\d+.*)"/
5
5
 
6
- def self.check!
7
- instance = new
8
- instance.check_caching_enabled
9
- instance.check_javascript_package_version
6
+ class << self
7
+ def check!
8
+ return if StimulusReflex.config.on_failed_sanity_checks == :ignore
9
+ return if called_by_installer?
10
+ return if called_by_generate_config?
11
+
12
+ instance = new
13
+ instance.check_caching_enabled
14
+ instance.check_javascript_package_version
15
+ end
16
+
17
+ private
18
+
19
+ def called_by_installer?
20
+ Rake.application.top_level_tasks.include? "stimulus_reflex:install"
21
+ rescue
22
+ false
23
+ end
24
+
25
+ def called_by_generate_config?
26
+ ARGV.include? "stimulus_reflex:config"
27
+ end
10
28
  end
11
29
 
12
30
  def check_caching_enabled
@@ -80,19 +98,50 @@ class StimulusReflex::SanityChecker
80
98
  Rails.root.join("node_modules", "stimulus_reflex", "package.json")
81
99
  end
82
100
 
101
+ def initializer_path
102
+ @_initializer_path ||= Rails.root.join("config", "initializers", "stimulus_reflex.rb")
103
+ end
104
+
83
105
  def warn_and_exit(text)
84
106
  puts "WARNING:"
85
107
  puts text
86
- exit_with_info if StimulusReflex.config.exit_on_failed_sanity_checks
108
+ exit_with_info if StimulusReflex.config.on_failed_sanity_checks == :exit
87
109
  end
88
110
 
89
111
  def exit_with_info
90
112
  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
113
+
114
+ # bundle exec rails generate stimulus_reflex:config
115
+ if File.exist?(initializer_path)
116
+ puts <<~INFO
117
+ If you know what you are doing and you want to start the application anyway,
118
+ you can add the following directive to the StimulusReflex initializer,
119
+ which is located at #{initializer_path}
120
+
121
+ StimulusReflex.configure do |config|
122
+ config.on_failed_sanity_checks = :warn
123
+ end
124
+
125
+ INFO
126
+ else
127
+ puts <<~INFO
128
+ If you know what you are doing and you want to start the application anyway,
129
+ you can create a StimulusReflex initializer with the command:
130
+
131
+ bundle exec rails generate stimulus_reflex:config
132
+
133
+ Then open your initializer at
134
+
135
+ <RAILS_ROOT>/config/initializers/stimulus_reflex.rb
136
+
137
+ and then add the following directive:
138
+
139
+ StimulusReflex.configure do |config|
140
+ config.on_failed_sanity_checks = :warn
141
+ end
142
+
143
+ INFO
144
+ end
145
+ exit false
97
146
  end
98
147
  end