view_component 3.23.2 → 4.12.0
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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +11 -14
- data/app/controllers/view_components_system_test_controller.rb +25 -19
- data/app/views/test_mailer/test_asset_email.html.erb +1 -0
- data/app/views/test_mailer/test_url_email.html.erb +1 -0
- data/app/views/view_components/preview.html.erb +1 -9
- data/docs/CHANGELOG.md +612 -108
- data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
- data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +17 -4
- data/lib/{rails/generators → generators/view_component}/component/templates/component.rb.tt +6 -1
- data/lib/{rails/generators/erb/component_generator.rb → generators/view_component/erb/erb_generator.rb} +4 -3
- data/lib/{rails/generators/haml/component_generator.rb → generators/view_component/haml/haml_generator.rb} +3 -3
- data/lib/{rails/generators/locale/component_generator.rb → generators/view_component/locale/locale_generator.rb} +3 -3
- data/lib/{rails/generators/preview/component_generator.rb → generators/view_component/preview/preview_generator.rb} +3 -3
- data/lib/{rails/generators/rspec/component_generator.rb → generators/view_component/rspec/rspec_generator.rb} +3 -3
- data/lib/{rails/generators/slim/component_generator.rb → generators/view_component/slim/slim_generator.rb} +3 -3
- data/lib/{rails/generators/stimulus/component_generator.rb → generators/view_component/stimulus/stimulus_generator.rb} +3 -3
- data/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb +11 -0
- data/lib/{rails/generators/test_unit/component_generator.rb → generators/view_component/test_unit/test_unit_generator.rb} +2 -2
- data/lib/view_component/base.rb +252 -163
- data/lib/view_component/collection.rb +33 -35
- data/lib/view_component/compiler.rb +55 -80
- data/lib/view_component/config.rb +55 -85
- data/lib/view_component/configurable.rb +19 -3
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/engine.rb +46 -106
- data/lib/view_component/errors.rb +16 -34
- data/lib/view_component/inline_template.rb +4 -4
- data/lib/view_component/instrumentation.rb +21 -14
- data/lib/view_component/preview.rb +7 -12
- data/lib/view_component/request_details.rb +30 -0
- data/lib/view_component/slot.rb +7 -15
- data/lib/view_component/slotable.rb +94 -80
- data/lib/view_component/system_spec_helpers.rb +11 -0
- data/lib/view_component/system_test_helpers.rb +2 -2
- data/lib/view_component/template.rb +159 -83
- data/lib/view_component/test_helpers.rb +67 -50
- data/lib/view_component/translatable.rb +33 -32
- data/lib/view_component/version.rb +3 -3
- data/lib/view_component.rb +9 -6
- metadata +36 -556
- data/app/assets/vendor/prism.css +0 -4
- data/app/assets/vendor/prism.min.js +0 -12
- data/app/helpers/preview_helper.rb +0 -85
- data/app/views/view_components/_preview_source.html.erb +0 -17
- data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
- data/lib/view_component/capture_compatibility.rb +0 -44
- data/lib/view_component/component_error.rb +0 -6
- data/lib/view_component/rails/tasks/view_component.rake +0 -20
- data/lib/view_component/render_component_helper.rb +0 -10
- data/lib/view_component/render_component_to_string_helper.rb +0 -9
- data/lib/view_component/render_monkey_patch.rb +0 -13
- data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
- data/lib/view_component/rendering_component_helper.rb +0 -9
- data/lib/view_component/rendering_monkey_patch.rb +0 -13
- data/lib/view_component/slotable_default.rb +0 -20
- data/lib/view_component/use_helpers.rb +0 -42
- /data/lib/{rails/generators → generators/view_component}/erb/templates/component.html.erb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/haml/templates/component.html.haml.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/preview/templates/component_preview.rb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/rspec/templates/component_spec.rb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/slim/templates/component.html.slim.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.js.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.ts.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/tailwindcss/templates/component.html.erb.tt +0 -0
- /data/lib/{rails/generators → generators/view_component}/test_unit/templates/component_test.rb.tt +0 -0
|
@@ -12,12 +12,10 @@ module ViewComponent
|
|
|
12
12
|
singular: %i[content render].freeze,
|
|
13
13
|
plural: %i[contents renders].freeze
|
|
14
14
|
}.freeze
|
|
15
|
+
private_constant :RESERVED_NAMES
|
|
15
16
|
|
|
16
|
-
# Setup component slot state
|
|
17
17
|
included do
|
|
18
|
-
|
|
19
|
-
class_attribute :registered_slots
|
|
20
|
-
self.registered_slots = {}
|
|
18
|
+
class_attribute :registered_slots, default: {}
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
class_methods do
|
|
@@ -75,26 +73,25 @@ module ViewComponent
|
|
|
75
73
|
#
|
|
76
74
|
# <%= render_inline(MyComponent.new.with_header_content("Foo")) %>
|
|
77
75
|
def renders_one(slot_name, callable = nil)
|
|
78
|
-
|
|
76
|
+
__vc_validate_singular_slot_name(slot_name)
|
|
79
77
|
|
|
80
78
|
if callable.is_a?(Hash) && callable.key?(:types)
|
|
81
|
-
|
|
79
|
+
__vc_register_polymorphic_slot(slot_name, callable[:types], collection: false)
|
|
82
80
|
else
|
|
83
|
-
|
|
81
|
+
__vc_validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
|
|
84
82
|
|
|
85
83
|
setter_method_name = :"with_#{slot_name}"
|
|
86
84
|
|
|
87
|
-
define_method setter_method_name do |*args, &block|
|
|
88
|
-
|
|
85
|
+
define_method setter_method_name do |*args, **kwargs, &block|
|
|
86
|
+
__vc_set_slot(slot_name, nil, *args, **kwargs, &block)
|
|
89
87
|
end
|
|
90
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
|
91
88
|
|
|
92
89
|
self::GeneratedSlotMethods.define_method slot_name do
|
|
93
|
-
|
|
90
|
+
__vc_get_slot(slot_name)
|
|
94
91
|
end
|
|
95
92
|
|
|
96
93
|
self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
|
|
97
|
-
|
|
94
|
+
__vc_get_slot(slot_name).present?
|
|
98
95
|
end
|
|
99
96
|
|
|
100
97
|
define_method :"with_#{slot_name}_content" do |content|
|
|
@@ -103,7 +100,7 @@ module ViewComponent
|
|
|
103
100
|
self
|
|
104
101
|
end
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
__vc_register_slot(slot_name, collection: false, callable: callable)
|
|
107
104
|
end
|
|
108
105
|
end
|
|
109
106
|
|
|
@@ -145,20 +142,19 @@ module ViewComponent
|
|
|
145
142
|
# <% end %>
|
|
146
143
|
# <% end %>
|
|
147
144
|
def renders_many(slot_name, callable = nil)
|
|
148
|
-
|
|
145
|
+
__vc_validate_plural_slot_name(slot_name)
|
|
149
146
|
|
|
150
147
|
if callable.is_a?(Hash) && callable.key?(:types)
|
|
151
|
-
|
|
148
|
+
__vc_register_polymorphic_slot(slot_name, callable[:types], collection: true)
|
|
152
149
|
else
|
|
153
150
|
singular_name = ActiveSupport::Inflector.singularize(slot_name)
|
|
154
|
-
|
|
151
|
+
__vc_validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
|
|
155
152
|
|
|
156
153
|
setter_method_name = :"with_#{singular_name}"
|
|
157
154
|
|
|
158
|
-
define_method setter_method_name do |*args, &block|
|
|
159
|
-
|
|
155
|
+
define_method setter_method_name do |*args, **kwargs, &block|
|
|
156
|
+
__vc_set_slot(slot_name, nil, *args, **kwargs, &block)
|
|
160
157
|
end
|
|
161
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
|
162
158
|
|
|
163
159
|
define_method :"with_#{singular_name}_content" do |content|
|
|
164
160
|
send(setter_method_name) { content.to_s }
|
|
@@ -169,22 +165,22 @@ module ViewComponent
|
|
|
169
165
|
define_method :"with_#{slot_name}" do |collection_args = nil, &block|
|
|
170
166
|
collection_args.map do |args|
|
|
171
167
|
if args.respond_to?(:to_hash)
|
|
172
|
-
|
|
168
|
+
__vc_set_slot(slot_name, nil, **args, &block)
|
|
173
169
|
else
|
|
174
|
-
|
|
170
|
+
__vc_set_slot(slot_name, nil, *args, &block)
|
|
175
171
|
end
|
|
176
172
|
end
|
|
177
173
|
end
|
|
178
174
|
|
|
179
175
|
self::GeneratedSlotMethods.define_method slot_name do
|
|
180
|
-
|
|
176
|
+
__vc_get_slot(slot_name)
|
|
181
177
|
end
|
|
182
178
|
|
|
183
179
|
self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
|
|
184
|
-
|
|
180
|
+
__vc_get_slot(slot_name).present?
|
|
185
181
|
end
|
|
186
182
|
|
|
187
|
-
|
|
183
|
+
__vc_register_slot(slot_name, collection: true, callable: callable)
|
|
188
184
|
end
|
|
189
185
|
end
|
|
190
186
|
|
|
@@ -215,13 +211,30 @@ module ViewComponent
|
|
|
215
211
|
super
|
|
216
212
|
end
|
|
217
213
|
|
|
218
|
-
|
|
214
|
+
# @private
|
|
215
|
+
# Called by the compiler, as instance methods are not defined when slots are first registered
|
|
216
|
+
def __vc_register_default_slots
|
|
217
|
+
registered_slots.each do |slot_name, config|
|
|
218
|
+
default_method_name = :"default_#{slot_name}"
|
|
219
|
+
config[:default_method] = instance_methods.find { |method_name| method_name == default_method_name }
|
|
220
|
+
|
|
221
|
+
registered_slots[slot_name] = config
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def __vc_register_slot(slot_name, **kwargs)
|
|
228
|
+
registered_slots[slot_name] = __vc_define_slot(slot_name, **kwargs)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def __vc_register_polymorphic_slot(slot_name, types, collection:)
|
|
219
232
|
self::GeneratedSlotMethods.define_method(slot_name) do
|
|
220
|
-
|
|
233
|
+
__vc_get_slot(slot_name)
|
|
221
234
|
end
|
|
222
235
|
|
|
223
236
|
self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do
|
|
224
|
-
|
|
237
|
+
__vc_get_slot(slot_name).present?
|
|
225
238
|
end
|
|
226
239
|
|
|
227
240
|
renderable_hash = types.each_with_object({}) do |(poly_type, poly_attributes_or_callable), memo|
|
|
@@ -240,20 +253,19 @@ module ViewComponent
|
|
|
240
253
|
"#{slot_name}_#{poly_type}"
|
|
241
254
|
end
|
|
242
255
|
|
|
243
|
-
memo[poly_type] =
|
|
256
|
+
memo[poly_type] = __vc_define_slot(
|
|
244
257
|
poly_slot_name, collection: collection, callable: poly_callable
|
|
245
258
|
)
|
|
246
259
|
|
|
247
260
|
setter_method_name = :"with_#{poly_slot_name}"
|
|
248
261
|
|
|
249
|
-
if
|
|
262
|
+
if method_defined?(setter_method_name)
|
|
250
263
|
raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name)
|
|
251
264
|
end
|
|
252
265
|
|
|
253
|
-
define_method(setter_method_name) do |*args, &block|
|
|
254
|
-
|
|
266
|
+
define_method(setter_method_name) do |*args, **kwargs, &block|
|
|
267
|
+
__vc_set_polymorphic_slot(slot_name, poly_type, *args, **kwargs, &block)
|
|
255
268
|
end
|
|
256
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
|
257
269
|
|
|
258
270
|
define_method :"with_#{poly_slot_name}_content" do |content|
|
|
259
271
|
send(setter_method_name) { content.to_s }
|
|
@@ -268,22 +280,7 @@ module ViewComponent
|
|
|
268
280
|
}
|
|
269
281
|
end
|
|
270
282
|
|
|
271
|
-
|
|
272
|
-
def register_default_slots
|
|
273
|
-
registered_slots.each do |slot_name, config|
|
|
274
|
-
config[:default_method] = instance_methods.find { |method_name| method_name == :"default_#{slot_name}" }
|
|
275
|
-
|
|
276
|
-
registered_slots[slot_name] = config
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
private
|
|
281
|
-
|
|
282
|
-
def register_slot(slot_name, **kwargs)
|
|
283
|
-
registered_slots[slot_name] = define_slot(slot_name, **kwargs)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def define_slot(slot_name, collection:, callable:)
|
|
283
|
+
def __vc_define_slot(slot_name, collection:, callable:)
|
|
287
284
|
slot = {collection: collection}
|
|
288
285
|
return slot unless callable
|
|
289
286
|
|
|
@@ -306,18 +303,18 @@ module ViewComponent
|
|
|
306
303
|
slot
|
|
307
304
|
end
|
|
308
305
|
|
|
309
|
-
def
|
|
306
|
+
def __vc_validate_plural_slot_name(slot_name)
|
|
310
307
|
if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
|
|
311
308
|
raise ReservedPluralSlotNameError.new(name, slot_name)
|
|
312
309
|
end
|
|
313
310
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
__vc_raise_if_slot_name_uncountable(slot_name)
|
|
312
|
+
__vc_raise_if_slot_conflicts_with_call(slot_name)
|
|
313
|
+
__vc_raise_if_slot_ends_with_question_mark(slot_name)
|
|
314
|
+
__vc_raise_if_slot_registered(slot_name)
|
|
318
315
|
end
|
|
319
316
|
|
|
320
|
-
def
|
|
317
|
+
def __vc_validate_singular_slot_name(slot_name)
|
|
321
318
|
if slot_name.to_sym == :content
|
|
322
319
|
raise ContentSlotNameError.new(name)
|
|
323
320
|
end
|
|
@@ -326,28 +323,28 @@ module ViewComponent
|
|
|
326
323
|
raise ReservedSingularSlotNameError.new(name, slot_name)
|
|
327
324
|
end
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
__vc_raise_if_slot_conflicts_with_call(slot_name)
|
|
327
|
+
__vc_raise_if_slot_ends_with_question_mark(slot_name)
|
|
328
|
+
__vc_raise_if_slot_registered(slot_name)
|
|
332
329
|
end
|
|
333
330
|
|
|
334
|
-
def
|
|
331
|
+
def __vc_raise_if_slot_registered(slot_name)
|
|
335
332
|
if registered_slots.key?(slot_name)
|
|
336
333
|
raise RedefinedSlotError.new(name, slot_name)
|
|
337
334
|
end
|
|
338
335
|
end
|
|
339
336
|
|
|
340
|
-
def
|
|
337
|
+
def __vc_raise_if_slot_ends_with_question_mark(slot_name)
|
|
341
338
|
raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.end_with?("?")
|
|
342
339
|
end
|
|
343
340
|
|
|
344
|
-
def
|
|
341
|
+
def __vc_raise_if_slot_conflicts_with_call(slot_name)
|
|
345
342
|
if slot_name.start_with?("call_")
|
|
346
343
|
raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}"
|
|
347
344
|
end
|
|
348
345
|
end
|
|
349
346
|
|
|
350
|
-
def
|
|
347
|
+
def __vc_raise_if_slot_name_uncountable(slot_name)
|
|
351
348
|
slot_name = slot_name.to_s
|
|
352
349
|
if slot_name.pluralize == slot_name.singularize
|
|
353
350
|
raise UncountableSlotNameError.new(name, slot_name)
|
|
@@ -355,24 +352,35 @@ module ViewComponent
|
|
|
355
352
|
end
|
|
356
353
|
end
|
|
357
354
|
|
|
358
|
-
def
|
|
359
|
-
content unless content_evaluated? # ensure content is loaded so slots will be defined
|
|
360
|
-
|
|
361
|
-
slot = self.class.registered_slots[slot_name]
|
|
355
|
+
def __vc_get_slot(slot_name)
|
|
362
356
|
@__vc_set_slots ||= {}
|
|
357
|
+
content unless defined?(@__vc_content_evaluated) && @__vc_content_evaluated # ensure content is loaded so slots will be defined
|
|
363
358
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
end
|
|
359
|
+
# If the slot is set, return it
|
|
360
|
+
return @__vc_set_slots[slot_name] if @__vc_set_slots[slot_name]
|
|
367
361
|
|
|
368
|
-
|
|
362
|
+
# If there is a default method for the slot, call it
|
|
363
|
+
if (default_method = registered_slots[slot_name][:default_method])
|
|
364
|
+
renderable_value = send(default_method)
|
|
365
|
+
slot = Slot.new(self)
|
|
366
|
+
|
|
367
|
+
if renderable_value.respond_to?(:render_in)
|
|
368
|
+
slot.__vc_component_instance = renderable_value
|
|
369
|
+
else
|
|
370
|
+
slot.__vc_content = renderable_value
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
slot
|
|
374
|
+
elsif self.class.registered_slots[slot_name][:collection]
|
|
375
|
+
# If empty slot is a collection, return an empty array
|
|
369
376
|
[]
|
|
370
377
|
end
|
|
371
378
|
end
|
|
372
379
|
|
|
373
|
-
def
|
|
380
|
+
def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
|
|
374
381
|
slot_definition ||= self.class.registered_slots[slot_name]
|
|
375
382
|
slot = Slot.new(self)
|
|
383
|
+
captured_block_virtual_path = nil
|
|
376
384
|
|
|
377
385
|
# Passing the block to the sub-component wrapper like this has two
|
|
378
386
|
# benefits:
|
|
@@ -383,15 +391,21 @@ module ViewComponent
|
|
|
383
391
|
# 2. Since we have to pass block content to components when calling
|
|
384
392
|
# `render`, evaluating the block here would require us to call
|
|
385
393
|
# `view_context.capture` twice, which is slower
|
|
386
|
-
|
|
394
|
+
if block
|
|
395
|
+
slot.__vc_content_block = block
|
|
396
|
+
# Capture the virtual path at the time the block is defined, so that
|
|
397
|
+
# translations resolve relative to where the block was created, not where it's rendered
|
|
398
|
+
captured_block_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
399
|
+
slot.__vc_content_block_virtual_path = captured_block_virtual_path
|
|
400
|
+
end
|
|
387
401
|
|
|
388
402
|
# If class
|
|
389
403
|
if slot_definition[:renderable]
|
|
390
|
-
slot.__vc_component_instance = slot_definition[:renderable].new(*args)
|
|
404
|
+
slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs)
|
|
391
405
|
# If class name as a string
|
|
392
406
|
elsif slot_definition[:renderable_class_name]
|
|
393
407
|
slot.__vc_component_instance =
|
|
394
|
-
self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
|
|
408
|
+
self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
|
|
395
409
|
# If passed a lambda
|
|
396
410
|
elsif slot_definition[:renderable_function]
|
|
397
411
|
# Use `bind(self)` to ensure lambda is executed in the context of the
|
|
@@ -400,11 +414,13 @@ module ViewComponent
|
|
|
400
414
|
renderable_function = slot_definition[:renderable_function].bind(self)
|
|
401
415
|
renderable_value =
|
|
402
416
|
if block
|
|
403
|
-
renderable_function.call(*args) do |*rargs|
|
|
404
|
-
|
|
417
|
+
renderable_function.call(*args, **kwargs) do |*rargs|
|
|
418
|
+
with_captured_virtual_path(captured_block_virtual_path) do
|
|
419
|
+
view_context.capture(*rargs, &block)
|
|
420
|
+
end
|
|
405
421
|
end
|
|
406
422
|
else
|
|
407
|
-
renderable_function.call(*args)
|
|
423
|
+
renderable_function.call(*args, **kwargs)
|
|
408
424
|
end
|
|
409
425
|
|
|
410
426
|
# Function calls can return components, so if it's a component handle it specially
|
|
@@ -426,9 +442,8 @@ module ViewComponent
|
|
|
426
442
|
|
|
427
443
|
slot
|
|
428
444
|
end
|
|
429
|
-
ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
|
|
430
445
|
|
|
431
|
-
def
|
|
446
|
+
def __vc_set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block)
|
|
432
447
|
slot_definition = self.class.registered_slots[slot_name]
|
|
433
448
|
|
|
434
449
|
if !slot_definition[:collection] && defined?(@__vc_set_slots) && @__vc_set_slots[slot_name]
|
|
@@ -437,8 +452,7 @@ module ViewComponent
|
|
|
437
452
|
|
|
438
453
|
poly_def = slot_definition[:renderable_hash][poly_type]
|
|
439
454
|
|
|
440
|
-
|
|
455
|
+
__vc_set_slot(slot_name, poly_def, *args, **kwargs, &block)
|
|
441
456
|
end
|
|
442
|
-
ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
|
|
443
457
|
end
|
|
444
458
|
end
|
|
@@ -4,10 +4,10 @@ module ViewComponent
|
|
|
4
4
|
module SystemTestHelpers
|
|
5
5
|
include TestHelpers
|
|
6
6
|
|
|
7
|
-
#
|
|
8
7
|
# Returns a block that can be used to visit the path of the inline rendered component.
|
|
9
8
|
# @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`.
|
|
10
9
|
# @param layout [String] The (optional) layout to use.
|
|
10
|
+
#
|
|
11
11
|
# @return [Proc] A block that can be used to visit the path of the inline rendered component.
|
|
12
12
|
def with_rendered_component_path(fragment, layout: false, &block)
|
|
13
13
|
file = Tempfile.new(
|
|
@@ -18,7 +18,7 @@ module ViewComponent
|
|
|
18
18
|
file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
|
|
19
19
|
file.rewind
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
yield("/_system_test_entrypoint?file=#{file.path.split("/").last}")
|
|
22
22
|
ensure
|
|
23
23
|
file.unlink
|
|
24
24
|
end
|
|
@@ -2,69 +2,168 @@
|
|
|
2
2
|
|
|
3
3
|
module ViewComponent
|
|
4
4
|
class Template
|
|
5
|
+
DEFAULT_FORMAT = :html
|
|
6
|
+
private_constant :DEFAULT_FORMAT
|
|
7
|
+
|
|
5
8
|
DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this_format: nil,
|
|
14
|
-
variant: nil,
|
|
15
|
-
lineno: nil,
|
|
16
|
-
path: nil,
|
|
17
|
-
extension: nil,
|
|
18
|
-
source: nil,
|
|
19
|
-
method_name: nil,
|
|
20
|
-
defined_on_self: true
|
|
21
|
-
)
|
|
9
|
+
|
|
10
|
+
attr_reader :details, :path
|
|
11
|
+
|
|
12
|
+
delegate :virtual_path, to: :@component
|
|
13
|
+
delegate :format, :variant, to: :@details
|
|
14
|
+
|
|
15
|
+
def initialize(component:, details:, lineno: nil, path: nil)
|
|
22
16
|
@component = component
|
|
23
|
-
@
|
|
24
|
-
@this_format = this_format
|
|
25
|
-
@variant = variant&.to_sym
|
|
17
|
+
@details = details
|
|
26
18
|
@lineno = lineno
|
|
27
19
|
@path = path
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class File < Template
|
|
23
|
+
def initialize(component:, details:, path:)
|
|
24
|
+
# If the template file has no format (e.g. .erb instead of .html.erb),
|
|
25
|
+
# assume the default format (html).
|
|
26
|
+
if details.format.nil?
|
|
27
|
+
Kernel.warn("WARNING: Template format for #{path} is missing, defaulting to :html.")
|
|
28
|
+
details = ActionView::TemplateDetails.new(details.locale, details.handler, DEFAULT_FORMAT, details.variant)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@strip_annotation_line = false
|
|
32
|
+
|
|
33
|
+
# Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
|
|
34
|
+
# Use -1 to compensate for correct line numbers in stack traces.
|
|
35
|
+
# However, negative line numbers cause segfaults when Ruby's coverage
|
|
36
|
+
# is enabled (bugs.ruby-lang.org/issues/19363). In that case, strip the
|
|
37
|
+
# annotation line from compiled source instead.
|
|
38
|
+
lineno =
|
|
39
|
+
if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
|
|
40
|
+
if coverage_running?
|
|
41
|
+
# Can't use negative lineno with coverage (causes segfault on Linux).
|
|
42
|
+
# Strip annotation line if enabled to preserve correct line numbers.
|
|
43
|
+
@strip_annotation_line = ActionView::Base.annotate_rendered_view_with_filenames
|
|
44
|
+
0
|
|
45
|
+
else
|
|
46
|
+
-1
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
super(
|
|
53
|
+
component: component,
|
|
54
|
+
details: details,
|
|
55
|
+
path: path,
|
|
56
|
+
lineno: lineno
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def type
|
|
61
|
+
:file
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Load file each time we look up #source in case the file has been modified
|
|
65
|
+
def source
|
|
66
|
+
::File.read(@path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def compiled_source
|
|
72
|
+
result = super
|
|
73
|
+
# Strip the annotation line to maintain correct line numbers when coverage
|
|
74
|
+
# is running (avoids segfault from negative lineno)
|
|
75
|
+
result = result.partition(";").last if @strip_annotation_line
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class Inline < Template
|
|
81
|
+
attr_reader :source
|
|
82
|
+
|
|
83
|
+
def initialize(component:, inline_template:)
|
|
84
|
+
details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil)
|
|
85
|
+
|
|
86
|
+
# Rails 8.1 added a newline to compiled ERB output (rails/rails#53731).
|
|
87
|
+
# Subtract 1 to compensate for correct line numbers in stack traces.
|
|
88
|
+
# Inline templates start at line 2+ (defined inside a class), so this
|
|
89
|
+
# won't result in negative line numbers that cause segfaults with coverage.
|
|
90
|
+
lineno =
|
|
91
|
+
if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
|
|
92
|
+
inline_template.lineno - 1
|
|
93
|
+
else
|
|
94
|
+
inline_template.lineno
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
super(
|
|
98
|
+
component: component,
|
|
99
|
+
details: details,
|
|
100
|
+
path: inline_template.path,
|
|
101
|
+
lineno: lineno,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@source = inline_template.source.dup
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def type
|
|
108
|
+
:inline
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class InlineCall < Template
|
|
113
|
+
def initialize(component:, method_name:, defined_on_self:)
|
|
114
|
+
variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil
|
|
115
|
+
details = ActionView::TemplateDetails.new(nil, nil, nil, variant)
|
|
116
|
+
|
|
117
|
+
super(component: component, details: details)
|
|
118
|
+
|
|
119
|
+
@call_method_name = method_name
|
|
120
|
+
@defined_on_self = defined_on_self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def type
|
|
124
|
+
:inline_call
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def compile_to_component
|
|
128
|
+
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def safe_method_name_call
|
|
132
|
+
m = safe_method_name
|
|
133
|
+
proc do
|
|
134
|
+
__vc_maybe_escape_html(send(m)) do
|
|
135
|
+
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \
|
|
136
|
+
"The output will be automatically escaped, but you may want to investigate.")
|
|
137
|
+
end
|
|
43
138
|
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def defined_on_self?
|
|
142
|
+
@defined_on_self
|
|
143
|
+
end
|
|
44
144
|
end
|
|
45
145
|
|
|
46
146
|
def compile_to_component
|
|
47
|
-
|
|
48
|
-
@component.silence_redefinition_of_method(@call_method_name)
|
|
147
|
+
@component.silence_redefinition_of_method(call_method_name)
|
|
49
148
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def #{
|
|
149
|
+
# rubocop:disable Style/EvalWithLocation
|
|
150
|
+
@component.class_eval <<~RUBY, @path, @lineno
|
|
151
|
+
def #{call_method_name}
|
|
53
152
|
#{compiled_source}
|
|
54
153
|
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
end
|
|
154
|
+
RUBY
|
|
155
|
+
# rubocop:enable Style/EvalWithLocation
|
|
58
156
|
|
|
59
157
|
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
|
|
60
158
|
end
|
|
61
159
|
|
|
62
|
-
def
|
|
63
|
-
|
|
160
|
+
def coverage_running?
|
|
161
|
+
defined?(Coverage) && Coverage.running?
|
|
162
|
+
end
|
|
64
163
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
164
|
+
def safe_method_name_call
|
|
165
|
+
m = safe_method_name
|
|
166
|
+
proc { send(m) }
|
|
68
167
|
end
|
|
69
168
|
|
|
70
169
|
def requires_compiled_superclass?
|
|
@@ -72,63 +171,40 @@ module ViewComponent
|
|
|
72
171
|
end
|
|
73
172
|
|
|
74
173
|
def inline_call?
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def inline?
|
|
79
|
-
@type == :inline
|
|
174
|
+
type == :inline_call
|
|
80
175
|
end
|
|
81
176
|
|
|
82
177
|
def default_format?
|
|
83
|
-
|
|
178
|
+
format.nil? || format == DEFAULT_FORMAT
|
|
84
179
|
end
|
|
180
|
+
alias_method :html?, :default_format?
|
|
85
181
|
|
|
86
|
-
def
|
|
87
|
-
@
|
|
182
|
+
def call_method_name
|
|
183
|
+
@call_method_name ||=
|
|
184
|
+
["call", (normalized_variant_name if variant.present?), (format unless default_format?)]
|
|
185
|
+
.compact.join("_").to_sym
|
|
88
186
|
end
|
|
89
187
|
|
|
90
188
|
def safe_method_name
|
|
91
|
-
"_#{
|
|
189
|
+
"_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
|
|
92
190
|
end
|
|
93
191
|
|
|
94
192
|
def normalized_variant_name
|
|
95
|
-
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def defined_on_self?
|
|
99
|
-
@defined_on_self
|
|
193
|
+
variant.to_s.gsub("-", "__")
|
|
100
194
|
end
|
|
101
195
|
|
|
102
196
|
private
|
|
103
197
|
|
|
104
|
-
def source
|
|
105
|
-
if @source_originally_nil
|
|
106
|
-
# Load file each time we look up #source in case the file has been modified
|
|
107
|
-
File.read(@path)
|
|
108
|
-
else
|
|
109
|
-
@source
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
198
|
def compiled_source
|
|
114
|
-
handler =
|
|
199
|
+
handler = details.handler_class
|
|
115
200
|
this_source = source
|
|
116
201
|
this_source.rstrip! if @component.strip_trailing_whitespace?
|
|
117
202
|
|
|
118
203
|
short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path
|
|
119
|
-
|
|
204
|
+
format = self.format || DEFAULT_FORMAT
|
|
205
|
+
type = ActionView::Template::Types[format]
|
|
120
206
|
|
|
121
|
-
|
|
122
|
-
handler.call(
|
|
123
|
-
DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type),
|
|
124
|
-
this_source
|
|
125
|
-
)
|
|
126
|
-
# :nocov:
|
|
127
|
-
# TODO: Remove in v4
|
|
128
|
-
else
|
|
129
|
-
handler.call(DataNoSource.new(source: this_source, identifier: @path, type: type))
|
|
130
|
-
end
|
|
131
|
-
# :nocov:
|
|
207
|
+
handler.call(DataWithSource.new(format:, identifier: @path, short_identifier:, type:), this_source)
|
|
132
208
|
end
|
|
133
209
|
end
|
|
134
210
|
end
|