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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +11 -14
  3. data/app/controllers/view_components_system_test_controller.rb +25 -19
  4. data/app/views/test_mailer/test_asset_email.html.erb +1 -0
  5. data/app/views/test_mailer/test_url_email.html.erb +1 -0
  6. data/app/views/view_components/preview.html.erb +1 -9
  7. data/docs/CHANGELOG.md +612 -108
  8. data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
  9. data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +17 -4
  10. data/lib/{rails/generators → generators/view_component}/component/templates/component.rb.tt +6 -1
  11. data/lib/{rails/generators/erb/component_generator.rb → generators/view_component/erb/erb_generator.rb} +4 -3
  12. data/lib/{rails/generators/haml/component_generator.rb → generators/view_component/haml/haml_generator.rb} +3 -3
  13. data/lib/{rails/generators/locale/component_generator.rb → generators/view_component/locale/locale_generator.rb} +3 -3
  14. data/lib/{rails/generators/preview/component_generator.rb → generators/view_component/preview/preview_generator.rb} +3 -3
  15. data/lib/{rails/generators/rspec/component_generator.rb → generators/view_component/rspec/rspec_generator.rb} +3 -3
  16. data/lib/{rails/generators/slim/component_generator.rb → generators/view_component/slim/slim_generator.rb} +3 -3
  17. data/lib/{rails/generators/stimulus/component_generator.rb → generators/view_component/stimulus/stimulus_generator.rb} +3 -3
  18. data/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb +11 -0
  19. data/lib/{rails/generators/test_unit/component_generator.rb → generators/view_component/test_unit/test_unit_generator.rb} +2 -2
  20. data/lib/view_component/base.rb +252 -163
  21. data/lib/view_component/collection.rb +33 -35
  22. data/lib/view_component/compiler.rb +55 -80
  23. data/lib/view_component/config.rb +55 -85
  24. data/lib/view_component/configurable.rb +19 -3
  25. data/lib/view_component/deprecation.rb +1 -1
  26. data/lib/view_component/engine.rb +46 -106
  27. data/lib/view_component/errors.rb +16 -34
  28. data/lib/view_component/inline_template.rb +4 -4
  29. data/lib/view_component/instrumentation.rb +21 -14
  30. data/lib/view_component/preview.rb +7 -12
  31. data/lib/view_component/request_details.rb +30 -0
  32. data/lib/view_component/slot.rb +7 -15
  33. data/lib/view_component/slotable.rb +94 -80
  34. data/lib/view_component/system_spec_helpers.rb +11 -0
  35. data/lib/view_component/system_test_helpers.rb +2 -2
  36. data/lib/view_component/template.rb +159 -83
  37. data/lib/view_component/test_helpers.rb +67 -50
  38. data/lib/view_component/translatable.rb +33 -32
  39. data/lib/view_component/version.rb +3 -3
  40. data/lib/view_component.rb +9 -6
  41. metadata +36 -556
  42. data/app/assets/vendor/prism.css +0 -4
  43. data/app/assets/vendor/prism.min.js +0 -12
  44. data/app/helpers/preview_helper.rb +0 -85
  45. data/app/views/view_components/_preview_source.html.erb +0 -17
  46. data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
  47. data/lib/view_component/capture_compatibility.rb +0 -44
  48. data/lib/view_component/component_error.rb +0 -6
  49. data/lib/view_component/rails/tasks/view_component.rake +0 -20
  50. data/lib/view_component/render_component_helper.rb +0 -10
  51. data/lib/view_component/render_component_to_string_helper.rb +0 -9
  52. data/lib/view_component/render_monkey_patch.rb +0 -13
  53. data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
  54. data/lib/view_component/rendering_component_helper.rb +0 -9
  55. data/lib/view_component/rendering_monkey_patch.rb +0 -13
  56. data/lib/view_component/slotable_default.rb +0 -20
  57. data/lib/view_component/use_helpers.rb +0 -42
  58. /data/lib/{rails/generators → generators/view_component}/erb/templates/component.html.erb.tt +0 -0
  59. /data/lib/{rails/generators → generators/view_component}/haml/templates/component.html.haml.tt +0 -0
  60. /data/lib/{rails/generators → generators/view_component}/preview/templates/component_preview.rb.tt +0 -0
  61. /data/lib/{rails/generators → generators/view_component}/rspec/templates/component_spec.rb.tt +0 -0
  62. /data/lib/{rails/generators → generators/view_component}/slim/templates/component.html.slim.tt +0 -0
  63. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.js.tt +0 -0
  64. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.ts.tt +0 -0
  65. /data/lib/{rails/generators → generators/view_component}/tailwindcss/templates/component.html.erb.tt +0 -0
  66. /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
- # Hash of registered Slots
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
- validate_singular_slot_name(slot_name)
76
+ __vc_validate_singular_slot_name(slot_name)
79
77
 
80
78
  if callable.is_a?(Hash) && callable.key?(:types)
81
- register_polymorphic_slot(slot_name, callable[:types], collection: false)
79
+ __vc_register_polymorphic_slot(slot_name, callable[:types], collection: false)
82
80
  else
83
- validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
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
- set_slot(slot_name, nil, *args, &block)
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
- get_slot(slot_name)
90
+ __vc_get_slot(slot_name)
94
91
  end
95
92
 
96
93
  self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
97
- get_slot(slot_name).present?
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
- register_slot(slot_name, collection: false, callable: callable)
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
- validate_plural_slot_name(slot_name)
145
+ __vc_validate_plural_slot_name(slot_name)
149
146
 
150
147
  if callable.is_a?(Hash) && callable.key?(:types)
151
- register_polymorphic_slot(slot_name, callable[:types], collection: true)
148
+ __vc_register_polymorphic_slot(slot_name, callable[:types], collection: true)
152
149
  else
153
150
  singular_name = ActiveSupport::Inflector.singularize(slot_name)
154
- validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
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
- set_slot(slot_name, nil, *args, &block)
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
- set_slot(slot_name, nil, **args, &block)
168
+ __vc_set_slot(slot_name, nil, **args, &block)
173
169
  else
174
- set_slot(slot_name, nil, *args, &block)
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
- get_slot(slot_name)
176
+ __vc_get_slot(slot_name)
181
177
  end
182
178
 
183
179
  self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
184
- get_slot(slot_name).present?
180
+ __vc_get_slot(slot_name).present?
185
181
  end
186
182
 
187
- register_slot(slot_name, collection: true, callable: callable)
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
- def register_polymorphic_slot(slot_name, types, collection:)
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
- get_slot(slot_name)
233
+ __vc_get_slot(slot_name)
221
234
  end
222
235
 
223
236
  self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do
224
- get_slot(slot_name).present?
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] = define_slot(
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 instance_methods.include?(setter_method_name)
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
- set_polymorphic_slot(slot_name, poly_type, *args, &block)
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
- # Called by the compiler, as instance methods are not defined when slots are first registered
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 validate_plural_slot_name(slot_name)
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
- raise_if_slot_name_uncountable(slot_name)
315
- raise_if_slot_conflicts_with_call(slot_name)
316
- raise_if_slot_ends_with_question_mark(slot_name)
317
- raise_if_slot_registered(slot_name)
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 validate_singular_slot_name(slot_name)
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
- raise_if_slot_conflicts_with_call(slot_name)
330
- raise_if_slot_ends_with_question_mark(slot_name)
331
- raise_if_slot_registered(slot_name)
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 raise_if_slot_registered(slot_name)
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 raise_if_slot_ends_with_question_mark(slot_name)
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 raise_if_slot_conflicts_with_call(slot_name)
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 raise_if_slot_name_uncountable(slot_name)
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 get_slot(slot_name)
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
- if @__vc_set_slots[slot_name]
365
- return @__vc_set_slots[slot_name]
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
- if slot[:collection]
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 set_slot(slot_name, slot_definition = nil, *args, &block)
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
- slot.__vc_content_block = block if block
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
- view_context.capture(*rargs, &block)
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 set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
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
- set_slot(slot_name, poly_def, *args, &block)
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module SystemSpecHelpers
5
+ include SystemTestHelpers
6
+
7
+ def page
8
+ Capybara.current_session
9
+ end
10
+ end
11
+ 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
- block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
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
- DataNoSource = Struct.new(:source, :identifier, :type, keyword_init: true)
7
-
8
- attr_reader :variant, :this_format, :type
9
-
10
- def initialize(
11
- component:,
12
- type:,
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
- @type = type
24
- @this_format = this_format
25
- @variant = variant&.to_sym
17
+ @details = details
26
18
  @lineno = lineno
27
19
  @path = path
28
- @extension = extension
29
- @source = source
30
- @method_name = method_name
31
- @defined_on_self = defined_on_self
32
-
33
- @source_originally_nil = @source.nil?
34
-
35
- @call_method_name =
36
- if @method_name
37
- @method_name
38
- else
39
- out = +"call"
40
- out << "_#{normalized_variant_name}" if @variant.present?
41
- out << "_#{@this_format}" if @this_format.present? && @this_format != ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
42
- out
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
- if !inline_call?
48
- @component.silence_redefinition_of_method(@call_method_name)
147
+ @component.silence_redefinition_of_method(call_method_name)
49
148
 
50
- # rubocop:disable Style/EvalWithLocation
51
- @component.class_eval <<-RUBY, @path, @lineno
52
- def #{@call_method_name}
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
- RUBY
56
- # rubocop:enable Style/EvalWithLocation
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 safe_method_name_call
63
- return safe_method_name unless inline_call?
160
+ def coverage_running?
161
+ defined?(Coverage) && Coverage.running?
162
+ end
64
163
 
65
- "maybe_escape_html(#{safe_method_name}) " \
66
- "{ Kernel.warn(\"WARNING: The #{@component} component rendered HTML-unsafe output. " \
67
- "The output will be automatically escaped, but you may want to investigate.\") } "
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
- @type == :inline_call
76
- end
77
-
78
- def inline?
79
- @type == :inline
174
+ type == :inline_call
80
175
  end
81
176
 
82
177
  def default_format?
83
- @this_format == ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT
178
+ format.nil? || format == DEFAULT_FORMAT
84
179
  end
180
+ alias_method :html?, :default_format?
85
181
 
86
- def format
87
- @this_format
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
- "_#{@call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
189
+ "_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
92
190
  end
93
191
 
94
192
  def normalized_variant_name
95
- @variant.to_s.gsub("-", "__").gsub(".", "___")
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 = ActionView::Template.handler_for_extension(@extension)
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
- type = ActionView::Template::Types[@this_format]
204
+ format = self.format || DEFAULT_FORMAT
205
+ type = ActionView::Template::Types[format]
120
206
 
121
- if handler.method(:call).parameters.length > 1
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