stimulus_reflex 3.4.0.pre1 → 3.4.0.pre6

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