stimulus_reflex 3.4.1 → 3.5.0.pre3
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 +664 -495
- data/Gemfile.lock +99 -95
- data/LATEST +1 -0
- data/README.md +17 -16
- data/app/channels/stimulus_reflex/channel.rb +44 -75
- data/lib/generators/USAGE +1 -1
- data/lib/generators/stimulus_reflex/{config_generator.rb → initializer_generator.rb} +3 -3
- data/lib/generators/stimulus_reflex/stimulus_reflex_generator.rb +5 -4
- 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 +21 -1
- data/lib/stimulus_reflex/broadcasters/broadcaster.rb +22 -18
- data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +6 -1
- data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +3 -5
- data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +22 -16
- data/lib/stimulus_reflex/broadcasters/update.rb +23 -0
- data/lib/stimulus_reflex/cable_ready_channels.rb +10 -2
- data/lib/stimulus_reflex/callbacks.rb +55 -5
- data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
- data/lib/stimulus_reflex/configuration.rb +4 -1
- data/lib/stimulus_reflex/dataset.rb +34 -0
- data/lib/stimulus_reflex/element.rb +20 -13
- data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
- data/lib/stimulus_reflex/reflex.rb +41 -21
- 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/utils/attribute_builder.rb +17 -0
- data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +6 -4
- data/lib/stimulus_reflex/utils/sanity_checker.rb +210 -0
- data/lib/stimulus_reflex/version.rb +1 -1
- data/lib/stimulus_reflex.rb +10 -2
- data/lib/tasks/stimulus_reflex/install.rake +54 -15
- data/test/broadcasters/broadcaster_test.rb +0 -1
- data/test/broadcasters/broadcaster_test_case.rb +25 -1
- data/test/broadcasters/nothing_broadcaster_test.rb +14 -20
- data/test/broadcasters/page_broadcaster_test.rb +31 -29
- data/test/broadcasters/selector_broadcaster_test.rb +165 -55
- data/test/callbacks_test.rb +652 -0
- data/test/concern_enhancer_test.rb +54 -0
- data/test/element_test.rb +254 -0
- data/test/generators/stimulus_reflex_generator_test.rb +8 -0
- data/test/reflex_test.rb +12 -1
- data/test/test_helper.rb +25 -1
- data/test/tmp/app/reflexes/application_reflex.rb +10 -3
- data/test/tmp/app/reflexes/{user_reflex.rb → demo_reflex.rb} +4 -12
- metadata +65 -36
- data/lib/stimulus_reflex/sanity_checker.rb +0 -154
- data/package.json +0 -57
- data/stimulus_reflex.gemspec +0 -42
- data/tags +0 -156
- data/yarn.lock +0 -4687
@@ -1,21 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# The ActionCable logger is REALLY noisy, and might even impact performance.
|
4
|
+
# Uncomment the line below to silence the ActionCable logger.
|
5
|
+
|
6
|
+
# ActionCable.server.config.logger = Logger.new(nil)
|
7
|
+
|
3
8
|
StimulusReflex.configure do |config|
|
4
9
|
# Enable/disable exiting / warning when the sanity checks fail options:
|
5
10
|
# `:exit` or `:warn` or `:ignore`
|
6
11
|
|
7
12
|
# config.on_failed_sanity_checks = :exit
|
8
13
|
|
14
|
+
# Enable/disable exiting / warning when there's a new StimulusReflex release
|
15
|
+
# `:exit` or `:warn` or `:ignore`
|
16
|
+
|
17
|
+
# config.on_new_version_available = :ignore
|
18
|
+
|
19
|
+
# Enable/disable exiting / warning when there is no default URLs specified in environment config
|
20
|
+
# `:warn` or `:ignore`
|
21
|
+
|
22
|
+
# config.on_missing_default_urls = :warn
|
23
|
+
|
9
24
|
# Override the parent class that the StimulusReflex ActionCable channel inherits from
|
10
25
|
|
11
26
|
# config.parent_channel = "ApplicationCable::Channel"
|
12
27
|
|
28
|
+
# Override the logger that the StimulusReflex uses; default is Rails' logger
|
29
|
+
# eg. Logger.new(RAILS_ROOT + "/log/reflex.log")
|
30
|
+
|
31
|
+
# config.logger = Rails.logger
|
32
|
+
|
13
33
|
# Customize server-side Reflex logging format, with optional colorization:
|
14
34
|
# 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
35
|
# Available colors: red, green, yellow, blue, magenta, cyan, white
|
16
36
|
# You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
|
17
37
|
# 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
|
-
# Learn more at: https://docs.stimulusreflex.com/troubleshooting#stimulusreflex-logging
|
38
|
+
# Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
|
19
39
|
|
20
40
|
# config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
|
21
41
|
|
@@ -3,7 +3,10 @@
|
|
3
3
|
module StimulusReflex
|
4
4
|
class Broadcaster
|
5
5
|
attr_reader :reflex, :logger, :operations
|
6
|
-
delegate :cable_ready, :permanent_attribute_name, to: :reflex
|
6
|
+
delegate :cable_ready, :permanent_attribute_name, :payload, to: :reflex
|
7
|
+
|
8
|
+
DEFAULT_HTML_WITHOUT_FORMAT = Nokogiri::XML::Node::SaveOptions::DEFAULT_HTML &
|
9
|
+
~Nokogiri::XML::Node::SaveOptions::FORMAT
|
7
10
|
|
8
11
|
def initialize(reflex)
|
9
12
|
@reflex = reflex
|
@@ -23,33 +26,34 @@ module StimulusReflex
|
|
23
26
|
false
|
24
27
|
end
|
25
28
|
|
26
|
-
def
|
27
|
-
logger.error "\e[31m#{body}\e[0m" if subject == "error"
|
29
|
+
def halted(data: {})
|
28
30
|
operations << ["document", :dispatch_event]
|
29
31
|
cable_ready.dispatch_event(
|
30
|
-
name: "stimulus-reflex:
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
32
|
+
name: "stimulus-reflex:morph-halted",
|
33
|
+
payload: payload,
|
34
|
+
stimulus_reflex: data.merge(morph: to_sym)
|
35
|
+
).broadcast
|
36
|
+
end
|
37
|
+
|
38
|
+
def error(data: {}, body: nil)
|
39
|
+
operations << ["document", :dispatch_event]
|
40
|
+
cable_ready.dispatch_event(
|
41
|
+
name: "stimulus-reflex:morph-error",
|
42
|
+
payload: payload,
|
43
|
+
stimulus_reflex: data.merge(morph: to_sym),
|
44
|
+
body: body&.to_s
|
45
|
+
).broadcast
|
46
|
+
end
|
47
|
+
|
48
|
+
# abstract methods to be implemented by subclasses
|
43
49
|
def broadcast(*args)
|
44
50
|
raise NotImplementedError
|
45
51
|
end
|
46
52
|
|
47
|
-
# abstract method to be implemented by subclasses
|
48
53
|
def to_sym
|
49
54
|
raise NotImplementedError
|
50
55
|
end
|
51
56
|
|
52
|
-
# abstract method to be implemented by subclasses
|
53
57
|
def to_s
|
54
58
|
raise NotImplementedError
|
55
59
|
end
|
@@ -3,7 +3,12 @@
|
|
3
3
|
module StimulusReflex
|
4
4
|
class NothingBroadcaster < Broadcaster
|
5
5
|
def broadcast(_, data)
|
6
|
-
|
6
|
+
operations << ["document", :dispatch_event]
|
7
|
+
cable_ready.dispatch_event(
|
8
|
+
name: "stimulus-reflex:morph-nothing",
|
9
|
+
payload: payload,
|
10
|
+
stimulus_reflex: data.merge(morph: to_sym)
|
11
|
+
).broadcast
|
7
12
|
end
|
8
13
|
|
9
14
|
def nothing?
|
@@ -12,18 +12,16 @@ module StimulusReflex
|
|
12
12
|
selectors = selectors.select { |s| document.css(s).present? }
|
13
13
|
selectors.each do |selector|
|
14
14
|
operations << [selector, :morph]
|
15
|
-
html = document.css(selector).inner_html
|
15
|
+
html = document.css(selector).inner_html(save_with: Broadcaster::DEFAULT_HTML_WITHOUT_FORMAT)
|
16
16
|
cable_ready.morph(
|
17
17
|
selector: selector,
|
18
18
|
html: html,
|
19
|
+
payload: payload,
|
19
20
|
children_only: true,
|
20
21
|
permanent_attribute_name: permanent_attribute_name,
|
21
|
-
stimulus_reflex: data.merge(
|
22
|
-
morph: to_sym
|
23
|
-
})
|
22
|
+
stimulus_reflex: data.merge(morph: to_sym)
|
24
23
|
)
|
25
24
|
end
|
26
|
-
|
27
25
|
cable_ready.broadcast
|
28
26
|
end
|
29
27
|
|
@@ -5,30 +5,27 @@ module StimulusReflex
|
|
5
5
|
def broadcast(_, data = {})
|
6
6
|
morphs.each do |morph|
|
7
7
|
selectors, html = morph
|
8
|
-
updates =
|
9
|
-
updates.each do |
|
10
|
-
|
11
|
-
|
12
|
-
match = fragment.at_css(selector)
|
8
|
+
updates = create_update_collection(selectors, html)
|
9
|
+
updates.each do |update|
|
10
|
+
fragment = Nokogiri::HTML.fragment(update.html.to_s)
|
11
|
+
match = fragment.at_css(update.selector)
|
13
12
|
if match.present?
|
14
|
-
operations << [selector, :morph]
|
13
|
+
operations << [update.selector, :morph]
|
15
14
|
cable_ready.morph(
|
16
|
-
selector: selector,
|
17
|
-
html: match.inner_html,
|
15
|
+
selector: update.selector,
|
16
|
+
html: match.inner_html(save_with: Broadcaster::DEFAULT_HTML_WITHOUT_FORMAT),
|
17
|
+
payload: payload,
|
18
18
|
children_only: true,
|
19
19
|
permanent_attribute_name: permanent_attribute_name,
|
20
|
-
stimulus_reflex: data.merge(
|
21
|
-
morph: to_sym
|
22
|
-
})
|
20
|
+
stimulus_reflex: data.merge(morph: to_sym)
|
23
21
|
)
|
24
22
|
else
|
25
|
-
operations << [selector, :inner_html]
|
23
|
+
operations << [update.selector, :inner_html]
|
26
24
|
cable_ready.inner_html(
|
27
|
-
selector: selector,
|
25
|
+
selector: update.selector,
|
28
26
|
html: fragment.to_html,
|
29
|
-
|
30
|
-
|
31
|
-
})
|
27
|
+
payload: payload,
|
28
|
+
stimulus_reflex: data.merge(morph: to_sym)
|
32
29
|
)
|
33
30
|
end
|
34
31
|
end
|
@@ -57,5 +54,14 @@ module StimulusReflex
|
|
57
54
|
def to_s
|
58
55
|
"Selector"
|
59
56
|
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def create_update_collection(selectors, html)
|
61
|
+
updates = selectors.is_a?(Hash) ? selectors : {selectors => html}
|
62
|
+
updates.map do |key, value|
|
63
|
+
StimulusReflex::Broadcasters::Update.new(key, value, reflex)
|
64
|
+
end
|
65
|
+
end
|
60
66
|
end
|
61
67
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module StimulusReflex
|
2
|
+
module Broadcasters
|
3
|
+
class Update
|
4
|
+
include CableReady::Identifiable
|
5
|
+
|
6
|
+
def initialize(key, value, reflex)
|
7
|
+
@key = key
|
8
|
+
@value = value
|
9
|
+
@reflex = reflex
|
10
|
+
end
|
11
|
+
|
12
|
+
def selector
|
13
|
+
@selector ||= identifiable?(@key) ? dom_id(@key) : @key.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def html
|
17
|
+
html = @reflex.render(@key) if @key.is_a?(ActiveRecord::Base) && @value.nil?
|
18
|
+
html = @reflex.render_collection(@key) if @key.is_a?(ActiveRecord::Relation) && @value.nil?
|
19
|
+
html || @value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -4,8 +4,9 @@ module StimulusReflex
|
|
4
4
|
class CableReadyChannels
|
5
5
|
delegate :[], to: "cable_ready_channels"
|
6
6
|
|
7
|
-
def initialize(stream_name)
|
7
|
+
def initialize(stream_name, reflex_id)
|
8
8
|
@stream_name = stream_name
|
9
|
+
@reflex_id = reflex_id
|
9
10
|
end
|
10
11
|
|
11
12
|
def cable_ready_channels
|
@@ -17,7 +18,14 @@ module StimulusReflex
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def method_missing(name, *args)
|
20
|
-
|
21
|
+
if stimulus_reflex_channel.respond_to?(name)
|
22
|
+
if (options = args.find_index { |a| a.is_a? Hash })
|
23
|
+
args[options][:reflex_id] = @reflex_id
|
24
|
+
elsif args.any?
|
25
|
+
args << {reflex_id: @reflex_id}
|
26
|
+
end
|
27
|
+
return stimulus_reflex_channel.public_send(name, *args)
|
28
|
+
end
|
21
29
|
super
|
22
30
|
end
|
23
31
|
|
@@ -22,24 +22,74 @@ module StimulusReflex
|
|
22
22
|
add_callback(:around, *args, &block)
|
23
23
|
end
|
24
24
|
|
25
|
+
def prepend_before_reflex(*args, &block)
|
26
|
+
prepend_callback(:before, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def prepend_after_reflex(*args, &block)
|
30
|
+
prepend_callback(:after, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepend_around_reflex(*args, &block)
|
34
|
+
prepend_callback(:around, *args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def skip_before_reflex(*args, &block)
|
38
|
+
omit_callback(:before, *args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_after_reflex(*args, &block)
|
42
|
+
omit_callback(:after, *args, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def skip_around_reflex(*args, &block)
|
46
|
+
omit_callback(:around, *args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :append_before_reflex, :before_reflex
|
50
|
+
alias_method :append_around_reflex, :around_reflex
|
51
|
+
alias_method :append_after_reflex, :after_reflex
|
52
|
+
|
25
53
|
private
|
26
54
|
|
27
55
|
def add_callback(kind, *args, &block)
|
28
|
-
|
29
|
-
|
30
|
-
|
56
|
+
insert_callbacks(args, block) do |name, options|
|
57
|
+
set_callback(:process, kind, name, options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prepend_callback(kind, *args, &block)
|
62
|
+
insert_callbacks(args, block) do |name, options|
|
63
|
+
set_callback(:process, kind, name, options.merge(prepend: true))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def omit_callback(kind, *args, &block)
|
68
|
+
insert_callbacks(args) do |name, options|
|
69
|
+
skip_callback(:process, kind, name, options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def insert_callbacks(callbacks, block = nil)
|
74
|
+
options = callbacks.extract_options!
|
75
|
+
normalize_callback_options!(options)
|
76
|
+
|
77
|
+
callbacks.push(block) if block
|
78
|
+
|
79
|
+
callbacks.each do |callback|
|
80
|
+
yield callback, options
|
81
|
+
end
|
31
82
|
end
|
32
83
|
|
33
84
|
def normalize_callback_options!(options)
|
34
85
|
normalize_callback_option! options, :only, :if
|
35
86
|
normalize_callback_option! options, :except, :unless
|
36
|
-
options
|
37
87
|
end
|
38
88
|
|
39
89
|
def normalize_callback_option!(options, from, to)
|
40
90
|
if (from = options.delete(from))
|
41
91
|
from_set = Array(from).map(&:to_s).to_set
|
42
|
-
from = proc { |reflex| from_set.include? reflex.method_name }
|
92
|
+
from = proc { |reflex| from_set.include? reflex.method_name.to_s }
|
43
93
|
options[to] = Array(options[to]).unshift(from)
|
44
94
|
end
|
45
95
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module StimulusReflex
|
2
|
+
module ConcernEnhancer
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def method_missing(name, *args)
|
7
|
+
case ancestors
|
8
|
+
when ->(a) { !(a & [StimulusReflex::Reflex]).empty? }
|
9
|
+
if (ActiveRecord::Base.public_methods + ActionController::Base.public_methods).include? name
|
10
|
+
nil
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
when ->(a) { !(a & [ActiveRecord::Base, ActionController::Base]).empty? }
|
15
|
+
if StimulusReflex::Reflex.public_methods.include? name
|
16
|
+
nil
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to_missing?(name, include_all = false)
|
26
|
+
case ancestors
|
27
|
+
when ->(a) { !(a & [StimulusReflex::Reflex]).empty? }
|
28
|
+
(ActiveRecord::Base.public_methods + ActionController::Base.public_methods).include?(name) || super
|
29
|
+
when ->(a) { !(a & [ActiveRecord::Base, ActionController::Base]).empty? }
|
30
|
+
StimulusReflex::Reflex.public_methods.include?(name) || super
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -14,14 +14,17 @@ 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, :logger, :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
|
27
|
+
@logger = Rails.logger
|
25
28
|
@middleware = ActionDispatch::MiddlewareStack.new
|
26
29
|
end
|
27
30
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stimulus_reflex/utils/attribute_builder"
|
4
|
+
|
5
|
+
class StimulusReflex::Dataset < OpenStruct
|
6
|
+
include StimulusReflex::AttributeBuilder
|
7
|
+
|
8
|
+
attr_accessor :attrs, :data_attrs
|
9
|
+
|
10
|
+
def initialize(data = {})
|
11
|
+
datasets = data["dataset"] || {}
|
12
|
+
regular_dataset = datasets["dataset"] || {}
|
13
|
+
@attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
|
14
|
+
@data_attrs = @attrs.transform_keys { |key| key.delete_prefix "data-" }
|
15
|
+
|
16
|
+
super build_underscored(@data_attrs)
|
17
|
+
end
|
18
|
+
|
19
|
+
def signed
|
20
|
+
@signed ||= ->(accessor) { GlobalID::Locator.locate_signed(self[accessor]) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def unsigned
|
24
|
+
@unsigned ||= ->(accessor) { GlobalID::Locator.locate(self[accessor]) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def boolean
|
28
|
+
@boolean ||= ->(accessor) { ActiveModel::Type::Boolean.new.cast(self[accessor]) || self[accessor].blank? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def numeric
|
32
|
+
@numeric ||= ->(accessor) { Float(self[accessor]) }
|
33
|
+
end
|
34
|
+
end
|
@@ -1,25 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "stimulus_reflex/dataset"
|
4
|
+
require "stimulus_reflex/utils/attribute_builder"
|
5
|
+
|
3
6
|
class StimulusReflex::Element < OpenStruct
|
4
|
-
|
7
|
+
include StimulusReflex::AttributeBuilder
|
8
|
+
|
9
|
+
attr_reader :attrs, :dataset
|
10
|
+
|
11
|
+
alias_method :data_attributes, :dataset
|
12
|
+
|
13
|
+
delegate :signed, :unsigned, :numeric, :boolean, :data_attrs, to: :dataset
|
5
14
|
|
6
15
|
def initialize(data = {})
|
7
|
-
@
|
8
|
-
@
|
9
|
-
all_attributes = @attributes.merge(@data_attributes)
|
10
|
-
super all_attributes.merge(all_attributes.transform_keys(&:underscore))
|
11
|
-
@data_attributes.transform_keys! { |key| key.delete_prefix "data-" }
|
12
|
-
end
|
16
|
+
@attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
|
17
|
+
@dataset = StimulusReflex::Dataset.new(data)
|
13
18
|
|
14
|
-
|
15
|
-
|
19
|
+
all_attributes = @attrs.merge(@dataset.attrs)
|
20
|
+
super build_underscored(all_attributes)
|
16
21
|
end
|
17
22
|
|
18
|
-
def
|
19
|
-
@
|
23
|
+
def attributes
|
24
|
+
@attributes ||= OpenStruct.new(build_underscored(attrs))
|
20
25
|
end
|
21
26
|
|
22
|
-
def
|
23
|
-
|
27
|
+
def to_dom_id
|
28
|
+
raise NoIDError.new "The element `morph` is called on must have a valid DOM ID" if id.blank?
|
29
|
+
|
30
|
+
"##{id}"
|
24
31
|
end
|
25
32
|
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,20 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
ClientAttributes = Struct.new(:reflex_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, keyword_init: true)
|
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
7
|
include StimulusReflex::Callbacks
|
8
|
+
include ActionView::Helpers::TagHelper
|
9
|
+
include CableReady::Identifiable
|
8
10
|
|
11
|
+
attr_accessor :payload, :headers
|
9
12
|
attr_reader :cable_ready, :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
|
10
13
|
|
11
14
|
alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
|
12
15
|
|
13
16
|
delegate :connection, :stream_name, to: :channel
|
14
17
|
delegate :controller_class, :flash, :session, to: :request
|
15
|
-
delegate :broadcast, :
|
16
|
-
delegate :reflex_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, to: :client_attributes
|
17
|
-
delegate :render, to: :controller_class
|
18
|
+
delegate :broadcast, :halted, :error, to: :broadcaster
|
19
|
+
delegate :reflex_id, :tab_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, to: :client_attributes
|
18
20
|
|
19
21
|
def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
|
20
22
|
if is_a? CableReady::Broadcaster
|
@@ -37,7 +39,9 @@ class StimulusReflex::Reflex
|
|
37
39
|
@broadcaster = StimulusReflex::PageBroadcaster.new(self)
|
38
40
|
@logger = StimulusReflex::Logger.new(self)
|
39
41
|
@client_attributes = ClientAttributes.new(client_attributes)
|
40
|
-
@cable_ready = StimulusReflex::CableReadyChannels.new(stream_name)
|
42
|
+
@cable_ready = StimulusReflex::CableReadyChannels.new(stream_name, reflex_id)
|
43
|
+
@payload = {}
|
44
|
+
@headers = {}
|
41
45
|
self.params
|
42
46
|
end
|
43
47
|
|
@@ -70,17 +74,15 @@ class StimulusReflex::Reflex
|
|
70
74
|
|
71
75
|
req = ActionDispatch::Request.new(env)
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
# fetch path params (controller, action, ...) and apply them
|
78
|
+
request_params = StimulusReflex::RequestParameters.new(params: @params, req: req, url: url)
|
79
|
+
req = request_params.apply!
|
76
80
|
|
77
|
-
req
|
78
|
-
req.env["action_dispatch.request.parameters"] = req.parameters.merge(@params)
|
79
|
-
req.tap { |r| r.session.send :load! }
|
81
|
+
req
|
80
82
|
end
|
81
83
|
end
|
82
84
|
|
83
|
-
def morph(selectors, html =
|
85
|
+
def morph(selectors, html = nil)
|
84
86
|
case selectors
|
85
87
|
when :page
|
86
88
|
raise StandardError.new("Cannot call :page morph after :#{broadcaster.to_sym} morph") unless broadcaster.page?
|
@@ -95,16 +97,29 @@ class StimulusReflex::Reflex
|
|
95
97
|
end
|
96
98
|
|
97
99
|
def controller
|
98
|
-
@controller ||=
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
c.set_response! controller_class.make_response!(request)
|
104
|
-
end
|
100
|
+
@controller ||= controller_class.new.tap do |c|
|
101
|
+
request.headers.merge!(headers)
|
102
|
+
c.instance_variable_set :@stimulus_reflex, true
|
103
|
+
c.set_request! request
|
104
|
+
c.set_response! controller_class.make_response!(request)
|
105
105
|
end
|
106
|
+
|
107
|
+
instance_variables.each { |name| @controller.instance_variable_set name, instance_variable_get(name) }
|
108
|
+
@controller
|
109
|
+
end
|
110
|
+
|
111
|
+
def controller?
|
112
|
+
!!defined? @controller
|
113
|
+
end
|
114
|
+
|
115
|
+
def render(*args)
|
116
|
+
options = args.extract_options!
|
117
|
+
(options[:locals] ||= {}).reverse_merge!(params: params)
|
118
|
+
args << options.reverse_merge(layout: false)
|
119
|
+
controller_class.renderer.new(connection.env.merge("SCRIPT_NAME" => "")).render(*args)
|
106
120
|
end
|
107
121
|
|
122
|
+
# Invoke the reflex action specified by `name` and run all callbacks
|
108
123
|
def process(name, *args)
|
109
124
|
reflex_invoked = false
|
110
125
|
result = run_callbacks(:process) {
|
@@ -129,7 +144,12 @@ class StimulusReflex::Reflex
|
|
129
144
|
@_params ||= ActionController::Parameters.new(request.parameters)
|
130
145
|
end
|
131
146
|
|
132
|
-
|
133
|
-
|
147
|
+
# morphdom needs content to be wrapped in an element with the same id when children_only: true
|
148
|
+
# 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
|
149
|
+
# Used internally to allow automatic partial collection rendering, but also useful to library users
|
150
|
+
# eg. `morph dom_id(@posts), render_collection(@posts)`
|
151
|
+
def render_collection(resource, content = nil)
|
152
|
+
content ||= render(resource)
|
153
|
+
tag.div(content.html_safe, id: dom_id(resource).from(1))
|
134
154
|
end
|
135
155
|
end
|