stimulus_reflex 3.3.0.pre1 → 3.3.0.pre6

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