stimulus_reflex 3.3.0.pre1 → 3.3.0.pre6

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,18 @@ 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/sanity_checker"
16
+ require "stimulus_reflex/broadcasters/broadcaster"
17
+ require "stimulus_reflex/broadcasters/nothing_broadcaster"
18
+ require "stimulus_reflex/broadcasters/page_broadcaster"
19
+ require "stimulus_reflex/broadcasters/selector_broadcaster"
20
20
  require "generators/stimulus_reflex_generator"
21
21
 
22
22
  module StimulusReflex
23
23
  class Engine < Rails::Engine
24
+ initializer "stimulus_reflex.sanity_check" do
25
+ SanityChecker.check!
26
+ end
24
27
  end
25
28
  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
+ broadcaster: 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
+ broadcaster: 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
+ broadcaster: 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,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class StimulusReflex::Channel < ActionCable::Channel::Base
4
- include StimulusReflex::Broadcaster
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ def initialize(connection, identifier, params = {})
6
+ super
7
+ application_channel = Rails.root.join("app", "channels", "application_cable", "channel.rb")
8
+ require application_channel if File.exist?(application_channel)
9
+ end
10
+ end
11
+ end
5
12
 
13
+ class StimulusReflex::Channel < ApplicationCable::Channel
6
14
  def stream_name
7
15
  ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) }
8
16
  [
@@ -12,6 +20,7 @@ class StimulusReflex::Channel < ActionCable::Channel::Base
12
20
  end
13
21
 
14
22
  def subscribed
23
+ super
15
24
  stream_from stream_name
16
25
  end
17
26
 
@@ -34,21 +43,26 @@ class StimulusReflex::Channel < ActionCable::Channel::Base
34
43
  reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, permanent_attribute_name: permanent_attribute_name, params: params)
35
44
  delegate_call_to_reflex reflex, method_name, arguments
36
45
  rescue => invoke_error
37
- reflex&.rescue_with_handler(invoke_error)
38
46
  message = exception_message_with_backtrace(invoke_error)
39
- return broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to invoke #{target}! #{url} #{message}", data: data
47
+ body = "StimulusReflex::Channel Failed to invoke #{target}! #{url} #{message}"
48
+ if reflex
49
+ reflex.rescue_with_handler(invoke_error)
50
+ reflex.broadcast_message subject: "error", body: body, data: data
51
+ else
52
+ logger.error "\e[31m#{body}\e[0m"
53
+ end
54
+ return
40
55
  end
41
56
 
42
57
  if reflex.halted?
43
- broadcast_message subject: "halted", data: data
58
+ reflex.broadcast_message subject: "halted", data: data
44
59
  else
45
60
  begin
46
- reflex.morph_mode.stream_name = stream_name
47
- reflex.morph_mode.broadcast(reflex, selectors, data)
61
+ reflex.broadcast(selectors, data)
48
62
  rescue => render_error
49
63
  reflex.rescue_with_handler(render_error)
50
64
  message = exception_message_with_backtrace(render_error)
51
- broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to re-render #{url} #{message}", data: data
65
+ reflex.broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to re-render #{url} #{message}", data: data
52
66
  end
53
67
  end
54
68
  ensure
@@ -43,12 +43,13 @@ class StimulusReflex::Reflex
43
43
  end
44
44
  end
45
45
 
46
- attr_reader :channel, :url, :element, :selectors, :method_name, :morph_mode, :permanent_attribute_name
46
+ attr_reader :channel, :url, :element, :selectors, :method_name, :broadcaster, :permanent_attribute_name
47
47
 
48
- delegate :connection, to: :channel
48
+ delegate :connection, :stream_name, to: :channel
49
49
  delegate :session, to: :request
50
+ delegate :broadcast, :broadcast_message, to: :broadcaster
50
51
 
51
- def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, stream_name: nil, permanent_attribute_name: nil, params: {})
52
+ def initialize(channel, url: nil, element: nil, selectors: [], method_name: nil, permanent_attribute_name: nil, params: {})
52
53
  @channel = channel
53
54
  @url = url
54
55
  @element = element
@@ -56,7 +57,7 @@ class StimulusReflex::Reflex
56
57
  @method_name = method_name
57
58
  @params = params
58
59
  @permanent_attribute_name = permanent_attribute_name
59
- @morph_mode = StimulusReflex::PageMorphMode.new
60
+ @broadcaster = StimulusReflex::PageBroadcaster.new(self)
60
61
  self.params
61
62
  end
62
63
 
@@ -89,35 +90,17 @@ class StimulusReflex::Reflex
89
90
  def morph(selectors, html = "")
90
91
  case selectors
91
92
  when :page
92
- raise StandardError.new("Cannot call :page morph after :#{@morph_mode.to_sym} morph") unless @morph_mode.page?
93
+ raise StandardError.new("Cannot call :page morph after :#{broadcaster.to_sym} morph") unless broadcaster.page?
93
94
  when :nothing
94
- raise StandardError.new("Cannot call :nothing morph after :selector morph") if @morph_mode.selector?
95
- @morph_mode = StimulusReflex::NothingMorphMode.new
95
+ raise StandardError.new("Cannot call :nothing morph after :selector morph") if broadcaster.selector?
96
+ @broadcaster = StimulusReflex::NothingBroadcaster.new(self) unless broadcaster.nothing?
96
97
  else
97
- raise StandardError.new("Cannot call :selector morph after :nothing morph") if @morph_mode.nothing?
98
- @morph_mode = StimulusReflex::SelectorMorphMode.new
99
- if selectors.is_a?(Hash)
100
- selectors.each do |selector, html|
101
- enqueue_selector_broadcast selector, html
102
- end
103
- else
104
- enqueue_selector_broadcast selectors, html
105
- end
106
- cable_ready.broadcast
98
+ raise StandardError.new("Cannot call :selector morph after :nothing morph") if broadcaster.nothing?
99
+ @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
100
+ broadcaster.morphs << [selectors, html]
107
101
  end
108
102
  end
109
103
 
110
- def enqueue_selector_broadcast(selector, html)
111
- fragment = Nokogiri::HTML(html)
112
- parent = fragment.at_css(selector)
113
- cable_ready[channel.stream_name].morph(
114
- selector: selector,
115
- html: parent.present? ? parent.inner_html : fragment.to_html,
116
- children_only: true,
117
- permanent_attribute_name: permanent_attribute_name
118
- )
119
- end
120
-
121
104
  def controller
122
105
  @controller ||= begin
123
106
  request.controller_class.new.tap do |c|