stimulus_reflex 3.3.0.pre0 → 3.3.0.pre5

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.

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