stimulus_reflex 3.4.1 → 3.5.0.pre0

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 (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