stimulus_reflex 3.4.0 → 3.5.0.pre2

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