stimulus_reflex 3.3.0.pre0 → 3.3.0.pre5

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.

@@ -1,56 +1,88 @@
1
1
  import ApplicationController from './application_controller'
2
2
 
3
- /* This is the custom StimulusReflex controller for <%= class_name %>Reflex.
3
+ /* This is the custom StimulusReflex controller for the <%= class_name %> Reflex.
4
4
  * Learn more at: https://docs.stimulusreflex.com
5
5
  */
6
6
  export default class extends ApplicationController {
7
+ /*
8
+ * Regular Stimulus lifecycle methods
9
+ * Learn more at: https://stimulusjs.org/reference/lifecycle-callbacks
10
+ *
11
+ * If you intend to use this controller as a regular stimulus controller as well,
12
+ * make sure any Stimulus lifecycle methods overridden in ApplicationController call super.
13
+ *
14
+ * Important:
15
+ * By default, StimulusReflex overrides the -connect- method so make sure you
16
+ * call super if you intend to do anything else when this controller connects.
17
+ */
18
+
19
+ connect () {
20
+ super.connect()
21
+ // add your code here, if applicable
22
+ }
23
+
7
24
  /* Reflex specific lifecycle methods.
8
- * Use methods similar to this example to handle lifecycle concerns for a specific Reflex method.
9
- * Using the lifecycle is optional, so feel free to delete these stubs if you don't need them.
25
+ *
26
+ * For every method defined in your Reflex class, a matching set of lifecycle methods become available
27
+ * in this javascript controller. These are optional, so feel free to delete these stubs if you don't
28
+ * need them.
29
+ *
30
+ * Important:
31
+ * Make sure to add data-controller="<%= class_name.underscore.dasherize %>" to your markup alongside
32
+ * data-reflex="<%= class_name %>#dance" for the lifecycle methods to fire properly.
10
33
  *
11
34
  * Example:
12
35
  *
13
- * <a href="#" data-reflex="<%= class_name %>Reflex#example">Example</a>
36
+ * <a href="#" data-reflex="click-><%= class_name %>#dance" data-controller="<%= class_name.underscore.dasherize %>">Dance!</a>
14
37
  *
15
38
  * Arguments:
16
39
  *
17
40
  * element - the element that triggered the reflex
18
41
  * may be different than the Stimulus controller's this.element
19
42
  *
20
- * reflex - the name of the reflex e.g. "<%= class_name %>Reflex#example"
43
+ * reflex - the name of the reflex e.g. "<%= class_name %>#dance"
44
+ *
45
+ * error/noop - the error message (for reflexError), otherwise null
21
46
  *
22
- * error - error message from the server
47
+ * reflexId - a UUID4 or developer-provided unique identifier for each Reflex
23
48
  */
24
49
 
25
50
  <% if actions.empty? -%>
26
- // beforeUpdate(element, reflex) {
27
- // element.innerText = 'Updating...'
51
+ // Assuming you create a "<%= class_name %>#dance" action in your Reflex class
52
+ // you'll be able to use the following lifecycle methods:
53
+
54
+ // beforeDance(element, reflex, noop, reflexId) {
55
+ // element.innerText = 'Putting dance shoes on...'
28
56
  // }
29
57
 
30
- // updateSuccess(element, reflex) {
31
- // element.innerText = 'Updated Successfully.'
58
+ // danceSuccess(element, reflex, noop, reflexId) {
59
+ // element.innerText = 'Danced like no one was watching! Was someone watching?'
32
60
  // }
33
61
 
34
- // updateError(element, reflex, error) {
35
- // console.error('updateError', error);
36
- // element.innerText = 'Update Failed!'
62
+ // danceError(element, reflex, error, reflexId) {
63
+ // console.error('danceError', error);
64
+ // element.innerText = "Couldn't dance!"
37
65
  // }
38
66
  <% end -%>
39
67
  <% actions.each do |action| -%>
40
- // <%= "before_#{action}".camelize(:lower) %>(element, reflex) {
41
- // console.log("before <%= action %>", element, reflex)
68
+ // <%= "before_#{action}".camelize(:lower) %>(element, reflex, noop, reflexId) {
69
+ // console.log("before <%= action %>", element, reflex, reflexId)
70
+ // }
71
+
72
+ // <%= "#{action}_success".camelize(:lower) %>(element, reflex, noop, reflexId) {
73
+ // console.log("<%= action %> success", element, reflex, reflexId)
42
74
  // }
43
75
 
44
- // <%= "#{action}_success".camelize(:lower) %>(element, reflex) {
45
- // console.log("<%= action %> success", element, reflex)
76
+ // <%= "#{action}_error".camelize(:lower) %>(element, reflex, error, reflexId) {
77
+ // console.error("<%= action %> error", element, reflex, error, reflexId)
46
78
  // }
47
79
 
48
- // <%= "#{action}_error".camelize(:lower) %>(element, reflex, error) {
49
- // console.error("<%= action %> error", element, reflex, error)
80
+ // <%= "#{action}_halted".camelize(:lower) %>(element, reflex, noop, reflexId) {
81
+ // console.warn("<%= action %> halted", element, reflex, reflexId)
50
82
  // }
51
83
 
52
- // <%= "after_#{action}".camelize(:lower) %>(element, reflex, error) {
53
- // console.log("after <%= action %>", element, reflex, error)
84
+ // <%= "after_#{action}".camelize(:lower) %>(element, reflex, noop, reflexId) {
85
+ // console.log("after <%= action %>", element, reflex, reflexId)
54
86
  // }
55
87
  <%= "\n" unless action == actions.last -%>
56
88
  <% end -%>
@@ -1,7 +1,7 @@
1
1
  import { Controller } from 'stimulus'
2
2
  import StimulusReflex from 'stimulus_reflex'
3
3
 
4
- /* This is your application's ApplicationController.
4
+ /* This is your ApplicationController.
5
5
  * All StimulusReflex controllers should inherit from this class.
6
6
  *
7
7
  * Example:
@@ -17,7 +17,8 @@ export default class extends Controller {
17
17
  StimulusReflex.register(this)
18
18
  }
19
19
 
20
- /* Application wide lifecycle methods.
20
+ /* Application-wide lifecycle methods
21
+ *
21
22
  * Use these methods to handle lifecycle concerns for the entire application.
22
23
  * Using the lifecycle is optional, so feel free to delete these stubs if you don't need them.
23
24
  *
@@ -26,24 +27,26 @@ export default class extends Controller {
26
27
  * element - the element that triggered the reflex
27
28
  * may be different than the Stimulus controller's this.element
28
29
  *
29
- * reflex - the name of the reflex e.g. "ExampleReflex#demo"
30
+ * reflex - the name of the reflex e.g. "Example#demo"
31
+ *
32
+ * error/noop - the error message (for reflexError), otherwise null
30
33
  *
31
- * error - error message from the server
34
+ * reflexId - a UUID4 or developer-provided unique identifier for each Reflex
32
35
  */
33
36
 
34
- beforeReflex (element, reflex) {
37
+ beforeReflex (element, reflex, noop, reflexId) {
35
38
  // document.body.classList.add('wait')
36
39
  }
37
40
 
38
- reflexSuccess (element, reflex, error) {
41
+ reflexSuccess (element, reflex, noop, reflexId) {
39
42
  // show success message etc...
40
43
  }
41
44
 
42
- reflexError (element, reflex, error) {
45
+ reflexError (element, reflex, error, reflexId) {
43
46
  // show error message etc...
44
47
  }
45
48
 
46
- afterReflex (element, reflex) {
49
+ afterReflex (element, reflex, noop, reflexId) {
47
50
  // document.body.classList.remove('wait')
48
51
  }
49
52
  }
@@ -11,15 +11,77 @@ require "cable_ready"
11
11
  require "stimulus_reflex/version"
12
12
  require "stimulus_reflex/reflex"
13
13
  require "stimulus_reflex/element"
14
- require "stimulus_reflex/broadcaster"
15
- require "stimulus_reflex/morph_mode"
16
14
  require "stimulus_reflex/channel"
17
- require "stimulus_reflex/morph_mode/nothing_morph_mode"
18
- require "stimulus_reflex/morph_mode/page_morph_mode"
19
- require "stimulus_reflex/morph_mode/selector_morph_mode"
15
+ require "stimulus_reflex/broadcasters/broadcaster"
16
+ require "stimulus_reflex/broadcasters/nothing_broadcaster"
17
+ require "stimulus_reflex/broadcasters/page_broadcaster"
18
+ require "stimulus_reflex/broadcasters/selector_broadcaster"
20
19
  require "generators/stimulus_reflex_generator"
21
20
 
22
21
  module StimulusReflex
23
22
  class Engine < Rails::Engine
23
+ NODE_VERSION_FORMAT = /(\d\.\d\.\d.*):/
24
+ JSON_VERSION_FORMAT = /(\d\.\d\.\d.*)\"/
25
+
26
+ initializer "stimulus_reflex.verify_caching_enabled" do
27
+ unless caching_enabled?
28
+ puts <<~WARN
29
+ Stimulus Reflex requires caching to be enabled. Caching allows the session to be modified during ActionCable requests.
30
+ To enable caching in development, run:
31
+
32
+ rails dev:cache
33
+ WARN
34
+ exit
35
+ end
36
+ end
37
+
38
+ initializer "stimulus_reflex.verify_npm_package_version" do
39
+ unless node_version_matches?
40
+ puts <<~WARN
41
+ The Stimulus Reflex javascript package version (#{node_package_version}) does not match the Rubygem version (#{gem_version}).
42
+ To update the Stimulus Reflex node module:
43
+
44
+ yarn upgrade stimulus_reflex@#{gem_version}
45
+ WARN
46
+ exit
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def caching_enabled?
53
+ Rails.application.config.action_controller.perform_caching &&
54
+ Rails.application.config.cache_store != :null_store
55
+ end
56
+
57
+ def node_version_matches?
58
+ node_package_version == gem_version
59
+ end
60
+
61
+ def gem_version
62
+ StimulusReflex::VERSION.gsub(".pre", "-pre")
63
+ end
64
+
65
+ def node_package_version
66
+ match = File.foreach(yarn_lock_path).grep(/^stimulus_reflex/)
67
+ return match.first[NODE_VERSION_FORMAT, 1] if match.present?
68
+
69
+ match = File.foreach(yarn_link_path).grep(/version/)
70
+ return match.first[JSON_VERSION_FORMAT, 1] if match.present?
71
+
72
+ puts <<~WARN
73
+ Can't locate the stimulus_reflex NPM package.
74
+ Either add it to your package.json as a dependency or use "yarn link stimulus_reflex" if you are doing development.
75
+ WARN
76
+ exit
77
+ end
78
+
79
+ def yarn_lock_path
80
+ Rails.root.join("yarn.lock")
81
+ end
82
+
83
+ def yarn_link_path
84
+ Rails.root.join("node_modules", "stimulus_reflex", "package.json")
85
+ end
24
86
  end
25
87
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex
4
+ class Broadcaster
5
+ include CableReady::Broadcaster
6
+
7
+ attr_reader :reflex, :logger
8
+ delegate :permanent_attribute_name, :stream_name, to: :reflex
9
+
10
+ def initialize(reflex)
11
+ @reflex = reflex
12
+ @logger = Rails.logger
13
+ end
14
+
15
+ def nothing?
16
+ false
17
+ end
18
+
19
+ def page?
20
+ false
21
+ end
22
+
23
+ def selector?
24
+ false
25
+ end
26
+
27
+ def enqueue_message(subject:, body: nil, data: {})
28
+ logger.error "\e[31m#{body}\e[0m" if subject == "error"
29
+ cable_ready[stream_name].dispatch_event(
30
+ name: "stimulus-reflex:server-message",
31
+ detail: {
32
+ reflexId: data["reflexId"],
33
+ stimulus_reflex: data.merge(
34
+ broadcaster: to_sym,
35
+ server_message: {subject: subject, body: body}
36
+ )
37
+ }
38
+ )
39
+ end
40
+
41
+ def broadcast_message(subject:, body: nil, data: {})
42
+ enqueue_message subject: subject, body: body, data: data
43
+ cable_ready.broadcast
44
+ end
45
+
46
+ # abstract method to be implemented by subclasses
47
+ def broadcast(*args)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ # abstract method to be implemented by subclasses
52
+ def to_sym
53
+ raise NotImplementedError
54
+ end
55
+ end
56
+ end
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StimulusReflex
2
- class NothingMorphMode < MorphMode
3
- def broadcast(reflex, selectors, data)
4
+ class NothingBroadcaster < Broadcaster
5
+ def broadcast(_, data)
4
6
  broadcast_message subject: "nothing", data: data
5
7
  end
6
8
 
7
- def to_sym
8
- :nothing
9
- end
10
-
11
9
  def nothing?
12
10
  true
13
11
  end
12
+
13
+ def to_sym
14
+ :nothing
15
+ end
14
16
  end
15
17
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex
4
+ class PageBroadcaster < Broadcaster
5
+ def broadcast(selectors, data)
6
+ reflex.controller.process reflex.url_params[:action]
7
+ page_html = reflex.controller.response.body
8
+
9
+ return unless page_html.present?
10
+
11
+ document = Nokogiri::HTML(page_html)
12
+ selectors = selectors.select { |s| document.css(s).present? }
13
+ selectors.each do |selector|
14
+ html = document.css(selector).inner_html
15
+ cable_ready[stream_name].morph(
16
+ selector: selector,
17
+ html: html,
18
+ children_only: true,
19
+ permanent_attribute_name: permanent_attribute_name,
20
+ stimulus_reflex: data.merge({
21
+ broadast_type: to_sym
22
+ })
23
+ )
24
+ end
25
+ cable_ready.broadcast
26
+ end
27
+
28
+ def to_sym
29
+ :page
30
+ end
31
+
32
+ def page?
33
+ true
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusReflex
4
+ class SelectorBroadcaster < Broadcaster
5
+ def broadcast(_, data = {})
6
+ morphs.each do |morph|
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)
13
+ if match.present?
14
+ cable_ready[stream_name].morph(
15
+ selector: selector,
16
+ html: match.inner_html,
17
+ children_only: true,
18
+ permanent_attribute_name: permanent_attribute_name,
19
+ stimulus_reflex: data.merge({
20
+ broadast_type: to_sym
21
+ })
22
+ )
23
+ else
24
+ cable_ready[stream_name].inner_html(
25
+ selector: selector,
26
+ html: fragment.to_html,
27
+ stimulus_reflex: data.merge({
28
+ broadast_type: to_sym
29
+ })
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ cable_ready.broadcast
36
+ morphs.clear
37
+ end
38
+
39
+ def morphs
40
+ @morphs ||= []
41
+ end
42
+
43
+ def to_sym
44
+ :selector
45
+ end
46
+
47
+ def selector?
48
+ true
49
+ end
50
+ end
51
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StimulusReflex::Channel < ActionCable::Channel::Base
4
- include StimulusReflex::Broadcaster
5
-
6
4
  def stream_name
7
5
  ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) }
8
6
  [
@@ -34,21 +32,26 @@ class StimulusReflex::Channel < ActionCable::Channel::Base
34
32
  reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, permanent_attribute_name: permanent_attribute_name, params: params)
35
33
  delegate_call_to_reflex reflex, method_name, arguments
36
34
  rescue => invoke_error
37
- reflex&.rescue_with_handler(invoke_error)
38
35
  message = exception_message_with_backtrace(invoke_error)
39
- return broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to invoke #{target}! #{url} #{message}", data: data
36
+ body = "StimulusReflex::Channel Failed to invoke #{target}! #{url} #{message}"
37
+ if reflex
38
+ reflex.rescue_with_handler(invoke_error)
39
+ reflex.broadcast_message subject: "error", body: body, data: data
40
+ else
41
+ logger.error "\e[31m#{body}\e[0m"
42
+ end
43
+ return
40
44
  end
41
45
 
42
46
  if reflex.halted?
43
- broadcast_message subject: "halted", data: data
47
+ reflex.broadcast_message subject: "halted", data: data
44
48
  else
45
49
  begin
46
- reflex.morph_mode.stream_name = stream_name
47
- reflex.morph_mode.broadcast(reflex, selectors, data)
50
+ reflex.broadcast(selectors, data)
48
51
  rescue => render_error
49
52
  reflex.rescue_with_handler(render_error)
50
53
  message = exception_message_with_backtrace(render_error)
51
- broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to re-render #{url} #{message}", data: data
54
+ reflex.broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to re-render #{url} #{message}", data: data
52
55
  end
53
56
  end
54
57
  ensure