stimulus_reflex 3.4.0 → 3.5.0.pre2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +639 -484
  3. data/CODE_OF_CONDUCT.md +6 -0
  4. data/Gemfile.lock +99 -96
  5. data/LATEST +1 -0
  6. data/README.md +15 -14
  7. data/app/channels/stimulus_reflex/channel.rb +42 -73
  8. data/lib/generators/USAGE +1 -1
  9. data/lib/generators/stimulus_reflex/{config_generator.rb → initializer_generator.rb} +3 -3
  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 +16 -1
  13. data/lib/stimulus_reflex.rb +11 -2
  14. data/lib/stimulus_reflex/broadcasters/broadcaster.rb +7 -4
  15. data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +2 -2
  16. data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +20 -10
  17. data/lib/stimulus_reflex/broadcasters/update.rb +23 -0
  18. data/lib/stimulus_reflex/cable_ready_channels.rb +18 -3
  19. data/lib/stimulus_reflex/callbacks.rb +98 -0
  20. data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
  21. data/lib/stimulus_reflex/configuration.rb +3 -1
  22. data/lib/stimulus_reflex/element.rb +48 -7
  23. data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
  24. data/lib/stimulus_reflex/reflex.rb +49 -58
  25. data/lib/stimulus_reflex/reflex_data.rb +79 -0
  26. data/lib/stimulus_reflex/reflex_factory.rb +31 -0
  27. data/lib/stimulus_reflex/request_parameters.rb +19 -0
  28. data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +6 -8
  29. data/lib/stimulus_reflex/utils/sanity_checker.rb +210 -0
  30. data/lib/stimulus_reflex/version.rb +1 -1
  31. data/lib/tasks/stimulus_reflex/install.rake +54 -15
  32. data/package.json +7 -6
  33. data/stimulus_reflex.gemspec +8 -8
  34. data/test/broadcasters/broadcaster_test_case.rb +1 -1
  35. data/test/broadcasters/nothing_broadcaster_test.rb +5 -3
  36. data/test/broadcasters/page_broadcaster_test.rb +8 -4
  37. data/test/broadcasters/selector_broadcaster_test.rb +171 -55
  38. data/test/callbacks_test.rb +652 -0
  39. data/test/concern_enhancer_test.rb +54 -0
  40. data/test/element_test.rb +181 -0
  41. data/test/reflex_test.rb +2 -2
  42. data/test/test_helper.rb +22 -0
  43. data/test/tmp/app/reflexes/application_reflex.rb +10 -3
  44. data/test/tmp/app/reflexes/demo_reflex.rb +4 -2
  45. data/yarn.lock +1280 -1284
  46. metadata +47 -33
  47. data/lib/stimulus_reflex/sanity_checker.rb +0 -154
  48. 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 %>
@@ -1,12 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ApplicationReflex < StimulusReflex::Reflex
4
- # Put application-wide Reflex behavior and callbacks in this file.
4
+ # Put application-wide Reflex behavior and callbacks in this file.
5
5
  #
6
- # Example:
6
+ # Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes
7
7
  #
8
- # # If your ActionCable connection is: `identified_by :current_user`
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
+ # If you need to localize your Reflexes, you can set the I18n locale here:
12
+ #
13
+ # before_reflex do
14
+ # I18n.locale = :fr
15
+ # end
16
+ #
17
+ # For code examples, considerations and caveats, see:
18
+ # https://docs.stimulusreflex.com/rtfm/patterns#internationalization
12
19
  end
@@ -1,11 +1,26 @@
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"
@@ -15,7 +30,7 @@ StimulusReflex.configure do |config|
15
30
  # Available colors: red, green, yellow, blue, magenta, cyan, white
16
31
  # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
17
32
  # 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
33
+ # Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
19
34
 
20
35
  # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
21
36
 
@@ -1,25 +1,34 @@
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"
17
+ require "stimulus_reflex/callbacks"
18
+ require "stimulus_reflex/request_parameters"
14
19
  require "stimulus_reflex/reflex"
20
+ require "stimulus_reflex/reflex_data"
21
+ require "stimulus_reflex/reflex_factory"
15
22
  require "stimulus_reflex/element"
16
- require "stimulus_reflex/sanity_checker"
17
23
  require "stimulus_reflex/broadcasters/broadcaster"
18
24
  require "stimulus_reflex/broadcasters/nothing_broadcaster"
19
25
  require "stimulus_reflex/broadcasters/page_broadcaster"
20
26
  require "stimulus_reflex/broadcasters/selector_broadcaster"
27
+ require "stimulus_reflex/broadcasters/update"
28
+ require "stimulus_reflex/policies/reflex_invocation_policy"
21
29
  require "stimulus_reflex/utils/colorize"
22
- require "stimulus_reflex/logger"
30
+ require "stimulus_reflex/utils/logger"
31
+ require "stimulus_reflex/utils/sanity_checker"
23
32
 
24
33
  module StimulusReflex
25
34
  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
 
@@ -5,16 +5,16 @@ 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
20
  stimulus_reflex: data.merge({
@@ -22,10 +22,11 @@ module StimulusReflex
22
22
  })
23
23
  )
24
24
  else
25
- operations << [selector, :inner_html]
25
+ operations << [update.selector, :inner_html]
26
26
  cable_ready.inner_html(
27
- selector: selector,
27
+ selector: update.selector,
28
28
  html: fragment.to_html,
29
+ payload: payload,
29
30
  stimulus_reflex: data.merge({
30
31
  morph: to_sym
31
32
  })
@@ -57,5 +58,14 @@ module StimulusReflex
57
58
  def to_s
58
59
  "Selector"
59
60
  end
61
+
62
+ private
63
+
64
+ def create_update_collection(selectors, html)
65
+ updates = selectors.is_a?(Hash) ? selectors : {selectors => html}
66
+ updates.map do |key, value|
67
+ StimulusReflex::Broadcasters::Update.new(key, value, reflex)
68
+ end
69
+ end
60
70
  end
61
71
  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
@@ -2,12 +2,11 @@
2
2
 
3
3
  module StimulusReflex
4
4
  class CableReadyChannels
5
- stimulus_reflex_channel_methods = CableReady::Channels.instance.operations.keys + [:broadcast, :broadcast_to]
6
- delegate(*stimulus_reflex_channel_methods, to: "stimulus_reflex_channel")
7
5
  delegate :[], to: "cable_ready_channels"
8
6
 
9
- def initialize(stream_name)
7
+ def initialize(stream_name, reflex_id)
10
8
  @stream_name = stream_name
9
+ @reflex_id = reflex_id
11
10
  end
12
11
 
13
12
  def cable_ready_channels
@@ -17,5 +16,21 @@ module StimulusReflex
17
16
  def stimulus_reflex_channel
18
17
  CableReady::Channels.instance[@stream_name]
19
18
  end
19
+
20
+ def method_missing(name, *args)
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
29
+ super
30
+ end
31
+
32
+ def respond_to_missing?(name, include_all)
33
+ stimulus_reflex_channel.respond_to?(name) || super
34
+ end
20
35
  end
21
36
  end
@@ -0,0 +1,98 @@
1
+ require "active_support/concern"
2
+
3
+ module StimulusReflex
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ActiveSupport::Callbacks
9
+ define_callbacks :process, skip_after_callbacks_if_terminated: true
10
+ end
11
+
12
+ class_methods do
13
+ def before_reflex(*args, &block)
14
+ add_callback(:before, *args, &block)
15
+ end
16
+
17
+ def after_reflex(*args, &block)
18
+ add_callback(:after, *args, &block)
19
+ end
20
+
21
+ def around_reflex(*args, &block)
22
+ add_callback(:around, *args, &block)
23
+ end
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
+
53
+ private
54
+
55
+ def add_callback(kind, *args, &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
82
+ end
83
+
84
+ def normalize_callback_options!(options)
85
+ normalize_callback_option! options, :only, :if
86
+ normalize_callback_option! options, :except, :unless
87
+ end
88
+
89
+ def normalize_callback_option!(options, from, to)
90
+ if (from = options.delete(from))
91
+ from_set = Array(from).map(&:to_s).to_set
92
+ from = proc { |reflex| from_set.include? reflex.method_name.to_s }
93
+ options[to] = Array(options[to]).unshift(from)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ 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