stimulus_reflex 3.4.1 → 3.5.0.pre3

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +664 -495
  3. data/Gemfile.lock +99 -95
  4. data/LATEST +1 -0
  5. data/README.md +17 -16
  6. data/app/channels/stimulus_reflex/channel.rb +44 -75
  7. data/lib/generators/USAGE +1 -1
  8. data/lib/generators/stimulus_reflex/{config_generator.rb → initializer_generator.rb} +3 -3
  9. data/lib/generators/stimulus_reflex/stimulus_reflex_generator.rb +5 -4
  10. data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +3 -2
  11. data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +11 -4
  12. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +21 -1
  13. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +22 -18
  14. data/lib/stimulus_reflex/broadcasters/nothing_broadcaster.rb +6 -1
  15. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +3 -5
  16. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +22 -16
  17. data/lib/stimulus_reflex/broadcasters/update.rb +23 -0
  18. data/lib/stimulus_reflex/cable_ready_channels.rb +10 -2
  19. data/lib/stimulus_reflex/callbacks.rb +55 -5
  20. data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
  21. data/lib/stimulus_reflex/configuration.rb +4 -1
  22. data/lib/stimulus_reflex/dataset.rb +34 -0
  23. data/lib/stimulus_reflex/element.rb +20 -13
  24. data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
  25. data/lib/stimulus_reflex/reflex.rb +41 -21
  26. data/lib/stimulus_reflex/reflex_data.rb +79 -0
  27. data/lib/stimulus_reflex/reflex_factory.rb +31 -0
  28. data/lib/stimulus_reflex/request_parameters.rb +19 -0
  29. data/lib/stimulus_reflex/utils/attribute_builder.rb +17 -0
  30. data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +6 -4
  31. data/lib/stimulus_reflex/utils/sanity_checker.rb +210 -0
  32. data/lib/stimulus_reflex/version.rb +1 -1
  33. data/lib/stimulus_reflex.rb +10 -2
  34. data/lib/tasks/stimulus_reflex/install.rake +54 -15
  35. data/test/broadcasters/broadcaster_test.rb +0 -1
  36. data/test/broadcasters/broadcaster_test_case.rb +25 -1
  37. data/test/broadcasters/nothing_broadcaster_test.rb +14 -20
  38. data/test/broadcasters/page_broadcaster_test.rb +31 -29
  39. data/test/broadcasters/selector_broadcaster_test.rb +165 -55
  40. data/test/callbacks_test.rb +652 -0
  41. data/test/concern_enhancer_test.rb +54 -0
  42. data/test/element_test.rb +254 -0
  43. data/test/generators/stimulus_reflex_generator_test.rb +8 -0
  44. data/test/reflex_test.rb +12 -1
  45. data/test/test_helper.rb +25 -1
  46. data/test/tmp/app/reflexes/application_reflex.rb +10 -3
  47. data/test/tmp/app/reflexes/{user_reflex.rb → demo_reflex.rb} +4 -12
  48. metadata +65 -36
  49. data/lib/stimulus_reflex/sanity_checker.rb +0 -154
  50. data/package.json +0 -57
  51. data/stimulus_reflex.gemspec +0 -42
  52. data/tags +0 -156
  53. 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 broadcast_message(subject:, body: nil, data: {}, error: nil)
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:server-message",
31
- detail: {
32
- reflexId: data["reflexId"],
33
- stimulus_reflex: data.merge(
34
- morph: to_sym,
35
- server_message: {subject: subject, body: error&.to_s}
36
- )
37
- }
38
- )
39
- cable_ready.broadcast
40
- end
41
-
42
- # abstract method to be implemented by subclasses
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
- broadcast_message subject: "nothing", data: data
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 = selectors.is_a?(Hash) ? selectors : Hash[selectors, html]
9
- updates.each do |selector, html|
10
- html = html.to_s
11
- fragment = Nokogiri::HTML.fragment(html)
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
- stimulus_reflex: data.merge({
30
- morph: to_sym
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
- return stimulus_reflex_channel.public_send(name, *args) if stimulus_reflex_channel.respond_to?(name)
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
- options = args.extract_options!
29
- options.assert_valid_keys :if, :unless, :only, :except
30
- set_callback(*[:process, kind, args, normalize_callback_options!(options)].flatten, &block)
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
- attr_reader :attributes, :data_attributes
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
- @attributes = HashWithIndifferentAccess.new(data["attrs"] || {})
8
- @data_attributes = HashWithIndifferentAccess.new(data["dataset"] || {})
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
- def signed
15
- @signed ||= ->(accessor) { GlobalID::Locator.locate_signed(dataset[accessor]) }
19
+ all_attributes = @attrs.merge(@dataset.attrs)
20
+ super build_underscored(all_attributes)
16
21
  end
17
22
 
18
- def unsigned
19
- @unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
23
+ def attributes
24
+ @attributes ||= OpenStruct.new(build_underscored(attrs))
20
25
  end
21
26
 
22
- def dataset
23
- @dataset ||= OpenStruct.new(data_attributes.merge(data_attributes.transform_keys(&:underscore)))
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, :broadcast_message, to: :broadcaster
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
- path_params = Rails.application.routes.recognize_path_with_request(req, url, req.env[:extras] || {})
74
- path_params[:controller] = path_params[:controller].force_encoding("UTF-8")
75
- path_params[:action] = path_params[:action].force_encoding("UTF-8")
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.env.merge(ActionDispatch::Http::Parameters::PARAMETERS_KEY => path_params)
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 ||= begin
99
- controller_class.new.tap do |c|
100
- c.instance_variable_set :"@stimulus_reflex", true
101
- instance_variables.each { |name| c.instance_variable_set name, instance_variable_get(name) }
102
- c.set_request! request
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
- def dom_id(record_or_class, prefix = nil)
133
- "#" + ActionView::RecordIdentifier.dom_id(record_or_class, prefix).to_s
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