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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -2
- data/Gemfile.lock +15 -15
- data/README.md +6 -10
- data/Rakefile +5 -5
- data/app/channels/stimulus_reflex/channel.rb +20 -4
- data/bin/console +1 -0
- data/bin/standardize +1 -1
- data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/%file_name%_controller.js.tt +14 -2
- data/lib/generators/stimulus_reflex/templates/app/javascript/controllers/application_controller.js.tt +10 -2
- data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +18 -9
- data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +2 -2
- data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +13 -2
- data/lib/stimulus_reflex.rb +3 -0
- data/lib/stimulus_reflex/broadcasters/broadcaster.rb +11 -6
- data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +4 -0
- data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +7 -2
- data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +12 -2
- data/lib/stimulus_reflex/cable_ready_channels.rb +18 -0
- data/lib/stimulus_reflex/configuration.rb +5 -2
- data/lib/stimulus_reflex/logger.rb +106 -0
- data/lib/stimulus_reflex/reflex.rb +9 -5
- data/lib/stimulus_reflex/sanity_checker.rb +60 -11
- data/lib/stimulus_reflex/utils/colorize.rb +23 -0
- data/lib/stimulus_reflex/version.rb +1 -1
- data/lib/tasks/stimulus_reflex/install.rake +3 -2
- data/package.json +11 -17
- data/stimulus_reflex.gemspec +1 -1
- data/tags +82 -41
- data/test/broadcasters/broadcaster_test.rb +10 -0
- data/test/broadcasters/broadcaster_test_case.rb +13 -0
- data/test/broadcasters/nothing_broadcaster_test.rb +33 -0
- data/test/broadcasters/page_broadcaster_test.rb +73 -0
- data/test/broadcasters/selector_broadcaster_test.rb +55 -0
- data/test/test_helper.rb +37 -0
- data/test/tmp/app/reflexes/application_reflex.rb +2 -2
- data/test/tmp/app/reflexes/demo_reflex.rb +34 -0
- data/yarn.lock +248 -1798
- metadata +23 -10
- 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
|
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
|
5
|
-
#
|
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
|
data/lib/stimulus_reflex.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
@@ -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
|
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
|
-
|
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
|
-
|
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 :
|
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
|
-
@
|
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, :
|
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,
|
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.
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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.
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|