view_component_reflex 3.1.14.pre3 → 3.1.14.pre7

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,32 +1,32 @@
1
- begin
2
- require 'bundler/setup'
3
- rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
- end
6
-
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'ViewComponentReflex'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.md')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
-
17
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
- load 'rails/tasks/engine.rake'
19
-
20
- load 'rails/tasks/statistics.rake'
21
-
22
- require 'bundler/gem_tasks'
23
-
24
- require 'rake/testtask'
25
-
26
- Rake::TestTask.new(:test) do |t|
27
- t.libs << 'test'
28
- t.pattern = 'test/**/*_test.rb'
29
- t.verbose = false
30
- end
31
-
32
- task default: :test
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ViewComponentReflex'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -1,244 +1,256 @@
1
- module ViewComponentReflex
2
- class Component < ViewComponent::Base
3
- class_attribute :reflex_base_class, default: ViewComponentReflex::Reflex
4
- attr_reader :key
5
-
6
- class << self
7
- def init_stimulus_reflex
8
- factory = ViewComponentReflex::ReflexFactory.new(self)
9
- @stimulus_reflex ||= factory.reflex
10
- wire_up_callbacks if factory.new?
11
- end
12
-
13
- def queue_callback(key, args, blk)
14
- callbacks(key).push({
15
- args: args,
16
- blk: blk
17
- })
18
- end
19
-
20
- def callbacks(key)
21
- @callbacks ||= {}
22
- @callbacks[key] ||= []
23
- end
24
-
25
- def register_callbacks(key)
26
- callbacks(key).each do |cb|
27
- @stimulus_reflex.send("#{key}_reflex", *cb[:args], &cb[:blk])
28
- end
29
- end
30
-
31
- def before_reflex(*args, &blk)
32
- queue_callback(:before, args, blk)
33
- end
34
-
35
- def after_reflex(*args, &blk)
36
- queue_callback(:after, args, blk)
37
- end
38
-
39
- def around_reflex(*args, &blk)
40
- queue_callback(:around, args, blk)
41
- end
42
-
43
- def wire_up_callbacks
44
- register_callbacks(:before)
45
- register_callbacks(:after)
46
- register_callbacks(:around)
47
- end
48
- end
49
-
50
- def self.stimulus_controller
51
- name.chomp("Component").underscore.dasherize.gsub("/", "--")
52
- end
53
-
54
- def stimulus_reflex?
55
- helpers.controller.instance_variable_get(:@stimulus_reflex)
56
- end
57
-
58
- def component_controller(opts_or_tag = :div, opts = {}, &blk)
59
- initialize_component
60
-
61
- tag = :div
62
- options = if opts_or_tag.is_a? Hash
63
- opts_or_tag
64
- else
65
- tag = opts_or_tag
66
- opts
67
- end
68
-
69
- options[:data] = {
70
- controller: self.class.stimulus_controller,
71
- key: key,
72
- **(options[:data] || {})
73
- }
74
- content_tag tag, capture(&blk), options
75
- end
76
-
77
- def can_render_to_string?
78
- omitted_from_state.empty?
79
- end
80
-
81
- # We can't truly initialize the component without the view_context,
82
- # which isn't available in the `initialize` method. We require the
83
- # developer to wrap components in `component_controller`, so this is where
84
- # we truly initialize the component.
85
- # This method is overridden in reflex.rb when the component is re-rendered. The
86
- # override simply sets @key to element.dataset[:key]
87
- # We don't want it to initialize the state again, and since we're rendering the component
88
- # outside of the view, we need to skip the initialize_key method as well
89
- def initialize_component
90
- initialize_key
91
- initialize_state
92
- end
93
-
94
- # Note to self:
95
- # This has to be in the Component class because there are situations
96
- # where the controller is the one rendering the component
97
- # so we can't rely on the component created by the reflex
98
- def initialize_state
99
- return if state_initialized?
100
- adapter = ViewComponentReflex::Engine.state_adapter
101
-
102
- # newly mounted
103
- if !stimulus_reflex? || adapter.state(request, @key).empty?
104
-
105
- new_state = create_safe_state
106
-
107
- adapter.wrap_write_async do
108
- adapter.store_state(request, @key, new_state)
109
- adapter.store_state(request, "#{@key}_initial", new_state)
110
- end
111
-
112
- # updating a mounted component
113
- else
114
- initial_state = adapter.state(request, "#{@key}_initial")
115
-
116
- parameters_changed = []
117
- adapter.state(request, @key).each do |k, v|
118
- instance_value = instance_variable_get(k)
119
- if permit_parameter?(initial_state[k], instance_value)
120
- parameters_changed << k
121
- adapter.wrap_write_async do
122
- adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
123
- adapter.set_state(request, controller, @key, {k => instance_value})
124
- end
125
- else
126
- instance_variable_set(k, v)
127
- end
128
- end
129
- after_state_initialized(parameters_changed)
130
- end
131
- @state_initialized = true
132
- end
133
-
134
- def state_initialized?
135
- @state_initialized
136
- end
137
-
138
- def initialize_key
139
- # we want the erb file that renders the component. `caller` gives the file name,
140
- # and line number, which should be unique. We hash it to make it a nice number
141
- erb_file = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]
142
- key = if erb_file
143
- Digest::SHA2.hexdigest(erb_file.split(":in")[0])
144
- else
145
- ""
146
- end
147
- key += collection_key.to_s if collection_key
148
- @key = key
149
- end
150
-
151
- # Helper to use to create the proper reflex data attributes for an element
152
- def reflex_data_attributes(reflex)
153
- action, method = reflex.to_s.split("->")
154
- if method.nil?
155
- method = action
156
- action = "click"
157
- end
158
-
159
- {
160
- reflex: "#{action}->#{self.class.name}##{method}",
161
- key: key
162
- }
163
- end
164
-
165
- def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
166
- if content_or_options_with_block.is_a?(Hash)
167
- merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
168
- else
169
- merge_data_attributes(options, reflex_data_attributes(reflex))
170
- end
171
- content_tag(name, content_or_options_with_block, options, escape, &block)
172
- end
173
-
174
- def collection_key
175
- nil
176
- end
177
-
178
- def permit_parameter?(initial_param, new_param)
179
- initial_param != new_param
180
- end
181
-
182
- def omitted_from_state
183
- []
184
- end
185
-
186
- def after_state_initialized(parameters_changed)
187
- # called after state component has been hydrated
188
- end
189
-
190
- # def receive_params(old_state, params)
191
- # # no op
192
- # end
193
-
194
- def safe_instance_variables
195
- instance_variables - unsafe_instance_variables - omitted_from_state
196
- end
197
-
198
- private
199
-
200
- def unsafe_instance_variables
201
- [
202
- :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
203
- :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
204
- :@helpers, :@controller, :@request, :@tag_builder, :@state_initialized,
205
- :@_content_evaluated, :@_render_in_block, :@__vc_controller, :@__vc_helpers,
206
- :@__cached_content, :@__vc_variant, :@__vc_content_evaluated, :@__vc_render_in_block,
207
- ]
208
- end
209
-
210
- def content
211
- if cached_content && !@_render_in_block
212
- cached_content
213
- else
214
- super
215
- end
216
- end
217
-
218
- def cached_content
219
- @__cached_content__
220
- end
221
-
222
- def create_safe_state
223
- new_state = {}
224
-
225
- # this will almost certainly break
226
- safe_instance_variables.each do |k|
227
- new_state[k] = instance_variable_get(k)
228
- end
229
-
230
- new_state[:@__cached_content__] = content
231
-
232
- new_state
233
- end
234
-
235
- def merge_data_attributes(options, attributes)
236
- data = options[:data]
237
- if data.nil?
238
- options[:data] = attributes
239
- else
240
- options[:data].merge! attributes
241
- end
242
- end
243
- end
244
- end
1
+ module ViewComponentReflex
2
+ class Component < ViewComponent::Base
3
+ class_attribute :reflex_base_class, default: ViewComponentReflex::Reflex
4
+ attr_reader :key
5
+
6
+ class << self
7
+ def init_stimulus_reflex
8
+ factory = ViewComponentReflex::ReflexFactory.new(self)
9
+ @stimulus_reflex ||= factory.reflex
10
+
11
+ # Always wire up new callbacks in development
12
+ if Rails.env.development?
13
+ reset_callbacks
14
+ wire_up_callbacks
15
+ elsif factory.new? # only wire up callbacks in production if they haven't been wired up yet
16
+ wire_up_callbacks
17
+ end
18
+ end
19
+
20
+ def queue_callback(key, args, blk)
21
+ callbacks(key).push({
22
+ args: args,
23
+ blk: blk
24
+ })
25
+ end
26
+
27
+ def callbacks(key)
28
+ @callbacks ||= {}
29
+ @callbacks[key] ||= []
30
+ end
31
+
32
+ def register_callbacks(key)
33
+ callbacks(key).each do |cb|
34
+ @stimulus_reflex.send("#{key}_reflex", *cb[:args], &cb[:blk])
35
+ end
36
+ end
37
+
38
+ def before_reflex(*args, &blk)
39
+ queue_callback(:before, args, blk)
40
+ end
41
+
42
+ def after_reflex(*args, &blk)
43
+ queue_callback(:after, args, blk)
44
+ end
45
+
46
+ def around_reflex(*args, &blk)
47
+ queue_callback(:around, args, blk)
48
+ end
49
+
50
+ def reset_callbacks
51
+ # SR uses :process as the underlying callback key
52
+ @stimulus_reflex.reset_callbacks(:process)
53
+ end
54
+
55
+ def wire_up_callbacks
56
+ register_callbacks(:before)
57
+ register_callbacks(:after)
58
+ register_callbacks(:around)
59
+ end
60
+ end
61
+
62
+ def self.stimulus_controller
63
+ name.chomp("Component").underscore.dasherize.gsub("/", "--")
64
+ end
65
+
66
+ def stimulus_reflex?
67
+ helpers.controller.instance_variable_get(:@stimulus_reflex)
68
+ end
69
+
70
+ def component_controller(opts_or_tag = :div, opts = {}, &blk)
71
+ initialize_component
72
+
73
+ tag = :div
74
+ options = if opts_or_tag.is_a? Hash
75
+ opts_or_tag
76
+ else
77
+ tag = opts_or_tag
78
+ opts
79
+ end
80
+
81
+ options[:data] = {
82
+ controller: self.class.stimulus_controller,
83
+ key: key,
84
+ **(options[:data] || {})
85
+ }
86
+ content_tag tag, capture(&blk), options
87
+ end
88
+
89
+ def can_render_to_string?
90
+ omitted_from_state.empty?
91
+ end
92
+
93
+ # We can't truly initialize the component without the view_context,
94
+ # which isn't available in the `initialize` method. We require the
95
+ # developer to wrap components in `component_controller`, so this is where
96
+ # we truly initialize the component.
97
+ # This method is overridden in reflex.rb when the component is re-rendered. The
98
+ # override simply sets @key to element.dataset[:key]
99
+ # We don't want it to initialize the state again, and since we're rendering the component
100
+ # outside of the view, we need to skip the initialize_key method as well
101
+ def initialize_component
102
+ initialize_key
103
+ initialize_state
104
+ end
105
+
106
+ # Note to self:
107
+ # This has to be in the Component class because there are situations
108
+ # where the controller is the one rendering the component
109
+ # so we can't rely on the component created by the reflex
110
+ def initialize_state
111
+ return if state_initialized?
112
+ adapter = ViewComponentReflex::Engine.state_adapter
113
+
114
+ # newly mounted
115
+ if !stimulus_reflex? || adapter.state(request, @key).empty?
116
+
117
+ new_state = create_safe_state
118
+
119
+ adapter.wrap_write_async do
120
+ adapter.store_state(request, @key, new_state)
121
+ adapter.store_state(request, "#{@key}_initial", new_state)
122
+ end
123
+
124
+ # updating a mounted component
125
+ else
126
+ initial_state = adapter.state(request, "#{@key}_initial")
127
+
128
+ parameters_changed = []
129
+ adapter.state(request, @key).each do |k, v|
130
+ instance_value = instance_variable_get(k)
131
+ if permit_parameter?(initial_state[k], instance_value)
132
+ parameters_changed << k
133
+ adapter.wrap_write_async do
134
+ adapter.set_state(request, controller, "#{@key}_initial", {k => instance_value})
135
+ adapter.set_state(request, controller, @key, {k => instance_value})
136
+ end
137
+ else
138
+ instance_variable_set(k, v)
139
+ end
140
+ end
141
+ after_state_initialized(parameters_changed)
142
+ end
143
+ @state_initialized = true
144
+ end
145
+
146
+ def state_initialized?
147
+ @state_initialized
148
+ end
149
+
150
+ def initialize_key
151
+ # we want the erb file that renders the component. `caller` gives the file name,
152
+ # and line number, which should be unique. We hash it to make it a nice number
153
+ erb_file = caller.select { |p| p.match? /.\.html\.(haml|erb|slim)/ }[1]
154
+ key = if erb_file
155
+ Digest::SHA2.hexdigest(erb_file.split(":in")[0])
156
+ else
157
+ ""
158
+ end
159
+ key += collection_key.to_s if collection_key
160
+ @key = key
161
+ end
162
+
163
+ # Helper to use to create the proper reflex data attributes for an element
164
+ def reflex_data_attributes(reflex)
165
+ action, method = reflex.to_s.split("->")
166
+ if method.nil?
167
+ method = action
168
+ action = "click"
169
+ end
170
+
171
+ {
172
+ reflex: "#{action}->#{self.class.name}##{method}",
173
+ key: key
174
+ }
175
+ end
176
+
177
+ def reflex_tag(reflex, name, content_or_options_with_block = {}, options = {}, escape = true, &block)
178
+ if content_or_options_with_block.is_a?(Hash)
179
+ merge_data_attributes(content_or_options_with_block, reflex_data_attributes(reflex))
180
+ else
181
+ merge_data_attributes(options, reflex_data_attributes(reflex))
182
+ end
183
+ content_tag(name, content_or_options_with_block, options, escape, &block)
184
+ end
185
+
186
+ def collection_key
187
+ nil
188
+ end
189
+
190
+ def permit_parameter?(initial_param, new_param)
191
+ initial_param != new_param
192
+ end
193
+
194
+ def omitted_from_state
195
+ []
196
+ end
197
+
198
+ def after_state_initialized(parameters_changed)
199
+ # called after state component has been hydrated
200
+ end
201
+
202
+ # def receive_params(old_state, params)
203
+ # # no op
204
+ # end
205
+
206
+ def safe_instance_variables
207
+ instance_variables.reject { |ivar| ivar.start_with?("@__vc") } - unsafe_instance_variables - omitted_from_state
208
+ end
209
+
210
+ private
211
+
212
+ def unsafe_instance_variables
213
+ [
214
+ :@view_context, :@lookup_context, :@view_renderer, :@view_flow,
215
+ :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,
216
+ :@helpers, :@controller, :@request, :@tag_builder, :@state_initialized,
217
+ :@_content_evaluated, :@_render_in_block, :@__cached_content,
218
+ :@original_view_context,
219
+ ]
220
+ end
221
+
222
+ def content
223
+ if cached_content && !@_render_in_block
224
+ cached_content
225
+ else
226
+ super
227
+ end
228
+ end
229
+
230
+ def cached_content
231
+ @__cached_content__
232
+ end
233
+
234
+ def create_safe_state
235
+ new_state = {}
236
+
237
+ # this will almost certainly break
238
+ safe_instance_variables.each do |k|
239
+ new_state[k] = instance_variable_get(k)
240
+ end
241
+
242
+ new_state[:@__cached_content__] = content
243
+
244
+ new_state
245
+ end
246
+
247
+ def merge_data_attributes(options, attributes)
248
+ data = options[:data]
249
+ if data.nil?
250
+ options[:data] = attributes
251
+ else
252
+ options[:data].merge! attributes
253
+ end
254
+ end
255
+ end
256
+ end
@@ -1,37 +1,40 @@
1
- module ViewComponentReflex
2
- class Engine < ::Rails::Engine
3
-
4
- mattr_accessor :state_adapter
5
- Engine.state_adapter = StateAdapter::Session
6
-
7
- config.to_prepare do
8
- StimulusReflex::Channel.class_eval do
9
- unless instance_methods.include?(:receive_original)
10
- alias_method :receive_original, :receive
11
- def receive(data)
12
- target = data["target"].to_s
13
- reflex_name, _ = target.split("#")
14
- reflex_name = reflex_name.camelize
15
- component_name = reflex_name.end_with?("Reflex") ? reflex_name[0...-6] : reflex_name
16
- component = begin
17
- component_name.constantize
18
- rescue
19
- # Since every reflex runs through this monkey patch, we're just going to ignore the ones that aren't for components
20
- end
21
-
22
- if component&.respond_to?(:init_stimulus_reflex)
23
- component.init_stimulus_reflex
24
- else
25
- Rails.logger.info "Tried to initialize view_component_reflex on #{component_name}, but it's not a view_component_reflex"
26
- end
27
- receive_original(data)
28
- end
29
- end
30
- end
31
- end
32
-
33
- def self.configure
34
- yield self if block_given?
35
- end
36
- end
37
- end
1
+ module ViewComponentReflex
2
+ class Engine < ::Rails::Engine
3
+
4
+ mattr_accessor :state_adapter
5
+ Engine.state_adapter = StateAdapter::Session
6
+
7
+ config.to_prepare do
8
+ StimulusReflex::Channel.class_eval do
9
+ unless instance_methods.include?(:receive_original)
10
+ alias_method :receive_original, :receive
11
+ def receive(data)
12
+ target = data["target"].to_s
13
+ reflex_name, _ = target.split("#")
14
+ reflex_name = reflex_name.camelize
15
+ component_name = reflex_name.end_with?("Reflex") ? reflex_name[0...-6] : reflex_name
16
+ component = begin
17
+ component_name.constantize
18
+ rescue
19
+ # Since every reflex runs through this monkey patch, we're just going to ignore the ones that aren't for components
20
+ end
21
+
22
+ if component
23
+ if component.respond_to?(:init_stimulus_reflex)
24
+ component.init_stimulus_reflex
25
+ else
26
+ Rails.logger.info "Tried to initialize view_component_reflex on #{component_name}, but it's not a view_component_reflex"
27
+ end
28
+ end
29
+
30
+ receive_original(data)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.configure
37
+ yield self if block_given?
38
+ end
39
+ end
40
+ end