stimulus_reflex 3.4.1 → 3.5.0.pre0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +587 -495
  3. data/Gemfile.lock +82 -86
  4. data/LATEST +1 -0
  5. data/README.md +10 -10
  6. data/app/channels/stimulus_reflex/channel.rb +40 -67
  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/templates/app/reflexes/%file_name%_reflex.rb.tt +3 -2
  10. data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +1 -1
  11. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +6 -1
  12. data/lib/stimulus_reflex.rb +9 -2
  13. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +7 -4
  14. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +2 -2
  15. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +12 -5
  16. data/lib/stimulus_reflex/cable_ready_channels.rb +6 -2
  17. data/lib/stimulus_reflex/callbacks.rb +55 -5
  18. data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
  19. data/lib/stimulus_reflex/configuration.rb +2 -1
  20. data/lib/stimulus_reflex/element.rb +31 -7
  21. data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
  22. data/lib/stimulus_reflex/reflex.rb +35 -20
  23. data/lib/stimulus_reflex/reflex_data.rb +79 -0
  24. data/lib/stimulus_reflex/reflex_factory.rb +31 -0
  25. data/lib/stimulus_reflex/request_parameters.rb +19 -0
  26. data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +0 -2
  27. data/lib/stimulus_reflex/{sanity_checker.rb → utils/sanity_checker.rb} +58 -10
  28. data/lib/stimulus_reflex/version.rb +1 -1
  29. data/lib/tasks/stimulus_reflex/install.rake +6 -4
  30. data/package.json +6 -5
  31. data/stimulus_reflex.gemspec +5 -5
  32. data/test/broadcasters/broadcaster_test_case.rb +1 -1
  33. data/test/broadcasters/nothing_broadcaster_test.rb +5 -3
  34. data/test/broadcasters/page_broadcaster_test.rb +8 -4
  35. data/test/broadcasters/selector_broadcaster_test.rb +171 -55
  36. data/test/callbacks_test.rb +652 -0
  37. data/test/concern_enhancer_test.rb +54 -0
  38. data/test/element_test.rb +181 -0
  39. data/test/reflex_test.rb +1 -1
  40. data/test/test_helper.rb +4 -0
  41. data/test/tmp/app/reflexes/application_reflex.rb +2 -2
  42. data/test/tmp/app/reflexes/user_reflex.rb +3 -2
  43. data/yarn.lock +1138 -919
  44. metadata +39 -28
  45. data/tags +0 -156
data/lib/generators/USAGE CHANGED
@@ -11,4 +11,4 @@ Example:
11
11
  app/reflexes/application_reflex.rb
12
12
  app/reflexes/user_reflex.rb
13
13
 
14
- Don't forget to setup the application: https://docs.stimulusreflex.com/setup
14
+ Don't forget to setup the application: https://docs.stimulusreflex.com/hello-world/setup
@@ -3,11 +3,11 @@
3
3
  require "rails/generators"
4
4
 
5
5
  module StimulusReflex
6
- class ConfigGenerator < Rails::Generators::Base
7
- desc "Creates an StimulusReflex configuration file in config/initializers"
6
+ class InitializerGenerator < Rails::Generators::Base
7
+ desc "Creates a StimulusReflex initializer in config/initializers"
8
8
  source_root File.expand_path("templates", __dir__)
9
9
 
10
- def copy_config_file
10
+ def copy_initializer_file
11
11
  copy_file "config/initializers/stimulus_reflex.rb"
12
12
  end
13
13
  end
@@ -17,12 +17,13 @@ class <%= class_name %>Reflex < ApplicationReflex
17
17
  # - unsigned - use an unsigned Global ID to map dataset attribute to a model eg. element.unsigned[:foo]
18
18
  # - cable_ready - a special cable_ready that can broadcast to the current visitor (no brackets needed)
19
19
  # - reflex_id - a UUIDv4 that uniquely identies each Reflex
20
+ # - tab_id - a UUIDv4 that uniquely identifies the browser tab
20
21
  #
21
22
  # Example:
22
23
  #
23
24
  # before_reflex do
24
25
  # # throw :abort # this will prevent the Reflex from continuing
25
- # # learn more about callbacks at https://docs.stimulusreflex.com/lifecycle
26
+ # # learn more about callbacks at https://docs.stimulusreflex.com/rtfm/lifecycle
26
27
  # end
27
28
  #
28
29
  # def example(argument=true)
@@ -30,7 +31,7 @@ class <%= class_name %>Reflex < ApplicationReflex
30
31
  # # Any declared instance variables will be made available to the Rails controller and view.
31
32
  # end
32
33
  #
33
- # Learn more at: https://docs.stimulusreflex.com/reflexes#reflex-classes
34
+ # Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes
34
35
 
35
36
  <% actions.each do |action| -%>
36
37
  def <%= action %>
@@ -8,5 +8,5 @@ class ApplicationReflex < StimulusReflex::Reflex
8
8
  # # If your ActionCable connection is: `identified_by :current_user`
9
9
  # delegate :current_user, to: :connection
10
10
  #
11
- # Learn more at: https://docs.stimulusreflex.com/reflexes#reflex-classes
11
+ # Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes
12
12
  end
@@ -6,6 +6,11 @@ StimulusReflex.configure do |config|
6
6
 
7
7
  # config.on_failed_sanity_checks = :exit
8
8
 
9
+ # Enable/disable exiting / warning when there's a new StimulusReflex release
10
+ # `:exit` or `:warn` or `:ignore`
11
+
12
+ # config.on_new_version_available = :ignore
13
+
9
14
  # Override the parent class that the StimulusReflex ActionCable channel inherits from
10
15
 
11
16
  # config.parent_channel = "ApplicationCable::Channel"
@@ -15,7 +20,7 @@ StimulusReflex.configure do |config|
15
20
  # Available colors: red, green, yellow, blue, magenta, cyan, white
16
21
  # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
17
22
  # 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
23
+ # Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
19
24
 
20
25
  # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
21
26
 
@@ -1,26 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
+ require "open-uri"
4
5
  require "rack"
5
6
  require "rails/engine"
6
7
  require "active_support/all"
7
8
  require "action_dispatch"
8
9
  require "action_cable"
10
+ require "action_view"
9
11
  require "nokogiri"
10
12
  require "cable_ready"
11
13
  require "stimulus_reflex/version"
12
14
  require "stimulus_reflex/cable_ready_channels"
15
+ require "stimulus_reflex/concern_enhancer"
13
16
  require "stimulus_reflex/configuration"
14
17
  require "stimulus_reflex/callbacks"
18
+ require "stimulus_reflex/request_parameters"
15
19
  require "stimulus_reflex/reflex"
20
+ require "stimulus_reflex/reflex_data"
21
+ require "stimulus_reflex/reflex_factory"
16
22
  require "stimulus_reflex/element"
17
- require "stimulus_reflex/sanity_checker"
18
23
  require "stimulus_reflex/broadcasters/broadcaster"
19
24
  require "stimulus_reflex/broadcasters/nothing_broadcaster"
20
25
  require "stimulus_reflex/broadcasters/page_broadcaster"
21
26
  require "stimulus_reflex/broadcasters/selector_broadcaster"
27
+ require "stimulus_reflex/policies/reflex_invocation_policy"
22
28
  require "stimulus_reflex/utils/colorize"
23
- require "stimulus_reflex/logger"
29
+ require "stimulus_reflex/utils/logger"
30
+ require "stimulus_reflex/utils/sanity_checker"
24
31
 
25
32
  module StimulusReflex
26
33
  class Engine < Rails::Engine
@@ -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,13 +26,13 @@ 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 broadcast_message(subject:, data: {}, error: nil)
28
30
  operations << ["document", :dispatch_event]
29
31
  cable_ready.dispatch_event(
30
32
  name: "stimulus-reflex:server-message",
31
33
  detail: {
32
- reflexId: data["reflexId"],
34
+ reflexId: data.delete("reflexId"),
35
+ payload: payload,
33
36
  stimulus_reflex: data.merge(
34
37
  morph: to_sym,
35
38
  server_message: {subject: subject, body: error&.to_s}
@@ -12,10 +12,11 @@ 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
22
  stimulus_reflex: data.merge({
@@ -23,7 +24,6 @@ module StimulusReflex
23
24
  })
24
25
  )
25
26
  end
26
-
27
27
  cable_ready.broadcast
28
28
  end
29
29
 
@@ -2,19 +2,25 @@
2
2
 
3
3
  module StimulusReflex
4
4
  class SelectorBroadcaster < Broadcaster
5
+ include CableReady::Identifiable
6
+
5
7
  def broadcast(_, data = {})
6
8
  morphs.each do |morph|
7
9
  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)
10
+ updates = selectors.is_a?(Hash) ? selectors : {selectors => html}
11
+ updates.each do |key, value|
12
+ html = reflex.render(key) if key.is_a?(ActiveRecord::Base) && value.nil?
13
+ html = reflex.render_collection(key) if key.is_a?(ActiveRecord::Relation) && value.nil?
14
+ html ||= value
15
+ fragment = Nokogiri::HTML.fragment(html.to_s)
16
+ selector = key.is_a?(ActiveRecord::Base) || key.is_a?(ActiveRecord::Relation) ? dom_id(key) : key.to_s
12
17
  match = fragment.at_css(selector)
13
18
  if match.present?
14
19
  operations << [selector, :morph]
15
20
  cable_ready.morph(
16
21
  selector: selector,
17
- html: match.inner_html,
22
+ html: match.inner_html(save_with: Broadcaster::DEFAULT_HTML_WITHOUT_FORMAT),
23
+ payload: payload,
18
24
  children_only: true,
19
25
  permanent_attribute_name: permanent_attribute_name,
20
26
  stimulus_reflex: data.merge({
@@ -26,6 +32,7 @@ module StimulusReflex
26
32
  cable_ready.inner_html(
27
33
  selector: selector,
28
34
  html: fragment.to_html,
35
+ payload: payload,
29
36
  stimulus_reflex: data.merge({
30
37
  morph: to_sym
31
38
  })
@@ -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,10 @@ 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
+ args[0][:reflex_id] = @reflex_id if args.any?
23
+ return stimulus_reflex_channel.public_send(name, *args)
24
+ end
21
25
  super
22
26
  end
23
27
 
@@ -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,12 +14,13 @@ 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, :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
23
24
  @parent_channel = "ApplicationCable::Channel"
24
25
  @logging = DEFAULT_LOGGING
25
26
  @middleware = ActionDispatch::MiddlewareStack.new
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StimulusReflex::Element < OpenStruct
4
- attr_reader :attributes, :data_attributes
4
+ attr_reader :attrs, :data_attrs
5
5
 
6
6
  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-" }
7
+ @attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
8
+ datasets = data["dataset"] || {}
9
+ regular_dataset = datasets["dataset"] || {}
10
+ @data_attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
11
+ all_attributes = @attrs.merge(@data_attrs)
12
+ super build_underscored(all_attributes)
13
+ @data_attrs.transform_keys! { |key| key.delete_prefix "data-" }
12
14
  end
13
15
 
14
16
  def signed
@@ -19,7 +21,29 @@ class StimulusReflex::Element < OpenStruct
19
21
  @unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
20
22
  end
21
23
 
24
+ def attributes
25
+ @attributes ||= OpenStruct.new(build_underscored(attrs))
26
+ end
27
+
22
28
  def dataset
23
- @dataset ||= OpenStruct.new(data_attributes.merge(data_attributes.transform_keys(&:underscore)))
29
+ @dataset ||= OpenStruct.new(build_underscored(data_attrs))
30
+ end
31
+
32
+ alias_method :data_attributes, :dataset
33
+
34
+ private
35
+
36
+ def build_data_attrs(dataset, dataset_all)
37
+ dataset_all.transform_keys! { |key| "data-#{key.delete_prefix("data-").pluralize}" }
38
+
39
+ dataset.each { |key, value| dataset_all[key]&.prepend(value) }
40
+
41
+ data_attrs = dataset.merge(dataset_all)
42
+
43
+ HashWithIndifferentAccess.new(data_attrs || {})
44
+ end
45
+
46
+ def build_underscored(attrs)
47
+ attrs.merge(attrs.transform_keys(&:underscore))
24
48
  end
25
49
  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