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.

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