stimulus_reflex 3.4.0.pre8 → 3.5.0.pre1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +639 -463
  3. data/CODE_OF_CONDUCT.md +6 -0
  4. data/Gemfile.lock +103 -100
  5. data/LATEST +1 -0
  6. data/README.md +14 -13
  7. data/app/channels/stimulus_reflex/channel.rb +62 -67
  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 +1 -1
  12. data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +14 -0
  13. data/lib/stimulus_reflex.rb +11 -3
  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 +12 -5
  17. data/lib/stimulus_reflex/cable_ready_channels.rb +30 -6
  18. data/lib/stimulus_reflex/callbacks.rb +98 -0
  19. data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
  20. data/lib/stimulus_reflex/configuration.rb +3 -1
  21. data/lib/stimulus_reflex/element.rb +31 -7
  22. data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
  23. data/lib/stimulus_reflex/reflex.rb +57 -57
  24. data/lib/stimulus_reflex/reflex_data.rb +79 -0
  25. data/lib/stimulus_reflex/reflex_factory.rb +31 -0
  26. data/lib/stimulus_reflex/request_parameters.rb +19 -0
  27. data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +6 -8
  28. data/lib/stimulus_reflex/{sanity_checker.rb → utils/sanity_checker.rb} +65 -10
  29. data/lib/stimulus_reflex/version.rb +1 -1
  30. data/lib/tasks/stimulus_reflex/install.rake +14 -7
  31. data/test/broadcasters/broadcaster_test.rb +2 -0
  32. data/test/broadcasters/broadcaster_test_case.rb +3 -1
  33. data/test/broadcasters/nothing_broadcaster_test.rb +7 -3
  34. data/test/broadcasters/page_broadcaster_test.rb +10 -4
  35. data/test/broadcasters/selector_broadcaster_test.rb +173 -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 +7 -1
  40. data/test/test_helper.rb +10 -0
  41. data/test/tmp/app/reflexes/application_reflex.rb +1 -1
  42. data/test/tmp/app/reflexes/demo_reflex.rb +3 -2
  43. metadata +45 -36
  44. data/package.json +0 -57
  45. data/stimulus_reflex.gemspec +0 -40
  46. data/tags +0 -139
  47. data/yarn.lock +0 -4711
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,6 +20,15 @@ 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)
23
+ # Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
18
24
 
19
25
  # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
26
+
27
+ # Optimized for speed, StimulusReflex doesn't enable Rack middleware by default.
28
+ # If you are using Page Morphs and your app uses Rack middleware to rewrite part of the request path, you must enable those middleware modules in StimulusReflex.
29
+ #
30
+ # Learn more about registering Rack middleware in Rails here: https://guides.rubyonrails.org/rails_on_rack.html#configuring-middleware-stack
31
+
32
+ # config.middleware.use FirstRackMiddleware
33
+ # config.middleware.use SecondRackMiddleware
20
34
  end
@@ -1,30 +1,38 @@
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/policies/reflex_invocation_policy"
21
28
  require "stimulus_reflex/utils/colorize"
22
- require "stimulus_reflex/logger"
29
+ require "stimulus_reflex/utils/logger"
30
+ require "stimulus_reflex/utils/sanity_checker"
23
31
 
24
32
  module StimulusReflex
25
33
  class Engine < Rails::Engine
26
34
  initializer "stimulus_reflex.sanity_check" do
27
- SanityChecker.check!
35
+ SanityChecker.check! unless Rails.env.production?
28
36
  end
29
37
  end
30
38
  end
@@ -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
  })
@@ -1,12 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StimulusReflex
2
4
  class CableReadyChannels
3
- stimulus_reflex_channel_methods = CableReady::Channels.instance.operations.keys + [:broadcast, :broadcast_to]
4
- delegate(*stimulus_reflex_channel_methods, to: "@stimulus_reflex_channel")
5
- delegate :[], to: "@cable_ready_channels"
5
+ delegate :[], to: "cable_ready_channels"
6
+
7
+ def initialize(stream_name, reflex_id)
8
+ @stream_name = stream_name
9
+ @reflex_id = reflex_id
10
+ end
11
+
12
+ def cable_ready_channels
13
+ CableReady::Channels.instance
14
+ end
15
+
16
+ def stimulus_reflex_channel
17
+ CableReady::Channels.instance[@stream_name]
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
6
31
 
7
- def initialize(stream_name)
8
- @cable_ready_channels = CableReady::Channels.instance
9
- @stimulus_reflex_channel = @cable_ready_channels[stream_name]
32
+ def respond_to_missing?(name, include_all)
33
+ stimulus_reflex_channel.respond_to?(name) || super
10
34
  end
11
35
  end
12
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
@@ -14,14 +14,16 @@ module StimulusReflex
14
14
  end
15
15
 
16
16
  class Configuration
17
- attr_accessor :on_failed_sanity_checks, :parent_channel, :logging
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
26
+ @middleware = ActionDispatch::MiddlewareStack.new
25
27
  end
26
28
  end
27
29
  end
@@ -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