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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_view"
4
- require "active_support/configurable"
4
+ require "action_view/base"
5
5
  require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
7
  require "view_component/compiler"
@@ -9,15 +9,27 @@ require "view_component/config"
9
9
  require "view_component/errors"
10
10
  require "view_component/inline_template"
11
11
  require "view_component/preview"
12
+ require "view_component/request_details"
12
13
  require "view_component/slotable"
13
- require "view_component/slotable_default"
14
14
  require "view_component/template"
15
15
  require "view_component/translatable"
16
16
  require "view_component/with_content_helper"
17
- require "view_component/use_helpers"
17
+
18
+ module ActionView
19
+ class OutputBuffer
20
+ def with_buffer(buf = nil)
21
+ new_buffer = buf || +""
22
+ old_buffer, @raw_buffer = @raw_buffer, new_buffer
23
+ yield
24
+ new_buffer
25
+ ensure
26
+ @raw_buffer = old_buffer
27
+ end
28
+ end
29
+ end
18
30
 
19
31
  module ViewComponent
20
- class Base < ActionView::Base
32
+ class Base
21
33
  class << self
22
34
  delegate(*ViewComponent::Config.defaults.keys, to: :config)
23
35
 
@@ -34,26 +46,33 @@ module ViewComponent
34
46
  end
35
47
  end
36
48
 
49
+ include ActionView::Helpers
50
+ include Rails.application.routes.url_helpers if defined?(Rails.application.routes)
51
+ include ERB::Escape
52
+ include ActiveSupport::CoreExt::ERBUtil
53
+
37
54
  include ViewComponent::InlineTemplate
38
- include ViewComponent::UseHelpers
39
55
  include ViewComponent::Slotable
40
56
  include ViewComponent::Translatable
41
57
  include ViewComponent::WithContentHelper
42
58
 
43
- RESERVED_PARAMETER = :content
44
- VC_INTERNAL_DEFAULT_FORMAT = :html
45
-
46
59
  # For CSRF authenticity tokens in forms
47
60
  delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
48
61
 
62
+ # HTML construction methods
63
+ delegate :output_buffer, :lookup_context, :view_renderer, :view_flow, to: :helpers
64
+
65
+ # For Turbo::StreamsHelper
66
+ delegate :formats, :formats=, to: :helpers
67
+
49
68
  # For Content Security Policy nonces
50
69
  delegate :content_security_policy_nonce, to: :helpers
51
70
 
52
71
  # Config option that strips trailing whitespace in templates before compiling them.
53
- class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
54
- self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
72
+ class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false
55
73
 
56
74
  attr_accessor :__vc_original_view_context
75
+ attr_reader :current_template
57
76
 
58
77
  # Components render in their own view context. Helpers and other functionality
59
78
  # require a reference to the original Rails view context, an instance of
@@ -63,9 +82,17 @@ module ViewComponent
63
82
  # so helpers, etc work as expected.
64
83
  #
65
84
  # @param view_context [ActionView::Base] The original view context.
85
+ #
66
86
  # @return [void]
67
87
  def set_original_view_context(view_context)
68
- self.__vc_original_view_context = view_context
88
+ # noop
89
+ end
90
+
91
+ using RequestDetails
92
+
93
+ # Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...),
94
+ # so we have to define our own empty initializer to overwrite it.
95
+ def initialize
69
96
  end
70
97
 
71
98
  # Entrypoint for rendering components.
@@ -76,55 +103,82 @@ module ViewComponent
76
103
  # Returns HTML that has been escaped by the respective template handler.
77
104
  #
78
105
  # @return [String]
79
- def render_in(view_context, &block)
80
- self.class.compile(raise_errors: true)
106
+ def render_in(view_context, **_, &block)
107
+ self.class.__vc_compile(raise_errors: true)
81
108
 
82
- @view_context = view_context
83
- self.__vc_original_view_context ||= view_context
109
+ __vc_reset_render_state!
84
110
 
85
- @output_buffer = ActionView::OutputBuffer.new
111
+ @view_context = view_context
112
+ @old_virtual_path = view_context.instance_variable_get(:@virtual_path)
113
+ self.__vc_original_view_context = view_context
86
114
 
87
- @lookup_context ||= view_context.lookup_context
115
+ @output_buffer = view_context.output_buffer
88
116
 
89
- # required for path helpers in older Rails versions
90
- @view_renderer ||= view_context.view_renderer
117
+ @lookup_context = view_context.lookup_context
91
118
 
92
119
  # For content_for
93
- @view_flow ||= view_context.view_flow
120
+ @view_flow = view_context.view_flow
94
121
 
95
122
  # For i18n
96
123
  @virtual_path ||= virtual_path
97
124
 
98
- # For template variants (+phone, +desktop, etc.)
99
- @__vc_variant ||= @lookup_context.variants.first
125
+ # Describes the inferred request constraints (locales, formats, variants)
126
+ @__vc_requested_details = @lookup_context.vc_requested_details
100
127
 
101
128
  # For caching, such as #cache_if
102
129
  @current_template = nil unless defined?(@current_template)
103
130
  old_current_template = @current_template
104
- @current_template = self
105
131
 
106
132
  if block && defined?(@__vc_content_set_by_with_content)
107
133
  raise DuplicateContentError.new(self.class.name)
108
134
  end
109
135
 
110
136
  @__vc_content_evaluated = false
137
+ remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
111
138
  @__vc_render_in_block = block
139
+ @view_context.instance_variable_set(:@virtual_path, virtual_path)
112
140
 
113
141
  before_render
114
142
 
115
143
  if render?
116
- rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s
144
+ value = nil
145
+
146
+ @output_buffer.with_buffer do
147
+ inner_rendered_template = nil
148
+ around_rendered_template = around_render do
149
+ inner_rendered_template = render_template_for(@__vc_requested_details).to_s
150
+ end
151
+
152
+ # If `around_render` returned the same object the block yielded, the inner
153
+ # template's escaping is authoritative and we can trust the result. If the
154
+ # user replaced/wrapped the value, re-check HTML safety to prevent
155
+ # bypassing the escaping applied to normal `#call` return values
156
+ # (GHSA-97jw-64cj-jc58).
157
+ rendered_template = if around_rendered_template.equal?(inner_rendered_template)
158
+ around_rendered_template
159
+ else
160
+ __vc_safe_around_render_output(around_rendered_template)
161
+ end
162
+
163
+ # Avoid allocating new string when output_preamble and output_postamble are blank
164
+ value = if output_preamble.blank? && output_postamble.blank?
165
+ rendered_template
166
+ else
167
+ __vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
168
+ end
169
+ end
117
170
 
118
- # Avoid allocating new string when output_preamble and output_postamble are blank
119
- if output_preamble.blank? && output_postamble.blank?
120
- rendered_template
121
- else
122
- safe_output_preamble + rendered_template + safe_output_postamble
171
+ if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
172
+ identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
173
+ value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
123
174
  end
175
+
176
+ value
124
177
  else
125
- ""
178
+ "".html_safe
126
179
  end
127
180
  ensure
181
+ view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
128
182
  @current_template = old_current_template
129
183
  end
130
184
 
@@ -161,7 +215,7 @@ module ViewComponent
161
215
  target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
162
216
  @__vc_parent_render_level += 1
163
217
 
164
- target_render.bind_call(self, @__vc_variant)
218
+ target_render.bind_call(self, @__vc_requested_details)
165
219
  ensure
166
220
  @__vc_parent_render_level -= 1
167
221
  end
@@ -189,6 +243,14 @@ module ViewComponent
189
243
  # noop
190
244
  end
191
245
 
246
+ # Called around rendering the component. Override to wrap the rendering of a
247
+ # component in custom instrumentation, etc.
248
+ #
249
+ # @return [void]
250
+ def around_render
251
+ yield
252
+ end
253
+
192
254
  # Override to determine whether the ViewComponent should render.
193
255
  #
194
256
  # @return [Boolean]
@@ -196,28 +258,56 @@ module ViewComponent
196
258
  true
197
259
  end
198
260
 
199
- # Override the ActionView::Base initializer so that components
200
- # do not need to define their own initializers.
201
- # @private
202
- def initialize(*)
203
- end
204
-
205
261
  # Re-use original view_context if we're not rendering a component.
206
262
  #
207
- # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
208
- # of the component. This is due to the partials compiled template method existing in the parent `view_context`,
209
- # and not the component's `view_context`.
263
+ # As of v4, ViewComponent::Base re-uses the existing view context created
264
+ # by ActionView, meaning the current view context and the original view
265
+ # context are the same object. set_original_view_context is still called
266
+ # to maintain backwards compatibility.
210
267
  #
211
268
  # @private
212
269
  def render(options = {}, args = {}, &block)
213
270
  if options.respond_to?(:set_original_view_context)
214
- options.set_original_view_context(self.__vc_original_view_context)
215
- super
271
+ options.set_original_view_context(__vc_original_view_context)
272
+
273
+ # We assume options is a component, so there's no need to evaluate the
274
+ # block in the view context as we do below.
275
+ @view_context.render(options, args, &block)
276
+ elsif block
277
+ __vc_original_view_context.render(options, args) do
278
+ # capture the block output in the view context of the component
279
+ output = capture(&block)
280
+
281
+ # Partials are rendered to their own buffer and do not append to the
282
+ # original @output_buffer we retain a reference to in #render_in. This
283
+ # is a problem since the block passed to us here in the #render method
284
+ # is evaluated within the context of ViewComponent::Base, and thus
285
+ # appends to the original @output_buffer. To avoid this, we evaluate the
286
+ # block in the view context instead, which will append to the output buffer
287
+ # created for the partial.
288
+ __vc_original_view_context.capture { output }
289
+ end
216
290
  else
217
- __vc_original_view_context.render(options, args, &block)
291
+ __vc_original_view_context.render(options, args)
218
292
  end
219
293
  end
220
294
 
295
+ # Sync @output_buffer with the view context's current output buffer before
296
+ # capturing. Form helpers create builders with @template_object pointing to
297
+ # the component. When those builders later call capture inside a partial
298
+ # (whose _run allocated a fresh OutputBuffer on the view context), the
299
+ # component's stale @output_buffer would capture from the wrong buffer.
300
+ # Temporarily switching to the view context's buffer keeps both in sync.
301
+ #
302
+ # @private
303
+ def capture(...)
304
+ old_output_buffer = @output_buffer
305
+ @output_buffer = view_context.output_buffer if view_context
306
+ super
307
+ ensure
308
+ @output_buffer = old_output_buffer
309
+ end
310
+
221
311
  # The current controller. Use sparingly as doing so introduces coupling
222
312
  # that inhibits encapsulation & reuse, often making testing difficult.
223
313
  #
@@ -245,7 +335,7 @@ module ViewComponent
245
335
  @__vc_helpers ||= __vc_original_view_context || controller.view_context
246
336
  end
247
337
 
248
- if ::Rails.env.development? || ::Rails.env.test?
338
+ if defined?(Rails.env) && (::Rails.env.development? || ::Rails.env.test?)
249
339
  # @private
250
340
  def method_missing(method_name, *args) # rubocop:disable Style/MissingRespondToMissing
251
341
  super
@@ -274,11 +364,12 @@ module ViewComponent
274
364
  []
275
365
  end
276
366
 
277
- # For caching, such as #cache_if
278
- #
279
- # @private
280
- def format
281
- @__vc_variant if defined?(@__vc_variant)
367
+ if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
368
+ # Rails expects us to define `format` on all renderables,
369
+ # but we do not know the `format` of a ViewComponent until runtime.
370
+ def format
371
+ nil
372
+ end
282
373
  end
283
374
 
284
375
  # The current request. Use sparingly as doing so introduces coupling that
@@ -289,10 +380,9 @@ module ViewComponent
289
380
  __vc_request
290
381
  end
291
382
 
292
- # Enables consumers to override request/@request
293
- #
294
383
  # @private
295
384
  def __vc_request
385
+ # The current request (if present, as mailers/jobs/etc do not have a request)
296
386
  @__vc_request ||= controller.request if controller.respond_to?(:request)
297
387
  end
298
388
 
@@ -305,7 +395,9 @@ module ViewComponent
305
395
 
306
396
  @__vc_content =
307
397
  if __vc_render_in_block_provided?
308
- view_context.capture(self, &@__vc_render_in_block)
398
+ with_captured_virtual_path(@old_virtual_path) do
399
+ view_context.capture(self, &@__vc_render_in_block)
400
+ end
309
401
  elsif __vc_content_set_by_with_content_defined?
310
402
  @__vc_content_set_by_with_content
311
403
  end
@@ -318,6 +410,17 @@ module ViewComponent
318
410
  __vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
319
411
  end
320
412
 
413
+ # @private
414
+ # Temporarily sets the virtual path to the captured value, then restores it.
415
+ # This ensures translations and other path-dependent code execute with the correct scope.
416
+ def with_captured_virtual_path(captured_path)
417
+ old_virtual_path = @view_context.instance_variable_get(:@virtual_path)
418
+ @view_context.instance_variable_set(:@virtual_path, captured_path)
419
+ yield
420
+ ensure
421
+ @view_context.instance_variable_set(:@virtual_path, old_virtual_path)
422
+ end
423
+
321
424
  private
322
425
 
323
426
  attr_reader :view_context
@@ -330,12 +433,8 @@ module ViewComponent
330
433
  defined?(@__vc_content_set_by_with_content)
331
434
  end
332
435
 
333
- def content_evaluated?
334
- defined?(@__vc_content_evaluated) && @__vc_content_evaluated
335
- end
336
-
337
- def maybe_escape_html(text)
338
- return text if __vc_request && !__vc_request.format.html?
436
+ def __vc_maybe_escape_html(text)
437
+ return text if @current_template && !@current_template.html?
339
438
  return text if text.blank?
340
439
 
341
440
  if text.html_safe?
@@ -346,53 +445,39 @@ module ViewComponent
346
445
  end
347
446
  end
348
447
 
349
- def safe_output_preamble
350
- maybe_escape_html(output_preamble) do
448
+ def __vc_safe_output_preamble
449
+ __vc_maybe_escape_html(output_preamble) do
351
450
  Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe preamble. The preamble will be automatically escaped, but you may want to investigate.")
352
451
  end
353
452
  end
354
453
 
355
- def safe_output_postamble
356
- maybe_escape_html(output_postamble) do
454
+ def __vc_safe_output_postamble
455
+ __vc_maybe_escape_html(output_postamble) do
357
456
  Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.")
358
457
  end
359
458
  end
360
459
 
361
- # Set the controller used for testing components:
362
- #
363
- # ```ruby
364
- # config.view_component.test_controller = "MyTestController"
365
- # ```
366
- #
367
- # Defaults to `nil`. If this is falsy, `"ApplicationController"` is used. Can also be
368
- # configured on a per-test basis using `with_controller_class`.
369
- #
370
-
371
- # Set if render monkey patches should be included or not in Rails <6.1:
372
- #
373
- # ```ruby
374
- # config.view_component.render_monkey_patch_enabled = false
375
- # ```
376
- #
377
-
378
- # Path for component files
379
- #
380
- # ```ruby
381
- # config.view_component.view_component_path = "app/my_components"
382
- # ```
383
- #
384
- # Defaults to `nil`. If this is falsy, `app/components` is used.
385
- #
460
+ def __vc_safe_around_render_output(output)
461
+ __vc_maybe_escape_html(output) do
462
+ Kernel.warn("WARNING: The #{self.class} component's around_render returned an HTML-unsafe string. The output will be automatically escaped, but you may want to investigate.")
463
+ end
464
+ end
386
465
 
387
- # Parent class for generated components
388
- #
389
- # ```ruby
390
- # config.view_component.component_parent_class = "MyBaseComponent"
391
- # ```
392
- #
393
- # Defaults to nil. If this is falsy, generators will use
394
- # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
395
- #
466
+ # Resets every render-scoped instance variable derived from the calling view
467
+ # context so a reused instance cannot leak controller/helper/request/format
468
+ # state from a previous render. Slot state (`@__vc_set_slots`,
469
+ # `@__vc_content_set_by_with_content`) is intentionally preserved because it
470
+ # is populated by callers _before_ `render_in` runs (e.g. via `with_*`
471
+ # slot setters or `with_content`).
472
+ def __vc_reset_render_state!
473
+ %i[
474
+ @__vc_controller
475
+ @__vc_helpers
476
+ @__vc_request
477
+ ].each do |ivar|
478
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
479
+ end
480
+ end
396
481
 
397
482
  # Configuration for generators.
398
483
  #
@@ -451,6 +536,18 @@ module ViewComponent
451
536
  # ```
452
537
  #
453
538
  # Defaults to `false`.
539
+ #
540
+ # #### ßparent_class
541
+ #
542
+ # Parent class for generated components
543
+ #
544
+ # ```ruby
545
+ # config.view_component.generate.parent_class = "MyBaseComponent"
546
+ # ```
547
+ #
548
+ # Defaults to nil. If this is falsy, generators will use
549
+ # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
550
+ #
454
551
 
455
552
  class << self
456
553
  # The file path of the component Ruby file.
@@ -473,6 +570,7 @@ module ViewComponent
473
570
  # @param extensions [Array<String>] Extensions of which to return matching sidecar files.
474
571
  def sidecar_files(extensions)
475
572
  return [] unless identifier
573
+ return [] unless name
476
574
 
477
575
  extensions = extensions.join(",")
478
576
 
@@ -519,52 +617,45 @@ module ViewComponent
519
617
  Collection.new(self, collection, spacer_component, **args)
520
618
  end
521
619
 
620
+ # @private
621
+ def __vc_compile(raise_errors: false, force: false)
622
+ __vc_compiler.compile(raise_errors: raise_errors, force: force)
623
+ end
624
+
522
625
  # @private
523
626
  def inherited(child)
524
627
  # Compile so child will inherit compiled `call_*` template methods that
525
628
  # `compile` defines
526
- compile
629
+ __vc_compile
527
630
 
528
631
  # Give the child its own personal #render_template_for to protect against the case when
529
632
  # eager loading is disabled and the parent component is rendered before the child. In
530
633
  # such a scenario, the parent will override ViewComponent::Base#render_template_for,
531
634
  # meaning it will not be called for any children and thus not compile their templates.
532
- if !child.instance_methods(false).include?(:render_template_for) && !child.compiled?
635
+ if !child.method_defined?(:render_template_for, false) && !child.__vc_compiled?
533
636
  child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
534
- def render_template_for(variant = nil, format = nil)
637
+ def render_template_for(requested_details)
535
638
  # Force compilation here so the compiler always redefines render_template_for.
536
639
  # This is mostly a safeguard to prevent infinite recursion.
537
- self.class.compile(raise_errors: true, force: true)
538
- # .compile replaces this method; call the new one
539
- render_template_for(variant, format)
640
+ self.class.__vc_compile(raise_errors: true, force: true)
641
+ # .__vc_compile replaces this method; call the new one
642
+ render_template_for(requested_details)
540
643
  end
541
644
  RUBY
542
645
  end
543
646
 
544
- # If Rails application is loaded, add application url_helpers to the component context
545
- # we need to check this to use this gem as a dependency
546
- if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers)
547
- child.include Rails.application.routes.url_helpers
548
- end
549
-
550
647
  # Derive the source location of the component Ruby file from the call stack.
551
648
  # We need to ignore `inherited` frames here as they indicate that `inherited`
552
649
  # has been re-defined by the consuming application, likely in ApplicationComponent.
553
650
  # We use `base_label` method here instead of `label` to avoid cases where the method
554
651
  # owner is included in a prefix like `ApplicationComponent.inherited`.
555
652
  child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
556
-
557
- # If Rails application is loaded, removes the first part of the path and the extension.
558
- if defined?(Rails) && Rails.application
559
- child.virtual_path = child.identifier.gsub(
560
- /(.*#{Regexp.quote(ViewComponent::Base.config.view_component_path)})|(\.rb)/, ""
561
- )
562
- end
653
+ child.virtual_path = child.name&.underscore
563
654
 
564
655
  # Set collection parameter to the extended component
565
- child.with_collection_parameter provided_collection_parameter
656
+ child.with_collection_parameter(__vc_provided_collection_parameter)
566
657
 
567
- if instance_methods(false).include?(:render_template_for)
658
+ if method_defined?(:render_template_for, false)
568
659
  vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
569
660
 
570
661
  vc_ancestor_calls.unshift(instance_method(:render_template_for))
@@ -575,22 +666,27 @@ module ViewComponent
575
666
  end
576
667
 
577
668
  # @private
578
- def compiled?
579
- compiler.compiled?
669
+ def __vc_compiled?
670
+ __vc_compiler.compiled?
580
671
  end
581
672
 
582
- # @private
583
- def ensure_compiled
584
- compile unless compiled?
673
+ # Hook called by the compiler after a component is compiled.
674
+ #
675
+ # Extensions can override this class method to run logic after
676
+ # compilation (e.g., generate helpers, register metadata, etc.).
677
+ #
678
+ # By default, this is a no-op.
679
+ def after_compile
680
+ # no-op by default
585
681
  end
586
682
 
587
683
  # @private
588
- def compile(raise_errors: false, force: false)
589
- compiler.compile(raise_errors: raise_errors, force: force)
684
+ def __vc_ensure_compiled
685
+ __vc_compile unless __vc_compiled?
590
686
  end
591
687
 
592
688
  # @private
593
- def compiler
689
+ def __vc_compiler
594
690
  @__vc_compiler ||= Compiler.new(self)
595
691
  end
596
692
 
@@ -602,8 +698,8 @@ module ViewComponent
602
698
  #
603
699
  # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
604
700
  def with_collection_parameter(parameter)
605
- @provided_collection_parameter = parameter
606
- @initialize_parameters = nil
701
+ @__vc_provided_collection_parameter = parameter
702
+ @__vc_initialize_parameters = nil
607
703
  end
608
704
 
609
705
  # Strips trailing whitespace from templates before compiling them.
@@ -632,18 +728,11 @@ module ViewComponent
632
728
  # is accepted, as support for collection
633
729
  # rendering is optional.
634
730
  # @private
635
- def validate_collection_parameter!(validate_default: false)
636
- parameter = validate_default ? collection_parameter : provided_collection_parameter
731
+ def __vc_validate_collection_parameter!(validate_default: false)
732
+ parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter
637
733
 
638
734
  return unless parameter
639
- return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present?
640
-
641
- # If Ruby can't parse the component class, then the initialize
642
- # parameters will be empty and ViewComponent will not be able to render
643
- # the component.
644
- if initialize_parameters.empty?
645
- raise EmptyOrInvalidInitializerError.new(name, parameter)
646
- end
735
+ return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present?
647
736
 
648
737
  raise MissingCollectionArgumentError.new(name, parameter)
649
738
  end
@@ -652,58 +741,58 @@ module ViewComponent
652
741
  # invalid parameters that could override the framework's
653
742
  # methods.
654
743
  # @private
655
- def validate_initialization_parameters!
656
- return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
744
+ def __vc_validate_initialization_parameters!
745
+ return unless __vc_initialize_parameter_names.include?(:content)
657
746
 
658
- raise ReservedParameterError.new(name, RESERVED_PARAMETER)
747
+ raise ReservedParameterError.new(name, :content)
659
748
  end
660
749
 
661
750
  # @private
662
- def collection_parameter
663
- provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
751
+ def __vc_collection_parameter
752
+ @__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
664
753
  end
665
754
 
666
755
  # @private
667
- def collection_counter_parameter
668
- :"#{collection_parameter}_counter"
756
+ def __vc_collection_counter_parameter
757
+ @__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
669
758
  end
670
759
 
671
760
  # @private
672
- def counter_argument_present?
673
- initialize_parameter_names.include?(collection_counter_parameter)
761
+ def __vc_counter_argument_present?
762
+ __vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
674
763
  end
675
764
 
676
765
  # @private
677
- def collection_iteration_parameter
678
- :"#{collection_parameter}_iteration"
766
+ def __vc_collection_iteration_parameter
767
+ @__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
679
768
  end
680
769
 
681
770
  # @private
682
- def iteration_argument_present?
683
- initialize_parameter_names.include?(collection_iteration_parameter)
771
+ def __vc_iteration_argument_present?
772
+ __vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
684
773
  end
685
774
 
686
775
  private
687
776
 
688
- def splatted_keyword_argument_present?
689
- initialize_parameters.flatten.include?(:keyrest) &&
690
- !initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!
777
+ def __vc_splatted_keyword_argument_present?
778
+ __vc_initialize_parameters.flatten.include?(:keyrest)
691
779
  end
692
780
 
693
- def initialize_parameter_names
694
- return attribute_names.map(&:to_sym) if respond_to?(:attribute_names)
695
-
696
- return attribute_types.keys.map(&:to_sym) if Rails::VERSION::MAJOR <= 5 && respond_to?(:attribute_types)
697
-
698
- initialize_parameters.map(&:last)
781
+ def __vc_initialize_parameter_names
782
+ @__vc_initialize_parameter_names ||=
783
+ if respond_to?(:attribute_names)
784
+ attribute_names.map(&:to_sym)
785
+ else
786
+ __vc_initialize_parameters.map(&:last)
787
+ end
699
788
  end
700
789
 
701
- def initialize_parameters
702
- @initialize_parameters ||= instance_method(:initialize).parameters
790
+ def __vc_initialize_parameters
791
+ @__vc_initialize_parameters ||= instance_method(:initialize).parameters
703
792
  end
704
793
 
705
- def provided_collection_parameter
706
- @provided_collection_parameter ||= nil
794
+ def __vc_provided_collection_parameter
795
+ @__vc_provided_collection_parameter ||= nil
707
796
  end
708
797
  end
709
798