view_component_reflex 2.2.2 → 2.3.1

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.
@@ -1,148 +1,192 @@
1
- module ViewComponentReflex
2
- class Component < ViewComponent::Base
3
- class << self
4
- def init_stimulus_reflex
5
- @stimulus_reflex ||= if name.include? "::"
6
- module_parent.const_set(name.split("::").last + "Reflex", Class.new(ViewComponentReflex::Reflex))
7
- else
8
- Object.const_set(name + "Reflex", Class.new(ViewComponentReflex::Reflex))
9
- end
10
- @stimulus_reflex.component_class = self
11
- end
12
- end
13
-
14
- def self.stimulus_controller
15
- name.chomp("Component").underscore.dasherize
16
- end
17
-
18
- def stimulus_reflex?
19
- helpers.controller.instance_variable_get(:@stimulus_reflex)
20
- end
21
-
22
- def component_controller(opts_or_tag = :div, opts = {}, &blk)
23
- self.class.init_stimulus_reflex
24
- init_key
25
-
26
- tag = :div
27
- options = if opts_or_tag.is_a? Hash
28
- opts_or_tag
29
- else
30
- tag = opts_or_tag
31
- opts
32
- end
33
- options[:data] = {
34
- controller: self.class.stimulus_controller,
35
- key: key,
36
- **(options[:data] || {})
37
- }
38
- content_tag tag, capture(&blk), options
39
- end
40
-
41
- def can_render_to_string?
42
- omitted_from_state.empty?
43
- end
44
-
45
- # key is required if you're using state
46
- # We can't initialize the session state in the initial method
47
- # because it doesn't have a view_context yet
48
- # This is the next best place to do it
49
- def init_key
50
- # we want the erb file that renders the component. `caller` gives the file name,
51
- # and line number, which should be unique. We hash it to make it a nice number
52
- key = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]&.hash.to_s
53
- key += collection_key.to_s if collection_key
54
- @key = key
55
- end
56
-
57
- # Helper to use to create the proper reflex data attributes for an element
58
- def reflex_data_attributes(reflex)
59
- action, method = reflex.to_s.split("->")
60
- if method.nil?
61
- method = action
62
- action = "click"
63
- end
64
-
65
- {
66
- reflex: "#{action}->#{self.class.name}##{method}",
67
- key: key
68
- }
69
- end
70
-
71
- def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
72
- if content_or_options_with_block.is_a?(Hash)
73
- merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
74
- else
75
- merge_data_attributes(options, reflex_data_attributes(reflex))
76
- end
77
- content_tag(name, content_or_options_with_block, options, escape, &block)
78
- end
79
-
80
- def collection_key
81
- nil
82
- end
83
-
84
- def permit_parameter?(initial_param, new_param)
85
- initial_param != new_param
86
- end
87
-
88
- def omitted_from_state
89
- []
90
- end
91
-
92
- def key
93
- # initialize session state
94
- if !stimulus_reflex? || ViewComponentReflex::Engine.state_adapter.state(request, @key).empty?
95
-
96
- new_state = create_safe_state
97
-
98
- ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
99
- ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
100
- else
101
- initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial")
102
- ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
103
- instance_value = instance_variable_get(k)
104
- if permit_parameter?(initial_state[k], instance_value)
105
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
106
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, @key, {k => instance_value})
107
- else
108
- instance_variable_set(k, v)
109
- end
110
- end
111
- end
112
- @key
113
- end
114
-
115
- def safe_instance_variables
116
- instance_variables - unsafe_instance_variables
117
- end
118
-
119
- private
120
-
121
- def unsafe_instance_variables
122
- [
123
- :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
124
- :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
125
- :@helpers, :@controller, :@request, :@tag_builder
126
- ]
127
- end
128
-
129
- def create_safe_state
130
- new_state = {}
131
-
132
- # this will almost certainly break
133
- safe_instance_variables.each do |k|
134
- new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
135
- end
136
- new_state
137
- end
138
-
139
- def merge_data_attributes(options, attributes)
140
- data = options[:data]
141
- if data.nil?
142
- options[:data] = attributes
143
- else
144
- options[:data].merge! attributes
145
- end
146
- end
147
- end
148
- end
1
+ module ViewComponentReflex
2
+ class Component < ViewComponent::Base
3
+ class << self
4
+ def init_stimulus_reflex
5
+ factory = ViewComponentReflex::ReflexFactory.new(self)
6
+ @stimulus_reflex ||= factory.reflex
7
+ wire_up_callbacks if factory.new?
8
+ end
9
+
10
+ def reflex_base_class(new_base_class = nil)
11
+ if new_base_class.nil?
12
+ @reflex_base_class ||= ViewComponentReflex::Reflex
13
+ else
14
+ if new_base_class <= ViewComponentReflex::Reflex
15
+ @reflex_base_class = new_base_class
16
+ else
17
+ raise StandardError.new("The reflex base class must inherit from ViewComponentReflex::Reflex")
18
+ end
19
+ end
20
+ end
21
+
22
+ def queue_callback(key, args, blk)
23
+ callbacks(key).push({
24
+ args: args,
25
+ blk: blk
26
+ })
27
+ end
28
+
29
+ def callbacks(key)
30
+ @callbacks ||= {}
31
+ @callbacks[key] ||= []
32
+ end
33
+
34
+ def register_callbacks(key)
35
+ callbacks(key).each do |cb|
36
+ @stimulus_reflex.send("#{key}_reflex", *cb[:args], &cb[:blk])
37
+ end
38
+ end
39
+
40
+ def before_reflex(*args, &blk)
41
+ queue_callback(:before, args, blk)
42
+ end
43
+
44
+ def after_reflex(*args, &blk)
45
+ queue_callback(:after, args, blk)
46
+ end
47
+
48
+ def around_reflex(*args, &blk)
49
+ queue_callback(:around, args, blk)
50
+ end
51
+
52
+ def wire_up_callbacks
53
+ register_callbacks(:before)
54
+ register_callbacks(:after)
55
+ register_callbacks(:around)
56
+ end
57
+ end
58
+
59
+ def self.stimulus_controller
60
+ name.chomp("Component").underscore.dasherize
61
+ end
62
+
63
+ def stimulus_reflex?
64
+ helpers.controller.instance_variable_get(:@stimulus_reflex)
65
+ end
66
+
67
+ def component_controller(opts_or_tag = :div, opts = {}, &blk)
68
+ init_key
69
+
70
+ tag = :div
71
+ options = if opts_or_tag.is_a? Hash
72
+ opts_or_tag
73
+ else
74
+ tag = opts_or_tag
75
+ opts
76
+ end
77
+ options[:data] = {
78
+ controller: self.class.stimulus_controller,
79
+ key: key,
80
+ **(options[:data] || {})
81
+ }
82
+ content_tag tag, capture(&blk), options
83
+ end
84
+
85
+ def can_render_to_string?
86
+ omitted_from_state.empty?
87
+ end
88
+
89
+ # key is required if you're using state
90
+ # We can't initialize the session state in the initial method
91
+ # because it doesn't have a view_context yet
92
+ # This is the next best place to do it
93
+ def init_key
94
+ # we want the erb file that renders the component. `caller` gives the file name,
95
+ # and line number, which should be unique. We hash it to make it a nice number
96
+ key = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]&.hash.to_s
97
+ key += collection_key.to_s if collection_key
98
+ @key = key
99
+ end
100
+
101
+ # Helper to use to create the proper reflex data attributes for an element
102
+ def reflex_data_attributes(reflex)
103
+ action, method = reflex.to_s.split("->")
104
+ if method.nil?
105
+ method = action
106
+ action = "click"
107
+ end
108
+
109
+ {
110
+ reflex: "#{action}->#{self.class.name}##{method}",
111
+ key: key
112
+ }
113
+ end
114
+
115
+ def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
116
+ if content_or_options_with_block.is_a?(Hash)
117
+ merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
118
+ else
119
+ merge_data_attributes(options, reflex_data_attributes(reflex))
120
+ end
121
+ content_tag(name, content_or_options_with_block, options, escape, &block)
122
+ end
123
+
124
+ def collection_key
125
+ nil
126
+ end
127
+
128
+ def permit_parameter?(initial_param, new_param)
129
+ initial_param != new_param
130
+ end
131
+
132
+ def omitted_from_state
133
+ []
134
+ end
135
+
136
+ def key
137
+ # initialize session state
138
+ if !stimulus_reflex? || ViewComponentReflex::Engine.state_adapter.state(request, @key).empty?
139
+
140
+ new_state = create_safe_state
141
+
142
+ ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state)
143
+ ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state)
144
+ else
145
+ initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial")
146
+ ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v|
147
+ instance_value = instance_variable_get(k)
148
+ if permit_parameter?(initial_state[k], instance_value)
149
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
150
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, @key, {k => instance_value})
151
+ else
152
+ instance_variable_set(k, v)
153
+ end
154
+ end
155
+ end
156
+ @key
157
+ end
158
+
159
+ def safe_instance_variables
160
+ instance_variables - unsafe_instance_variables
161
+ end
162
+
163
+ private
164
+
165
+ def unsafe_instance_variables
166
+ [
167
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
168
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
169
+ :@helpers, :@controller, :@request, :@tag_builder
170
+ ]
171
+ end
172
+
173
+ def create_safe_state
174
+ new_state = {}
175
+
176
+ # this will almost certainly break
177
+ safe_instance_variables.each do |k|
178
+ new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k)
179
+ end
180
+ new_state
181
+ end
182
+
183
+ def merge_data_attributes(options, attributes)
184
+ data = options[:data]
185
+ if data.nil?
186
+ options[:data] = attributes
187
+ else
188
+ options[:data].merge! attributes
189
+ end
190
+ end
191
+ end
192
+ end
@@ -1,9 +1,10 @@
1
- require "stimulus_reflex"
2
- require "view_component_reflex/state_adapter/session"
3
- require "view_component_reflex/state_adapter/memory"
4
- require "view_component_reflex/reflex"
5
- require "view_component_reflex/engine"
6
-
7
- module ViewComponentReflex
8
- # Your code goes here...
9
- end
1
+ require "stimulus_reflex"
2
+ require 'view_component_reflex/reflex_factory'
3
+ require "view_component_reflex/state_adapter/session"
4
+ require "view_component_reflex/state_adapter/memory"
5
+ require "view_component_reflex/reflex"
6
+ require "view_component_reflex/engine"
7
+
8
+ module ViewComponentReflex
9
+ # Your code goes here...
10
+ end
@@ -1,13 +1,36 @@
1
- module ViewComponentReflex
2
- class Engine < ::Rails::Engine
3
- class << self
4
- mattr_accessor :state_adapter
5
-
6
- self.state_adapter = StateAdapter::Memory
7
- end
8
-
9
- def self.configure
10
- yield self if block_given?
11
- end
12
- end
13
- end
1
+ module ViewComponentReflex
2
+ class Engine < ::Rails::Engine
3
+ class << self
4
+ mattr_accessor :state_adapter
5
+
6
+ self.state_adapter = StateAdapter::Memory
7
+ end
8
+
9
+ config.to_prepare do
10
+ StimulusReflex::Channel.class_eval do
11
+ unless instance_methods.include?(:receive_original)
12
+ alias_method :receive_original, :receive
13
+ def receive(data)
14
+ target = data["target"].to_s
15
+ reflex_name, _ = target.split("#")
16
+ reflex_name = reflex_name.camelize
17
+ component_name = reflex_name.end_with?("Reflex") ? reflex_name[0...-6] : reflex_name
18
+ if component_name.end_with?("Component")
19
+ begin
20
+ component_name.constantize.init_stimulus_reflex
21
+ rescue
22
+ p "Tried to initialize view_component_reflex on #{component_name}, but it's not a view_component_reflex"
23
+ end
24
+
25
+ end
26
+ receive_original(data)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.configure
33
+ yield self if block_given?
34
+ end
35
+ end
36
+ end
@@ -1,147 +1,147 @@
1
- module ViewComponentReflex
2
- class Reflex < StimulusReflex::Reflex
3
- include CableReady::Broadcaster
4
-
5
- class << self
6
- attr_accessor :component_class
7
- end
8
-
9
- def refresh!(primary_selector = nil, *rest)
10
- save_state
11
-
12
- if primary_selector.nil? && !component.can_render_to_string?
13
- primary_selector = selector
14
- end
15
- if primary_selector
16
- prevent_refresh!
17
-
18
- controller.process(url_params[:action])
19
- document = Nokogiri::HTML(controller.response.body)
20
- [primary_selector, *rest].each do |s|
21
- html = document.css(s)
22
- if html.present?
23
- cable_ready[channel.stream_name].morph(
24
- selector: s,
25
- html: html.inner_html,
26
- children_only: true,
27
- permanent_attribute_name: "data-reflex-permanent"
28
- )
29
- end
30
- end
31
- else
32
- refresh_component!
33
- end
34
- cable_ready.broadcast
35
- end
36
-
37
- def refresh_component!
38
- component.tap do |k|
39
- k.define_singleton_method(:key) do
40
- element.dataset[:key]
41
- end
42
- end
43
- document = Nokogiri::HTML(controller.render_component_to_string(component))
44
- cable_ready[channel.stream_name].morph(
45
- selector: selector,
46
- children_only: true,
47
- html: document.css(selector).inner_html,
48
- permanent_attribute_name: "data-reflex-permanent"
49
- )
50
- end
51
-
52
- def refresh_all!
53
- refresh!("body")
54
- end
55
-
56
- def selector
57
- "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]"
58
- end
59
-
60
- # SR's delegate_call_to_reflex in channel.rb
61
- # uses method to gather the method parameters, but since we're abusing
62
- # method_missing here, that'll always fail
63
- def method(name)
64
- component.method(name.to_sym)
65
- end
66
-
67
- def respond_to_missing?(name, _ = false)
68
- !!name.to_proc
69
- end
70
-
71
- # this is copied out of stimulus_reflex/reflex.rb and modified
72
- def morph(selectors, html = "")
73
- case selectors
74
- when :nothing
75
- @broadcaster = StimulusReflex::NothingBroadcaster.new(self)
76
- else
77
- @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
78
- broadcaster.morphs << [selectors, html]
79
- end
80
- end
81
-
82
- def method_missing(name, *args)
83
- morph :nothing
84
- super unless respond_to_missing?(name)
85
- state.each do |k, v|
86
- component.instance_variable_set(k, v)
87
- end
88
- name.to_proc.call(component, *args)
89
- refresh! unless @prevent_refresh
90
- end
91
-
92
- def prevent_refresh!
93
- @prevent_refresh = true
94
- end
95
-
96
- private
97
-
98
- def component_class
99
- self.class.component_class
100
- end
101
-
102
- def stimulus_controller
103
- component_class.stimulus_controller
104
- end
105
-
106
- def stimulate(target, data)
107
- dataset = {}
108
- data.each do |k, v|
109
- dataset["data-#{k}"] = v.to_s
110
- end
111
- channel.receive({
112
- "target" => target,
113
- "attrs" => element.attributes.to_h.symbolize_keys,
114
- "dataset" => dataset
115
- })
116
- end
117
-
118
- def component
119
- return @component if @component
120
- @component = component_class.allocate
121
- reflex = self
122
- exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session, :prevent_refresh!, :selector, :stimulate]
123
- exposed_methods.each do |meth|
124
- @component.define_singleton_method(meth) do |*a|
125
- reflex.send(meth, *a)
126
- end
127
- end
128
- @component
129
- end
130
-
131
- def set_state(new_state = {})
132
- ViewComponentReflex::Engine.state_adapter.set_state(request, controller, element.dataset[:key], new_state)
133
- end
134
-
135
- def state
136
- ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
137
- end
138
-
139
- def save_state
140
- new_state = {}
141
- component.safe_instance_variables.each do |k|
142
- new_state[k] = component.instance_variable_get(k)
143
- end
144
- set_state(new_state)
145
- end
146
- end
147
- end
1
+ module ViewComponentReflex
2
+ class Reflex < StimulusReflex::Reflex
3
+ include CableReady::Broadcaster
4
+
5
+ class << self
6
+ attr_accessor :component_class
7
+ end
8
+
9
+ def refresh!(primary_selector = nil, *rest)
10
+ save_state
11
+
12
+ if primary_selector.nil? && !component.can_render_to_string?
13
+ primary_selector = selector
14
+ end
15
+ if primary_selector
16
+ prevent_refresh!
17
+
18
+ controller.process(url_params[:action])
19
+ document = Nokogiri::HTML(controller.response.body)
20
+ [primary_selector, *rest].each do |s|
21
+ html = document.css(s)
22
+ if html.present?
23
+ cable_ready[channel.stream_name].morph(
24
+ selector: s,
25
+ html: html.inner_html,
26
+ children_only: true,
27
+ permanent_attribute_name: "data-reflex-permanent"
28
+ )
29
+ end
30
+ end
31
+ else
32
+ refresh_component!
33
+ end
34
+ cable_ready.broadcast
35
+ end
36
+
37
+ def refresh_component!
38
+ component.tap do |k|
39
+ k.define_singleton_method(:key) do
40
+ element.dataset[:key]
41
+ end
42
+ end
43
+ document = Nokogiri::HTML(controller.render_component_to_string(component))
44
+ cable_ready[channel.stream_name].morph(
45
+ selector: selector,
46
+ children_only: true,
47
+ html: document.css(selector).inner_html,
48
+ permanent_attribute_name: "data-reflex-permanent"
49
+ )
50
+ end
51
+
52
+ def refresh_all!
53
+ refresh!("body")
54
+ end
55
+
56
+ def selector
57
+ "[data-controller~=\"#{stimulus_controller}\"][data-key=\"#{element.dataset[:key]}\"]"
58
+ end
59
+
60
+ # SR's delegate_call_to_reflex in channel.rb
61
+ # uses method to gather the method parameters, but since we're abusing
62
+ # method_missing here, that'll always fail
63
+ def method(name)
64
+ component.method(name.to_sym)
65
+ end
66
+
67
+ def respond_to_missing?(name, _ = false)
68
+ !!name.to_proc
69
+ end
70
+
71
+ # this is copied out of stimulus_reflex/reflex.rb and modified
72
+ def morph(selectors, html = "")
73
+ case selectors
74
+ when :nothing
75
+ @broadcaster = StimulusReflex::NothingBroadcaster.new(self)
76
+ else
77
+ @broadcaster = StimulusReflex::SelectorBroadcaster.new(self) unless broadcaster.selector?
78
+ broadcaster.morphs << [selectors, html]
79
+ end
80
+ end
81
+
82
+ def method_missing(name, *args)
83
+ morph :nothing
84
+ super unless respond_to_missing?(name)
85
+ state.each do |k, v|
86
+ component.instance_variable_set(k, v)
87
+ end
88
+ name.to_proc.call(component, *args)
89
+ refresh! unless @prevent_refresh
90
+ end
91
+
92
+ def prevent_refresh!
93
+ @prevent_refresh = true
94
+ end
95
+
96
+ private
97
+
98
+ def component_class
99
+ self.class.component_class
100
+ end
101
+
102
+ def stimulus_controller
103
+ component_class.stimulus_controller
104
+ end
105
+
106
+ def stimulate(target, data)
107
+ dataset = {}
108
+ data.each do |k, v|
109
+ dataset["data-#{k}"] = v.to_s
110
+ end
111
+ channel.receive({
112
+ "target" => target,
113
+ "attrs" => element.attributes.to_h.symbolize_keys,
114
+ "dataset" => dataset
115
+ })
116
+ end
117
+
118
+ def component
119
+ return @component if @component
120
+ @component = component_class.allocate
121
+ reflex = self
122
+ exposed_methods = [:params, :request, :element, :refresh!, :refresh_all!, :stimulus_controller, :session, :prevent_refresh!, :selector, :stimulate]
123
+ exposed_methods.each do |meth|
124
+ @component.define_singleton_method(meth) do |*a|
125
+ reflex.send(meth, *a)
126
+ end
127
+ end
128
+ @component
129
+ end
130
+
131
+ def set_state(new_state = {})
132
+ ViewComponentReflex::Engine.state_adapter.set_state(request, controller, element.dataset[:key], new_state)
133
+ end
134
+
135
+ def state
136
+ ViewComponentReflex::Engine.state_adapter.state(request, element.dataset[:key])
137
+ end
138
+
139
+ def save_state
140
+ new_state = {}
141
+ component.safe_instance_variables.each do |k|
142
+ new_state[k] = component.instance_variable_get(k)
143
+ end
144
+ set_state(new_state)
145
+ end
146
+ end
147
+ end