stimulus_reflex 3.4.0 → 3.5.0.pre2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +639 -484
- data/CODE_OF_CONDUCT.md +6 -0
- data/Gemfile.lock +99 -96
- data/LATEST +1 -0
- data/README.md +15 -14
- data/app/channels/stimulus_reflex/channel.rb +42 -73
- data/lib/generators/USAGE +1 -1
- data/lib/generators/stimulus_reflex/{config_generator.rb → initializer_generator.rb} +3 -3
- data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +3 -2
- data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +11 -4
- data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +16 -1
- data/lib/stimulus_reflex.rb +11 -2
- data/lib/stimulus_reflex/broadcasters/broadcaster.rb +7 -4
- data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +2 -2
- data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +20 -10
- data/lib/stimulus_reflex/broadcasters/update.rb +23 -0
- data/lib/stimulus_reflex/cable_ready_channels.rb +18 -3
- data/lib/stimulus_reflex/callbacks.rb +98 -0
- data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
- data/lib/stimulus_reflex/configuration.rb +3 -1
- data/lib/stimulus_reflex/element.rb +48 -7
- data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
- data/lib/stimulus_reflex/reflex.rb +49 -58
- data/lib/stimulus_reflex/reflex_data.rb +79 -0
- data/lib/stimulus_reflex/reflex_factory.rb +31 -0
- data/lib/stimulus_reflex/request_parameters.rb +19 -0
- data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +6 -8
- data/lib/stimulus_reflex/utils/sanity_checker.rb +210 -0
- data/lib/stimulus_reflex/version.rb +1 -1
- data/lib/tasks/stimulus_reflex/install.rake +54 -15
- data/package.json +7 -6
- data/stimulus_reflex.gemspec +8 -8
- data/test/broadcasters/broadcaster_test_case.rb +1 -1
- data/test/broadcasters/nothing_broadcaster_test.rb +5 -3
- data/test/broadcasters/page_broadcaster_test.rb +8 -4
- data/test/broadcasters/selector_broadcaster_test.rb +171 -55
- data/test/callbacks_test.rb +652 -0
- data/test/concern_enhancer_test.rb +54 -0
- data/test/element_test.rb +181 -0
- data/test/reflex_test.rb +2 -2
- data/test/test_helper.rb +22 -0
- data/test/tmp/app/reflexes/application_reflex.rb +10 -3
- data/test/tmp/app/reflexes/demo_reflex.rb +4 -2
- data/yarn.lock +1280 -1284
- metadata +47 -33
- data/lib/stimulus_reflex/sanity_checker.rb +0 -154
- data/tags +0 -156
@@ -14,12 +14,14 @@ module StimulusReflex
|
|
14
14
|
end
|
15
15
|
|
16
16
|
class Configuration
|
17
|
-
attr_accessor :on_failed_sanity_checks, :parent_channel, :logging, :middleware
|
17
|
+
attr_accessor :on_failed_sanity_checks, :on_new_version_available, :on_missing_default_urls, :parent_channel, :logging, :middleware
|
18
18
|
|
19
19
|
DEFAULT_LOGGING = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
|
20
20
|
|
21
21
|
def initialize
|
22
22
|
@on_failed_sanity_checks = :exit
|
23
|
+
@on_new_version_available = :ignore
|
24
|
+
@on_missing_default_urls = :warn
|
23
25
|
@parent_channel = "ApplicationCable::Channel"
|
24
26
|
@logging = DEFAULT_LOGGING
|
25
27
|
@middleware = ActionDispatch::MiddlewareStack.new
|
@@ -1,14 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class StimulusReflex::Element < OpenStruct
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :attrs, :data_attrs, :inner_html, :text_content
|
5
5
|
|
6
6
|
def initialize(data = {})
|
7
|
-
@
|
8
|
-
@
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
@attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
|
8
|
+
@inner_html = data["inner_html"]
|
9
|
+
@text_content = data["text_content"]
|
10
|
+
|
11
|
+
datasets = data["dataset"] || {}
|
12
|
+
regular_dataset = datasets["dataset"] || {}
|
13
|
+
@data_attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
|
14
|
+
|
15
|
+
super build_underscored(all_attributes)
|
16
|
+
|
17
|
+
@data_attrs.transform_keys! { |key| key.delete_prefix "data-" }
|
12
18
|
end
|
13
19
|
|
14
20
|
def signed
|
@@ -19,7 +25,42 @@ class StimulusReflex::Element < OpenStruct
|
|
19
25
|
@unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
|
20
26
|
end
|
21
27
|
|
28
|
+
def attributes
|
29
|
+
@attributes ||= OpenStruct.new(build_underscored(attrs))
|
30
|
+
end
|
31
|
+
|
22
32
|
def dataset
|
23
|
-
@dataset ||= OpenStruct.new(
|
33
|
+
@dataset ||= OpenStruct.new(build_underscored(data_attrs))
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_dom_id
|
37
|
+
raise NoIDError.new "The element `morph` is called on must have a valid DOM ID" if id.blank?
|
38
|
+
|
39
|
+
"##{id}"
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :data_attributes, :dataset
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def all_attributes
|
47
|
+
@attrs.merge(@data_attrs)
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_data_attrs(dataset, dataset_all)
|
51
|
+
dataset_all.transform_keys! { |key| "data-#{key.delete_prefix("data-").pluralize}" }
|
52
|
+
|
53
|
+
dataset.each { |key, value| dataset_all[key]&.prepend(value) }
|
54
|
+
|
55
|
+
data_attrs = dataset.merge(dataset_all)
|
56
|
+
|
57
|
+
HashWithIndifferentAccess.new(data_attrs || {})
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_underscored(attrs)
|
61
|
+
attrs.merge(attrs.transform_keys(&:underscore))
|
62
|
+
end
|
63
|
+
|
64
|
+
class NoIDError < StandardError
|
24
65
|
end
|
25
66
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StimulusReflex
|
4
|
+
class ReflexMethodInvocationPolicy
|
5
|
+
attr_reader :arguments, :required_params, :optional_params
|
6
|
+
|
7
|
+
def initialize(method, arguments)
|
8
|
+
@arguments = arguments
|
9
|
+
@required_params = method.parameters.select { |(kind, _)| kind == :req }
|
10
|
+
@optional_params = method.parameters.select { |(kind, _)| kind == :opt }
|
11
|
+
end
|
12
|
+
|
13
|
+
def no_arguments?
|
14
|
+
arguments.size == 0 && required_params.size == 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def arguments?
|
18
|
+
arguments.size >= required_params.size && arguments.size <= required_params.size + optional_params.size
|
19
|
+
end
|
20
|
+
|
21
|
+
def unknown?
|
22
|
+
return false if no_arguments?
|
23
|
+
return false if arguments?
|
24
|
+
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,49 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
ClientAttributes = Struct.new(:reflex_id, :reflex_controller, :
|
3
|
+
ClientAttributes = Struct.new(:reflex_id, :tab_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, keyword_init: true)
|
4
4
|
|
5
5
|
class StimulusReflex::Reflex
|
6
6
|
include ActiveSupport::Rescuable
|
7
|
-
include
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
class << self
|
12
|
-
def before_reflex(*args, &block)
|
13
|
-
add_callback(:before, *args, &block)
|
14
|
-
end
|
15
|
-
|
16
|
-
def after_reflex(*args, &block)
|
17
|
-
add_callback(:after, *args, &block)
|
18
|
-
end
|
19
|
-
|
20
|
-
def around_reflex(*args, &block)
|
21
|
-
add_callback(:around, *args, &block)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def add_callback(kind, *args, &block)
|
27
|
-
options = args.extract_options!
|
28
|
-
options.assert_valid_keys :if, :unless, :only, :except
|
29
|
-
set_callback(*[:process, kind, args, normalize_callback_options!(options)].flatten, &block)
|
30
|
-
end
|
31
|
-
|
32
|
-
def normalize_callback_options!(options)
|
33
|
-
normalize_callback_option! options, :only, :if
|
34
|
-
normalize_callback_option! options, :except, :unless
|
35
|
-
options
|
36
|
-
end
|
37
|
-
|
38
|
-
def normalize_callback_option!(options, from, to)
|
39
|
-
if (from = options.delete(from))
|
40
|
-
from_set = Array(from).map(&:to_s).to_set
|
41
|
-
from = proc { |reflex| from_set.include? reflex.method_name }
|
42
|
-
options[to] = Array(options[to]).unshift(from)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
7
|
+
include StimulusReflex::Callbacks
|
8
|
+
include ActionView::Helpers::TagHelper
|
9
|
+
include CableReady::Identifiable
|
46
10
|
|
11
|
+
attr_accessor :payload
|
47
12
|
attr_reader :cable_ready, :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
|
48
13
|
|
49
14
|
alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
|
@@ -51,11 +16,20 @@ class StimulusReflex::Reflex
|
|
51
16
|
delegate :connection, :stream_name, to: :channel
|
52
17
|
delegate :controller_class, :flash, :session, to: :request
|
53
18
|
delegate :broadcast, :broadcast_message, to: :broadcaster
|
54
|
-
delegate :reflex_id, :reflex_controller, :
|
55
|
-
delegate :render, to: :controller_class
|
56
|
-
delegate :dom_id, to: "ActionView::RecordIdentifier"
|
19
|
+
delegate :reflex_id, :tab_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, to: :client_attributes
|
57
20
|
|
58
21
|
def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
|
22
|
+
if is_a? CableReady::Broadcaster
|
23
|
+
message = <<~MSG
|
24
|
+
|
25
|
+
#{self.class.name} includes CableReady::Broadcaster, and you need to remove it.
|
26
|
+
Reflexes have their own CableReady interface. You can just assume that it's present.
|
27
|
+
See https://docs.stimulusreflex.com/rtfm/cableready#using-cableready-inside-a-reflex-action for more details.
|
28
|
+
|
29
|
+
MSG
|
30
|
+
raise TypeError.new(message.strip)
|
31
|
+
end
|
32
|
+
|
59
33
|
@channel = channel
|
60
34
|
@url = url
|
61
35
|
@element = element
|
@@ -65,7 +39,8 @@ class StimulusReflex::Reflex
|
|
65
39
|
@broadcaster = StimulusReflex::PageBroadcaster.new(self)
|
66
40
|
@logger = StimulusReflex::Logger.new(self)
|
67
41
|
@client_attributes = ClientAttributes.new(client_attributes)
|
68
|
-
@cable_ready = StimulusReflex::CableReadyChannels.new(stream_name)
|
42
|
+
@cable_ready = StimulusReflex::CableReadyChannels.new(stream_name, reflex_id)
|
43
|
+
@payload = {}
|
69
44
|
self.params
|
70
45
|
end
|
71
46
|
|
@@ -98,17 +73,15 @@ class StimulusReflex::Reflex
|
|
98
73
|
|
99
74
|
req = ActionDispatch::Request.new(env)
|
100
75
|
|
101
|
-
|
102
|
-
|
103
|
-
|
76
|
+
# fetch path params (controller, action, ...) and apply them
|
77
|
+
request_params = StimulusReflex::RequestParameters.new(params: @params, req: req, url: url)
|
78
|
+
req = request_params.apply!
|
104
79
|
|
105
|
-
req
|
106
|
-
req.env["action_dispatch.request.parameters"] = req.parameters.merge(@params)
|
107
|
-
req.tap { |r| r.session.send :load! }
|
80
|
+
req
|
108
81
|
end
|
109
82
|
end
|
110
83
|
|
111
|
-
def morph(selectors, html =
|
84
|
+
def morph(selectors, html = nil)
|
112
85
|
case selectors
|
113
86
|
when :page
|
114
87
|
raise StandardError.new("Cannot call :page morph after :#{broadcaster.to_sym} morph") unless broadcaster.page?
|
@@ -123,16 +96,25 @@ class StimulusReflex::Reflex
|
|
123
96
|
end
|
124
97
|
|
125
98
|
def controller
|
126
|
-
@controller ||=
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
c.set_request! request
|
131
|
-
c.set_response! controller_class.make_response!(request)
|
132
|
-
end
|
99
|
+
@controller ||= controller_class.new.tap do |c|
|
100
|
+
c.instance_variable_set :@stimulus_reflex, true
|
101
|
+
c.set_request! request
|
102
|
+
c.set_response! controller_class.make_response!(request)
|
133
103
|
end
|
104
|
+
|
105
|
+
instance_variables.each { |name| @controller.instance_variable_set name, instance_variable_get(name) }
|
106
|
+
@controller
|
107
|
+
end
|
108
|
+
|
109
|
+
def controller?
|
110
|
+
!!defined? @controller
|
134
111
|
end
|
135
112
|
|
113
|
+
def render(*args)
|
114
|
+
controller_class.renderer.new(connection.env.merge("SCRIPT_NAME" => "")).render(*args)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Invoke the reflex action specified by `name` and run all callbacks
|
136
118
|
def process(name, *args)
|
137
119
|
reflex_invoked = false
|
138
120
|
result = run_callbacks(:process) {
|
@@ -156,4 +138,13 @@ class StimulusReflex::Reflex
|
|
156
138
|
def params
|
157
139
|
@_params ||= ActionController::Parameters.new(request.parameters)
|
158
140
|
end
|
141
|
+
|
142
|
+
# morphdom needs content to be wrapped in an element with the same id when children_only: true
|
143
|
+
# Oddly, it doesn't matter if the target element is a div! See: https://docs.stimulusreflex.com/appendices/troubleshooting#different-element-type-altogether-who-cares-so-long-as-the-css-selector-matches
|
144
|
+
# Used internally to allow automatic partial collection rendering, but also useful to library users
|
145
|
+
# eg. `morph dom_id(@posts), render_collection(@posts)`
|
146
|
+
def render_collection(resource, content = nil)
|
147
|
+
content ||= render(resource)
|
148
|
+
tag.div(content.html_safe, id: dom_id(resource).from(1))
|
149
|
+
end
|
159
150
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class StimulusReflex::ReflexData
|
2
|
+
attr_reader :data
|
3
|
+
|
4
|
+
def initialize(data)
|
5
|
+
@data = data
|
6
|
+
end
|
7
|
+
|
8
|
+
def reflex_name
|
9
|
+
reflex_name = target.split("#").first
|
10
|
+
reflex_name = reflex_name.camelize
|
11
|
+
reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex"
|
12
|
+
end
|
13
|
+
|
14
|
+
def selectors
|
15
|
+
selectors = (data["selectors"] || []).select(&:present?)
|
16
|
+
selectors = data["selectors"] = ["body"] if selectors.blank?
|
17
|
+
selectors
|
18
|
+
end
|
19
|
+
|
20
|
+
def target
|
21
|
+
data["target"].to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_name
|
25
|
+
target.split("#").second
|
26
|
+
end
|
27
|
+
|
28
|
+
def arguments
|
29
|
+
(data["args"] || []).map { |arg| object_with_indifferent_access arg } || []
|
30
|
+
end
|
31
|
+
|
32
|
+
def url
|
33
|
+
data["url"].to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def element
|
37
|
+
StimulusReflex::Element.new(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def permanent_attribute_name
|
41
|
+
data["permanentAttributeName"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def form_data
|
45
|
+
Rack::Utils.parse_nested_query(data["formData"])
|
46
|
+
end
|
47
|
+
|
48
|
+
def form_params
|
49
|
+
form_data.deep_merge(data["params"] || {})
|
50
|
+
end
|
51
|
+
|
52
|
+
def reflex_id
|
53
|
+
data["reflexId"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def tab_id
|
57
|
+
data["tabId"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def xpath_controller
|
61
|
+
data["xpathController"]
|
62
|
+
end
|
63
|
+
|
64
|
+
def xpath_element
|
65
|
+
data["xpathElement"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def reflex_controller
|
69
|
+
data["reflexController"]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def object_with_indifferent_access(object)
|
75
|
+
return object.with_indifferent_access if object.respond_to?(:with_indifferent_access)
|
76
|
+
object.map! { |obj| object_with_indifferent_access obj } if object.is_a?(Array)
|
77
|
+
object
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class StimulusReflex::ReflexFactory
|
2
|
+
class << self
|
3
|
+
attr_reader :reflex_data
|
4
|
+
|
5
|
+
def create_reflex_from_data(channel, reflex_data)
|
6
|
+
@reflex_data = reflex_data
|
7
|
+
reflex_class.new(channel,
|
8
|
+
url: reflex_data.url,
|
9
|
+
element: reflex_data.element,
|
10
|
+
selectors: reflex_data.selectors,
|
11
|
+
method_name: reflex_data.method_name,
|
12
|
+
params: reflex_data.form_params,
|
13
|
+
client_attributes: {
|
14
|
+
reflex_id: reflex_data.reflex_id,
|
15
|
+
tab_id: reflex_data.tab_id,
|
16
|
+
xpath_controller: reflex_data.xpath_controller,
|
17
|
+
xpath_element: reflex_data.xpath_element,
|
18
|
+
reflex_controller: reflex_data.reflex_controller,
|
19
|
+
permanent_attribute_name: reflex_data.permanent_attribute_name
|
20
|
+
})
|
21
|
+
end
|
22
|
+
|
23
|
+
def reflex_class
|
24
|
+
reflex_data.reflex_name.constantize.tap { |klass| raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex") unless is_reflex?(klass) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_reflex?(klass)
|
28
|
+
klass.ancestors.include? StimulusReflex::Reflex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module StimulusReflex
|
2
|
+
class RequestParameters
|
3
|
+
def initialize(params:, req:, url:)
|
4
|
+
@params = params
|
5
|
+
@req = req
|
6
|
+
@url = url
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply!
|
10
|
+
path_params = Rails.application.routes.recognize_path_with_request(@req, @url, @req.env[:extras] || {})
|
11
|
+
path_params[:controller] = path_params[:controller].force_encoding("UTF-8")
|
12
|
+
path_params[:action] = path_params[:action].force_encoding("UTF-8")
|
13
|
+
|
14
|
+
@req.env.merge(ActionDispatch::Http::Parameters::PARAMETERS_KEY => path_params)
|
15
|
+
@req.env["action_dispatch.request.parameters"] = @req.parameters.merge(@params)
|
16
|
+
@req.tap { |r| r.session.send :load! }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -12,12 +12,10 @@ module StimulusReflex
|
|
12
12
|
def print
|
13
13
|
return unless config_logging.instance_of?(Proc)
|
14
14
|
|
15
|
-
puts
|
16
15
|
reflex.broadcaster.operations.each do
|
17
16
|
puts instance_eval(&config_logging) + "\e[0m"
|
18
17
|
@current_operation += 1
|
19
18
|
end
|
20
|
-
puts
|
21
19
|
end
|
22
20
|
|
23
21
|
private
|
@@ -79,22 +77,22 @@ module StimulusReflex
|
|
79
77
|
Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
80
78
|
end
|
81
79
|
|
82
|
-
def method_missing
|
83
|
-
return send(
|
80
|
+
def method_missing(name, *args)
|
81
|
+
return send(name) if private_instance_methods.include?(name.to_sym)
|
84
82
|
|
85
83
|
reflex.connection.identifiers.each do |identifier|
|
86
84
|
ident = reflex.connection.send(identifier)
|
87
|
-
return ident.send(
|
85
|
+
return ident.send(name) if ident.respond_to?(:attributes) && ident.attributes.key?(name.to_s)
|
88
86
|
end
|
89
87
|
"-"
|
90
88
|
end
|
91
89
|
|
92
|
-
def respond_to_missing?
|
93
|
-
return true if private_instance_methods.include?(
|
90
|
+
def respond_to_missing?(name, include_all)
|
91
|
+
return true if private_instance_methods.include?(name.to_sym)
|
94
92
|
|
95
93
|
reflex.connection.identifiers.each do |identifier|
|
96
94
|
ident = reflex.connection.send(identifier)
|
97
|
-
return true if ident.respond_to?(:attributes) && ident.attributes.key?(
|
95
|
+
return true if ident.respond_to?(:attributes) && ident.attributes.key?(name.to_s)
|
98
96
|
end
|
99
97
|
false
|
100
98
|
end
|