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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -4
- data/Gemfile.lock +72 -70
- data/README.md +26 -20
- data/lib/generators/templates/app/javascript/controllers/%file_name%_controller.js.tt +53 -21
- data/lib/generators/templates/app/javascript/controllers/application_controller.js.tt +11 -8
- data/lib/stimulus_reflex.rb +67 -5
- data/lib/stimulus_reflex/broadcasters/broadcaster.rb +56 -0
- data/lib/stimulus_reflex/{morph_mode/nothing_morph_mode.rb → broadcasters/nothing_broadcaster.rb} +8 -6
- data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +36 -0
- data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +51 -0
- data/lib/stimulus_reflex/channel.rb +11 -8
- data/lib/stimulus_reflex/reflex.rb +11 -26
- data/lib/stimulus_reflex/version.rb +1 -1
- data/lib/tasks/stimulus_reflex/install.rake +13 -1
- metadata +25 -18
- data/lib/stimulus_reflex/broadcaster.rb +0 -53
- data/lib/stimulus_reflex/morph_mode.rb +0 -19
- data/lib/stimulus_reflex/morph_mode/page_morph_mode.rb +0 -15
- data/lib/stimulus_reflex/morph_mode/selector_morph_mode.rb +0 -15
- data/stimulus_reflex.gemspec +0 -39
- data/tags +0 -42
- data/test/tmp/app/reflexes/application_reflex.rb +0 -12
- data/test/tmp/app/reflexes/demo_reflex.rb +0 -25
@@ -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
|
-
*
|
9
|
-
*
|
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 %>
|
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
|
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
|
-
*
|
47
|
+
* reflexId - a UUID4 or developer-provided unique identifier for each Reflex
|
23
48
|
*/
|
24
49
|
|
25
50
|
<% if actions.empty? -%>
|
26
|
-
//
|
27
|
-
//
|
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
|
-
//
|
31
|
-
// element.innerText = '
|
58
|
+
// danceSuccess(element, reflex, noop, reflexId) {
|
59
|
+
// element.innerText = 'Danced like no one was watching! Was someone watching?'
|
32
60
|
// }
|
33
61
|
|
34
|
-
//
|
35
|
-
// console.error('
|
36
|
-
// element.innerText = '
|
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}
|
45
|
-
// console.
|
76
|
+
// <%= "#{action}_error".camelize(:lower) %>(element, reflex, error, reflexId) {
|
77
|
+
// console.error("<%= action %> error", element, reflex, error, reflexId)
|
46
78
|
// }
|
47
79
|
|
48
|
-
// <%= "#{action}
|
49
|
-
// console.
|
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,
|
53
|
-
// console.log("after <%= action %>", element, reflex,
|
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
|
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
|
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. "
|
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
|
-
*
|
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,
|
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
|
}
|
data/lib/stimulus_reflex.rb
CHANGED
@@ -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/
|
18
|
-
require "stimulus_reflex/
|
19
|
-
require "stimulus_reflex/
|
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
|
data/lib/stimulus_reflex/{morph_mode/nothing_morph_mode.rb → broadcasters/nothing_broadcaster.rb}
RENAMED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StimulusReflex
|
2
|
-
class
|
3
|
-
def broadcast(
|
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
|
-
|
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.
|
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
|