stimulus_reflex 3.4.0.pre6 → 3.4.1

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.

@@ -40,8 +40,8 @@ class StimulusReflex::Channel < StimulusReflex.configuration.parent_channel.cons
40
40
  params: params,
41
41
  client_attributes: {
42
42
  reflex_id: data["reflexId"],
43
- xpath: data["xpath"],
44
- c_xpath: data["cXpath"],
43
+ xpath_controller: data["xpathController"],
44
+ xpath_element: data["xpathElement"],
45
45
  reflex_controller: data["reflexController"],
46
46
  permanent_attribute_name: permanent_attribute_name
47
47
  })
@@ -49,11 +49,33 @@ class StimulusReflex::Channel < StimulusReflex.configuration.parent_channel.cons
49
49
  rescue => invoke_error
50
50
  message = exception_message_with_backtrace(invoke_error)
51
51
  body = "Reflex #{target} failed: #{message} [#{url}]"
52
+
52
53
  if reflex
53
54
  reflex.rescue_with_handler(invoke_error)
54
55
  reflex.broadcast_message subject: "error", body: body, data: data, error: invoke_error
55
56
  else
56
57
  puts "\e[31m#{body}\e[0m"
58
+
59
+ if body.to_s.include? "No route matches"
60
+ initializer_path = Rails.root.join("config", "initializers", "stimulus_reflex.rb")
61
+
62
+ puts <<~NOTE
63
+ \e[33mNOTE: StimulusReflex failed to locate a matching route and could not re-render the page.
64
+
65
+ If your app uses Rack middleware to rewrite part of the request path, you must enable those middleware modules in StimulusReflex.
66
+ The StimulusReflex initializer should be located at #{initializer_path}, or you can generate it with:
67
+
68
+ $ bundle exec rails generate stimulus_reflex:config
69
+
70
+ Configure any required middleware:
71
+
72
+ StimulusReflex.configure do |config|
73
+ config.middleware.use FirstRackMiddleware
74
+ config.middleware.use SecondRackMiddleware
75
+ end\e[0m
76
+
77
+ NOTE
78
+ end
57
79
  end
58
80
  return
59
81
  end
@@ -16,6 +16,7 @@ class <%= class_name %>Reflex < ApplicationReflex
16
16
  # - signed - use a signed Global ID to map dataset attribute to a model eg. element.signed[:foo]
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
+ # - reflex_id - a UUIDv4 that uniquely identies each Reflex
19
20
  #
20
21
  # Example:
21
22
  #
@@ -12,9 +12,18 @@ StimulusReflex.configure do |config|
12
12
 
13
13
  # Customize server-side Reflex logging format, with optional colorization:
14
14
  # Available tokens: session_id, session_id_full, reflex_info, operation, reflex_id, reflex_id_full, mode, selector, operation_counter, connection_id, connection_id_full, timestamp
15
- # Available colors: green, yellow, blue, magenta, cyan, white
15
+ # Available colors: red, green, yellow, blue, magenta, cyan, white
16
16
  # You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
17
17
  # 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
18
19
 
19
20
  # config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
21
+
22
+ # Optimized for speed, StimulusReflex doesn't enable Rack middleware by default.
23
+ # 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.
24
+ #
25
+ # Learn more about registering Rack middleware in Rails here: https://guides.rubyonrails.org/rails_on_rack.html#configuring-middleware-stack
26
+
27
+ # config.middleware.use FirstRackMiddleware
28
+ # config.middleware.use SecondRackMiddleware
20
29
  end
@@ -11,6 +11,7 @@ require "cable_ready"
11
11
  require "stimulus_reflex/version"
12
12
  require "stimulus_reflex/cable_ready_channels"
13
13
  require "stimulus_reflex/configuration"
14
+ require "stimulus_reflex/callbacks"
14
15
  require "stimulus_reflex/reflex"
15
16
  require "stimulus_reflex/element"
16
17
  require "stimulus_reflex/sanity_checker"
@@ -24,7 +25,7 @@ require "stimulus_reflex/logger"
24
25
  module StimulusReflex
25
26
  class Engine < Rails::Engine
26
27
  initializer "stimulus_reflex.sanity_check" do
27
- SanityChecker.check!
28
+ SanityChecker.check! unless Rails.env.production?
28
29
  end
29
30
  end
30
31
  end
@@ -23,6 +23,7 @@ module StimulusReflex
23
23
  })
24
24
  )
25
25
  end
26
+
26
27
  cable_ready.broadcast
27
28
  end
28
29
 
@@ -1,18 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StimulusReflex
2
4
  class CableReadyChannels
5
+ delegate :[], to: "cable_ready_channels"
6
+
3
7
  def initialize(stream_name)
4
- @cable_ready_channels = CableReady::Channels.instance
5
- @stimulus_reflex_channel = @cable_ready_channels[stream_name]
8
+ @stream_name = stream_name
9
+ end
10
+
11
+ def cable_ready_channels
12
+ CableReady::Channels.instance
13
+ end
14
+
15
+ def stimulus_reflex_channel
16
+ CableReady::Channels.instance[@stream_name]
6
17
  end
7
18
 
8
19
  def method_missing(name, *args)
9
- return @stimulus_reflex_channel.send(name, *args) if @stimulus_reflex_channel.respond_to?(name)
10
- @cable_ready_channels.send(name, *args)
20
+ return stimulus_reflex_channel.public_send(name, *args) if stimulus_reflex_channel.respond_to?(name)
21
+ super
11
22
  end
12
23
 
13
24
  def respond_to_missing?(name, include_all)
14
- @stimulus_reflex_channel.respond_to?(name, include_all) ||
15
- @cable_ready_channels.respond_to?(name, include_all)
25
+ stimulus_reflex_channel.respond_to?(name) || super
16
26
  end
17
27
  end
18
28
  end
@@ -0,0 +1,48 @@
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
+ private
26
+
27
+ 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)
31
+ end
32
+
33
+ def normalize_callback_options!(options)
34
+ normalize_callback_option! options, :only, :if
35
+ normalize_callback_option! options, :except, :unless
36
+ options
37
+ end
38
+
39
+ def normalize_callback_option!(options, from, to)
40
+ if (from = options.delete(from))
41
+ from_set = Array(from).map(&:to_s).to_set
42
+ from = proc { |reflex| from_set.include? reflex.method_name }
43
+ options[to] = Array(options[to]).unshift(from)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,7 +14,7 @@ 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, :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
 
@@ -22,6 +22,7 @@ module StimulusReflex
22
22
  @on_failed_sanity_checks = :exit
23
23
  @parent_channel = "ApplicationCable::Channel"
24
24
  @logging = DEFAULT_LOGGING
25
+ @middleware = ActionDispatch::MiddlewareStack.new
25
26
  end
26
27
  end
27
28
  end
@@ -79,22 +79,22 @@ module StimulusReflex
79
79
  Time.now.strftime("%Y-%m-%d %H:%M:%S")
80
80
  end
81
81
 
82
- def method_missing method
83
- return send(method.to_sym) if private_instance_methods.include?(method.to_sym)
82
+ def method_missing(name, *args)
83
+ return send(name) if private_instance_methods.include?(name.to_sym)
84
84
 
85
85
  reflex.connection.identifiers.each do |identifier|
86
86
  ident = reflex.connection.send(identifier)
87
- return ident.send(method) if ident.respond_to?(:attributes) && ident.attributes.key?(method.to_s)
87
+ return ident.send(name) if ident.respond_to?(:attributes) && ident.attributes.key?(name.to_s)
88
88
  end
89
89
  "-"
90
90
  end
91
91
 
92
- def respond_to_missing? method
93
- return true if private_instance_methods.include?(method.to_sym)
92
+ def respond_to_missing?(name, include_all)
93
+ return true if private_instance_methods.include?(name.to_sym)
94
94
 
95
95
  reflex.connection.identifiers.each do |identifier|
96
96
  ident = reflex.connection.send(identifier)
97
- return true if ident.respond_to?(:attributes) && ident.attributes.key?(method.to_s)
97
+ return true if ident.respond_to?(:attributes) && ident.attributes.key?(name.to_s)
98
98
  end
99
99
  false
100
100
  end
@@ -1,59 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ClientAttributes = Struct.new(:reflex_id, :reflex_controller, :xpath, :c_xpath, :permanent_attribute_name, keyword_init: true)
3
+ ClientAttributes = Struct.new(:reflex_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, keyword_init: true)
4
4
 
5
5
  class StimulusReflex::Reflex
6
6
  include ActiveSupport::Rescuable
7
- include ActiveSupport::Callbacks
8
-
9
- define_callbacks :process, skip_after_callbacks_if_terminated: true
10
-
11
- class << self
12
- def before_reflex(*args, &block)
13
- add_callback(:before, *args, &block)
14
- end
15
-
16
- def after_reflex(*args, &block)
17
- add_callback(:after, *args, &block)
18
- end
19
-
20
- def around_reflex(*args, &block)
21
- add_callback(:around, *args, &block)
22
- end
23
-
24
- private
25
-
26
- def add_callback(kind, *args, &block)
27
- options = args.extract_options!
28
- options.assert_valid_keys :if, :unless, :only, :except
29
- set_callback(*[:process, kind, args, normalize_callback_options!(options)].flatten, &block)
30
- end
31
-
32
- def normalize_callback_options!(options)
33
- normalize_callback_option! options, :only, :if
34
- normalize_callback_option! options, :except, :unless
35
- options
36
- end
37
-
38
- def normalize_callback_option!(options, from, to)
39
- if (from = options.delete(from))
40
- from_set = Array(from).map(&:to_s).to_set
41
- from = proc { |reflex| from_set.include? reflex.method_name }
42
- options[to] = Array(options[to]).unshift(from)
43
- end
44
- end
45
- end
7
+ include StimulusReflex::Callbacks
46
8
 
47
9
  attr_reader :cable_ready, :channel, :url, :element, :selectors, :method_name, :broadcaster, :client_attributes, :logger
48
10
 
49
11
  alias_method :action_name, :method_name # for compatibility with controller libraries like Pundit that expect an action name
50
12
 
51
13
  delegate :connection, :stream_name, to: :channel
52
- delegate :flash, :session, to: :request
14
+ delegate :controller_class, :flash, :session, to: :request
53
15
  delegate :broadcast, :broadcast_message, to: :broadcaster
54
- delegate :reflex_id, :reflex_controller, :xpath, :c_xpath, :permanent_attribute_name, to: :client_attributes
16
+ delegate :reflex_id, :reflex_controller, :xpath_controller, :xpath_element, :permanent_attribute_name, to: :client_attributes
17
+ delegate :render, to: :controller_class
55
18
 
56
19
  def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, params: {}, client_attributes: {})
20
+ if is_a? CableReady::Broadcaster
21
+ message = <<~MSG
22
+
23
+ #{self.class.name} includes CableReady::Broadcaster, and you need to remove it.
24
+ Reflexes have their own CableReady interface. You can just assume that it's present.
25
+ See https://docs.stimulusreflex.com/rtfm/cableready#using-cableready-inside-a-reflex-action for more details.
26
+
27
+ MSG
28
+ raise TypeError.new(message.strip)
29
+ end
30
+
57
31
  @channel = channel
58
32
  @url = url
59
33
  @element = element
@@ -86,6 +60,14 @@ class StimulusReflex::Reflex
86
60
  )
87
61
 
88
62
  env = connection.env.merge(mock_env)
63
+
64
+ middleware = StimulusReflex.config.middleware
65
+
66
+ if middleware.any?
67
+ stack = middleware.build(Rails.application.routes)
68
+ stack.call(env)
69
+ end
70
+
89
71
  req = ActionDispatch::Request.new(env)
90
72
 
91
73
  path_params = Rails.application.routes.recognize_path_with_request(req, url, req.env[:extras] || {})
@@ -114,11 +96,11 @@ class StimulusReflex::Reflex
114
96
 
115
97
  def controller
116
98
  @controller ||= begin
117
- request.controller_class.new.tap do |c|
99
+ controller_class.new.tap do |c|
118
100
  c.instance_variable_set :"@stimulus_reflex", true
119
101
  instance_variables.each { |name| c.instance_variable_set name, instance_variable_get(name) }
120
- c.request = request
121
- c.response = ActionDispatch::Response.new
102
+ c.set_request! request
103
+ c.set_response! controller_class.make_response!(request)
122
104
  end
123
105
  end
124
106
  end
@@ -146,4 +128,8 @@ class StimulusReflex::Reflex
146
128
  def params
147
129
  @_params ||= ActionController::Parameters.new(request.parameters)
148
130
  end
131
+
132
+ def dom_id(record_or_class, prefix = nil)
133
+ "#" + ActionView::RecordIdentifier.dom_id(record_or_class, prefix).to_s
134
+ end
149
135
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StimulusReflex::SanityChecker
4
+ NODE_VERSION_FORMAT = /(\d+\.\d+\.\d+.*):/
4
5
  JSON_VERSION_FORMAT = /(\d+\.\d+\.\d+.*)"/
5
6
 
6
7
  class << self
@@ -86,6 +87,8 @@ class StimulusReflex::SanityChecker
86
87
  def find_javascript_package_version
87
88
  if (match = search_file(package_json_path, regex: /version/))
88
89
  match[JSON_VERSION_FORMAT, 1]
90
+ elsif (match = search_file(yarn_lock_path, regex: /^stimulus_reflex/))
91
+ match[NODE_VERSION_FORMAT, 1]
89
92
  end
90
93
  end
91
94
 
@@ -98,6 +101,10 @@ class StimulusReflex::SanityChecker
98
101
  Rails.root.join("node_modules", "stimulus_reflex", "package.json")
99
102
  end
100
103
 
104
+ def yarn_lock_path
105
+ Rails.root.join("yarn.lock")
106
+ end
107
+
101
108
  def initializer_path
102
109
  @_initializer_path ||= Rails.root.join("config", "initializers", "stimulus_reflex.rb")
103
110
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusReflex
4
- VERSION = "3.4.0.pre6"
4
+ VERSION = "3.4.1"
5
5
  end
@@ -6,9 +6,10 @@ require "stimulus_reflex/version"
6
6
  namespace :stimulus_reflex do
7
7
  desc "Install StimulusReflex in this application"
8
8
  task install: :environment do
9
- system "bundle exec rails webpacker:install:stimulus"
9
+ system "rails dev:cache" unless Rails.root.join("tmp", "caching-dev.txt").exist?
10
10
  gem_version = StimulusReflex::VERSION.gsub(".pre", "-pre")
11
11
  system "yarn add stimulus_reflex@#{gem_version}"
12
+ system "bundle exec rails webpacker:install:stimulus"
12
13
  main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
13
14
 
14
15
  FileUtils.mkdir_p Rails.root.join("#{main_folder}/controllers"), verbose: true
@@ -39,7 +40,7 @@ namespace :stimulus_reflex do
39
40
 
40
41
  unless lines.find { |line| line.start_with?("import controller") }
41
42
  matches = lines.select { |line| line =~ /\A(require|import)/ }
42
- lines.insert lines.index(matches.last).to_i + 1, "import controller from './application_controller'\n"
43
+ lines.insert lines.index(matches.last).to_i + 1, "import controller from '../controllers/application_controller'\n"
43
44
  end
44
45
 
45
46
  initialize_line = lines.find { |line| line.start_with?("StimulusReflex.initialize") }
@@ -67,6 +68,10 @@ namespace :stimulus_reflex do
67
68
  system "bundle exec rails generate stimulus_reflex example"
68
69
  puts "Generating default StimulusReflex configuration file into your application config/initializers directory"
69
70
  system "bundle exec rails generate stimulus_reflex:config"
70
- system "rails dev:cache" unless Rails.root.join("tmp", "caching-dev.txt").exist?
71
+
72
+ puts
73
+ puts "StimulusReflex and CableReady have been successfully installed!"
74
+ puts "Go to https://docs.stimulusreflex.com/quickstart if you need help getting started."
75
+ puts
71
76
  end
72
77
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stimulus_reflex",
3
- "version": "3.4.0-pre5",
3
+ "version": "3.4.0",
4
4
  "description": "Build reactive applications with the Rails tooling you already know and love.",
5
5
  "keywords": [
6
6
  "ruby",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@rails/actioncable": ">= 6.0",
44
- "cable_ready": ">= 4.4"
44
+ "cable_ready": ">= 4.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@babel/core": "^7.6.2",
@@ -52,6 +52,6 @@
52
52
  "jsdom": "^16.0.1",
53
53
  "mocha": "^8.0.1",
54
54
  "prettier-standard": "^16.1.0",
55
- "stimulus": "^1.1.1"
55
+ "stimulus": ">= 1.1"
56
56
  }
57
57
  }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path("../lib/stimulus_reflex/version", __FILE__)
2
4
 
3
5
  Gem::Specification.new do |gem|
@@ -30,7 +32,7 @@ Gem::Specification.new do |gem|
30
32
  gem.add_dependency "nokogiri"
31
33
  gem.add_dependency "rails", ">= 5.2"
32
34
  gem.add_dependency "redis"
33
- gem.add_dependency "cable_ready", ">= 4.4"
35
+ gem.add_dependency "cable_ready", ">= 4.5"
34
36
 
35
37
  gem.add_development_dependency "bundler", "~> 2.0"
36
38
  gem.add_development_dependency "pry-nav"