view_component 3.23.2 → 4.0.2

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 +15 -20
  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 +422 -0
  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 +16 -3
  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 +178 -158
  21. data/lib/view_component/collection.rb +19 -25
  22. data/lib/view_component/compiler.rb +52 -79
  23. data/lib/view_component/config.rb +51 -85
  24. data/lib/view_component/configurable.rb +1 -1
  25. data/lib/view_component/deprecation.rb +1 -1
  26. data/lib/view_component/engine.rb +37 -107
  27. data/lib/view_component/errors.rb +16 -34
  28. data/lib/view_component/inline_template.rb +3 -4
  29. data/lib/view_component/instrumentation.rb +4 -10
  30. data/lib/view_component/preview.rb +4 -11
  31. data/lib/view_component/request_details.rb +30 -0
  32. data/lib/view_component/slot.rb +6 -13
  33. data/lib/view_component/slotable.rb +82 -77
  34. data/lib/view_component/system_spec_helpers.rb +11 -0
  35. data/lib/view_component/system_test_helpers.rb +1 -2
  36. data/lib/view_component/template.rb +106 -83
  37. data/lib/view_component/test_helpers.rb +46 -43
  38. data/lib/view_component/translatable.rb +33 -32
  39. data/lib/view_component/version.rb +2 -2
  40. data/lib/view_component.rb +8 -6
  41. metadata +31 -559
  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
@@ -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) && Rails.application
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
@@ -65,7 +84,14 @@ module ViewComponent
65
84
  # @param view_context [ActionView::Base] The original view context.
66
85
  # @return [void]
67
86
  def set_original_view_context(view_context)
68
- self.__vc_original_view_context = view_context
87
+ # noop
88
+ end
89
+
90
+ using RequestDetails
91
+
92
+ # Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...),
93
+ # so we have to define our own empty initializer to overwrite it.
94
+ def initialize
69
95
  end
70
96
 
71
97
  # Entrypoint for rendering components.
@@ -77,31 +103,28 @@ module ViewComponent
77
103
  #
78
104
  # @return [String]
79
105
  def render_in(view_context, &block)
80
- self.class.compile(raise_errors: true)
106
+ self.class.__vc_compile(raise_errors: true)
81
107
 
82
108
  @view_context = view_context
109
+ @old_virtual_path = view_context.instance_variable_get(:@virtual_path)
83
110
  self.__vc_original_view_context ||= view_context
84
111
 
85
- @output_buffer = ActionView::OutputBuffer.new
112
+ @output_buffer = view_context.output_buffer
86
113
 
87
114
  @lookup_context ||= view_context.lookup_context
88
115
 
89
- # required for path helpers in older Rails versions
90
- @view_renderer ||= view_context.view_renderer
91
-
92
116
  # For content_for
93
117
  @view_flow ||= view_context.view_flow
94
118
 
95
119
  # For i18n
96
120
  @virtual_path ||= virtual_path
97
121
 
98
- # For template variants (+phone, +desktop, etc.)
99
- @__vc_variant ||= @lookup_context.variants.first
122
+ # Describes the inferred request constraints (locales, formats, variants)
123
+ @__vc_requested_details ||= @lookup_context.vc_requested_details
100
124
 
101
125
  # For caching, such as #cache_if
102
126
  @current_template = nil unless defined?(@current_template)
103
127
  old_current_template = @current_template
104
- @current_template = self
105
128
 
106
129
  if block && defined?(@__vc_content_set_by_with_content)
107
130
  raise DuplicateContentError.new(self.class.name)
@@ -113,18 +136,35 @@ module ViewComponent
113
136
  before_render
114
137
 
115
138
  if render?
116
- rendered_template = render_template_for(@__vc_variant, __vc_request&.format&.to_sym).to_s
139
+ value = nil
140
+
141
+ @output_buffer.with_buffer do
142
+ @view_context.instance_variable_set(:@virtual_path, virtual_path)
143
+
144
+ rendered_template =
145
+ around_render do
146
+ render_template_for(@__vc_requested_details).to_s
147
+ end
148
+
149
+ # Avoid allocating new string when output_preamble and output_postamble are blank
150
+ value = if output_preamble.blank? && output_postamble.blank?
151
+ rendered_template
152
+ else
153
+ __vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
154
+ end
155
+ end
117
156
 
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
157
+ if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
158
+ identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
159
+ value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
123
160
  end
161
+
162
+ value
124
163
  else
125
164
  ""
126
165
  end
127
166
  ensure
167
+ view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
128
168
  @current_template = old_current_template
129
169
  end
130
170
 
@@ -161,7 +201,7 @@ module ViewComponent
161
201
  target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
162
202
  @__vc_parent_render_level += 1
163
203
 
164
- target_render.bind_call(self, @__vc_variant)
204
+ target_render.bind_call(self, @__vc_requested_details)
165
205
  ensure
166
206
  @__vc_parent_render_level -= 1
167
207
  end
@@ -189,6 +229,14 @@ module ViewComponent
189
229
  # noop
190
230
  end
191
231
 
232
+ # Called around rendering the component. Override to wrap the rendering of a
233
+ # component in custom instrumentation, etc.
234
+ #
235
+ # @return [void]
236
+ def around_render
237
+ yield
238
+ end
239
+
192
240
  # Override to determine whether the ViewComponent should render.
193
241
  #
194
242
  # @return [Boolean]
@@ -196,25 +244,34 @@ module ViewComponent
196
244
  true
197
245
  end
198
246
 
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
247
  # Re-use original view_context if we're not rendering a component.
206
248
  #
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`.
249
+ # As of v4, ViewComponent::Base re-uses the existing view context created
250
+ # by ActionView, meaning the current view context and the original view
251
+ # context are the same object. set_original_view_context is still called
252
+ # to maintain backwards compatibility.
210
253
  #
211
254
  # @private
212
255
  def render(options = {}, args = {}, &block)
213
256
  if options.respond_to?(:set_original_view_context)
214
257
  options.set_original_view_context(self.__vc_original_view_context)
215
- super
258
+
259
+ # We assume options is a component, so there's no need to evaluate the
260
+ # block in the view context as we do below.
261
+ @view_context.render(options, args, &block)
262
+ elsif block
263
+ __vc_original_view_context.render(options, args) do
264
+ # Partials are rendered to their own buffer and do not append to the
265
+ # original @output_buffer we retain a reference to in #render_in. This
266
+ # is a problem since the block passed to us here in the #render method
267
+ # is evaluated within the context of ViewComponent::Base, and thus
268
+ # appends to the original @output_buffer. To avoid this, we evaluate the
269
+ # block in the view context instead, which will append to the output buffer
270
+ # created for the partial.
271
+ __vc_original_view_context.instance_exec(&block)
272
+ end
216
273
  else
217
- __vc_original_view_context.render(options, args, &block)
274
+ __vc_original_view_context.render(options, args)
218
275
  end
219
276
  end
220
277
 
@@ -274,11 +331,12 @@ module ViewComponent
274
331
  []
275
332
  end
276
333
 
277
- # For caching, such as #cache_if
278
- #
279
- # @private
280
- def format
281
- @__vc_variant if defined?(@__vc_variant)
334
+ if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
335
+ # Rails expects us to define `format` on all renderables,
336
+ # but we do not know the `format` of a ViewComponent until runtime.
337
+ def format
338
+ nil
339
+ end
282
340
  end
283
341
 
284
342
  # The current request. Use sparingly as doing so introduces coupling that
@@ -289,10 +347,9 @@ module ViewComponent
289
347
  __vc_request
290
348
  end
291
349
 
292
- # Enables consumers to override request/@request
293
- #
294
350
  # @private
295
351
  def __vc_request
352
+ # The current request (if present, as mailers/jobs/etc do not have a request)
296
353
  @__vc_request ||= controller.request if controller.respond_to?(:request)
297
354
  end
298
355
 
@@ -305,7 +362,9 @@ module ViewComponent
305
362
 
306
363
  @__vc_content =
307
364
  if __vc_render_in_block_provided?
308
- view_context.capture(self, &@__vc_render_in_block)
365
+ with_original_virtual_path do
366
+ view_context.capture(self, &@__vc_render_in_block)
367
+ end
309
368
  elsif __vc_content_set_by_with_content_defined?
310
369
  @__vc_content_set_by_with_content
311
370
  end
@@ -318,6 +377,14 @@ module ViewComponent
318
377
  __vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
319
378
  end
320
379
 
380
+ # @private
381
+ def with_original_virtual_path
382
+ @view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
383
+ yield
384
+ ensure
385
+ @view_context.instance_variable_set(:@virtual_path, virtual_path)
386
+ end
387
+
321
388
  private
322
389
 
323
390
  attr_reader :view_context
@@ -330,12 +397,8 @@ module ViewComponent
330
397
  defined?(@__vc_content_set_by_with_content)
331
398
  end
332
399
 
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?
400
+ def __vc_maybe_escape_html(text)
401
+ return text if @current_template && !@current_template.html?
339
402
  return text if text.blank?
340
403
 
341
404
  if text.html_safe?
@@ -346,54 +409,18 @@ module ViewComponent
346
409
  end
347
410
  end
348
411
 
349
- def safe_output_preamble
350
- maybe_escape_html(output_preamble) do
412
+ def __vc_safe_output_preamble
413
+ __vc_maybe_escape_html(output_preamble) do
351
414
  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
415
  end
353
416
  end
354
417
 
355
- def safe_output_postamble
356
- maybe_escape_html(output_postamble) do
418
+ def __vc_safe_output_postamble
419
+ __vc_maybe_escape_html(output_postamble) do
357
420
  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
421
  end
359
422
  end
360
423
 
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
- #
386
-
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
- #
396
-
397
424
  # Configuration for generators.
398
425
  #
399
426
  # All options under this namespace default to `false` unless otherwise
@@ -451,6 +478,18 @@ module ViewComponent
451
478
  # ```
452
479
  #
453
480
  # Defaults to `false`.
481
+ #
482
+ # #### ßparent_class
483
+ #
484
+ # Parent class for generated components
485
+ #
486
+ # ```ruby
487
+ # config.view_component.generate.parent_class = "MyBaseComponent"
488
+ # ```
489
+ #
490
+ # Defaults to nil. If this is falsy, generators will use
491
+ # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
492
+ #
454
493
 
455
494
  class << self
456
495
  # The file path of the component Ruby file.
@@ -519,50 +558,43 @@ module ViewComponent
519
558
  Collection.new(self, collection, spacer_component, **args)
520
559
  end
521
560
 
561
+ # @private
562
+ def __vc_compile(raise_errors: false, force: false)
563
+ __vc_compiler.compile(raise_errors: raise_errors, force: force)
564
+ end
565
+
522
566
  # @private
523
567
  def inherited(child)
524
568
  # Compile so child will inherit compiled `call_*` template methods that
525
569
  # `compile` defines
526
- compile
570
+ __vc_compile
527
571
 
528
572
  # Give the child its own personal #render_template_for to protect against the case when
529
573
  # eager loading is disabled and the parent component is rendered before the child. In
530
574
  # such a scenario, the parent will override ViewComponent::Base#render_template_for,
531
575
  # 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?
576
+ if !child.instance_methods(false).include?(:render_template_for) && !child.__vc_compiled?
533
577
  child.class_eval <<~RUBY, __FILE__, __LINE__ + 1
534
- def render_template_for(variant = nil, format = nil)
578
+ def render_template_for(requested_details)
535
579
  # Force compilation here so the compiler always redefines render_template_for.
536
580
  # 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)
581
+ self.class.__vc_compile(raise_errors: true, force: true)
582
+ # .__vc_compile replaces this method; call the new one
583
+ render_template_for(requested_details)
540
584
  end
541
585
  RUBY
542
586
  end
543
587
 
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
588
  # Derive the source location of the component Ruby file from the call stack.
551
589
  # We need to ignore `inherited` frames here as they indicate that `inherited`
552
590
  # has been re-defined by the consuming application, likely in ApplicationComponent.
553
591
  # We use `base_label` method here instead of `label` to avoid cases where the method
554
592
  # owner is included in a prefix like `ApplicationComponent.inherited`.
555
593
  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
594
+ child.virtual_path = child.name&.underscore
563
595
 
564
596
  # Set collection parameter to the extended component
565
- child.with_collection_parameter provided_collection_parameter
597
+ child.with_collection_parameter(__vc_provided_collection_parameter)
566
598
 
567
599
  if instance_methods(false).include?(:render_template_for)
568
600
  vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
@@ -575,22 +607,17 @@ module ViewComponent
575
607
  end
576
608
 
577
609
  # @private
578
- def compiled?
579
- compiler.compiled?
580
- end
581
-
582
- # @private
583
- def ensure_compiled
584
- compile unless compiled?
610
+ def __vc_compiled?
611
+ __vc_compiler.compiled?
585
612
  end
586
613
 
587
614
  # @private
588
- def compile(raise_errors: false, force: false)
589
- compiler.compile(raise_errors: raise_errors, force: force)
615
+ def __vc_ensure_compiled
616
+ __vc_compile unless __vc_compiled?
590
617
  end
591
618
 
592
619
  # @private
593
- def compiler
620
+ def __vc_compiler
594
621
  @__vc_compiler ||= Compiler.new(self)
595
622
  end
596
623
 
@@ -602,8 +629,8 @@ module ViewComponent
602
629
  #
603
630
  # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
604
631
  def with_collection_parameter(parameter)
605
- @provided_collection_parameter = parameter
606
- @initialize_parameters = nil
632
+ @__vc_provided_collection_parameter = parameter
633
+ @__vc_initialize_parameters = nil
607
634
  end
608
635
 
609
636
  # Strips trailing whitespace from templates before compiling them.
@@ -632,18 +659,11 @@ module ViewComponent
632
659
  # is accepted, as support for collection
633
660
  # rendering is optional.
634
661
  # @private
635
- def validate_collection_parameter!(validate_default: false)
636
- parameter = validate_default ? collection_parameter : provided_collection_parameter
662
+ def __vc_validate_collection_parameter!(validate_default: false)
663
+ parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter
637
664
 
638
665
  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
666
+ return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present?
647
667
 
648
668
  raise MissingCollectionArgumentError.new(name, parameter)
649
669
  end
@@ -652,58 +672,58 @@ module ViewComponent
652
672
  # invalid parameters that could override the framework's
653
673
  # methods.
654
674
  # @private
655
- def validate_initialization_parameters!
656
- return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
675
+ def __vc_validate_initialization_parameters!
676
+ return unless __vc_initialize_parameter_names.include?(:content)
657
677
 
658
- raise ReservedParameterError.new(name, RESERVED_PARAMETER)
678
+ raise ReservedParameterError.new(name, :content)
659
679
  end
660
680
 
661
681
  # @private
662
- def collection_parameter
663
- provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
682
+ def __vc_collection_parameter
683
+ @__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
664
684
  end
665
685
 
666
686
  # @private
667
- def collection_counter_parameter
668
- :"#{collection_parameter}_counter"
687
+ def __vc_collection_counter_parameter
688
+ @__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
669
689
  end
670
690
 
671
691
  # @private
672
- def counter_argument_present?
673
- initialize_parameter_names.include?(collection_counter_parameter)
692
+ def __vc_counter_argument_present?
693
+ __vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
674
694
  end
675
695
 
676
696
  # @private
677
- def collection_iteration_parameter
678
- :"#{collection_parameter}_iteration"
697
+ def __vc_collection_iteration_parameter
698
+ @__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
679
699
  end
680
700
 
681
701
  # @private
682
- def iteration_argument_present?
683
- initialize_parameter_names.include?(collection_iteration_parameter)
702
+ def __vc_iteration_argument_present?
703
+ __vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
684
704
  end
685
705
 
686
706
  private
687
707
 
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!
708
+ def __vc_splatted_keyword_argument_present?
709
+ __vc_initialize_parameters.flatten.include?(:keyrest)
691
710
  end
692
711
 
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)
712
+ def __vc_initialize_parameter_names
713
+ @__vc_initialize_parameter_names ||=
714
+ if respond_to?(:attribute_names)
715
+ attribute_names.map(&:to_sym)
716
+ else
717
+ __vc_initialize_parameters.map(&:last)
718
+ end
699
719
  end
700
720
 
701
- def initialize_parameters
702
- @initialize_parameters ||= instance_method(:initialize).parameters
721
+ def __vc_initialize_parameters
722
+ @__vc_initialize_parameters ||= instance_method(:initialize).parameters
703
723
  end
704
724
 
705
- def provided_collection_parameter
706
- @provided_collection_parameter ||= nil
725
+ def __vc_provided_collection_parameter
726
+ @__vc_provided_collection_parameter ||= nil
707
727
  end
708
728
  end
709
729
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
3
+ require "action_view/renderer/collection_renderer"
4
4
 
5
5
  module ViewComponent
6
6
  class Collection
@@ -9,25 +9,32 @@ module ViewComponent
9
9
 
10
10
  delegate :size, to: :@collection
11
11
 
12
- attr_accessor :__vc_original_view_context
13
-
14
- def set_original_view_context(view_context)
15
- self.__vc_original_view_context = view_context
16
- end
17
-
18
12
  def render_in(view_context, &block)
19
13
  components.map do |component|
20
- component.set_original_view_context(__vc_original_view_context)
21
14
  component.render_in(view_context, &block)
22
15
  end.join(rendered_spacer(view_context)).html_safe
23
16
  end
24
17
 
18
+ def each(&block)
19
+ components.each(&block)
20
+ end
21
+
22
+ if Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
23
+ # Rails expects us to define `format` on all renderables,
24
+ # but we do not know the `format` of a ViewComponent until runtime.
25
+ def format
26
+ nil
27
+ end
28
+ end
29
+
30
+ private
31
+
25
32
  def components
26
33
  return @components if defined? @components
27
34
 
28
35
  iterator = ActionView::PartialIteration.new(@collection.size)
29
36
 
30
- component.validate_collection_parameter!(validate_default: true)
37
+ component.__vc_validate_collection_parameter!(validate_default: true)
31
38
 
32
39
  @components = @collection.map do |item|
33
40
  component.new(**component_options(item, iterator)).tap do |component|
@@ -36,18 +43,6 @@ module ViewComponent
36
43
  end
37
44
  end
38
45
 
39
- def each(&block)
40
- components.each(&block)
41
- end
42
-
43
- # Rails expects us to define `format` on all renderables,
44
- # but we do not know the `format` of a ViewComponent until runtime.
45
- def format
46
- nil
47
- end
48
-
49
- private
50
-
51
46
  def initialize(component, object, spacer_component, **options)
52
47
  @component = component
53
48
  @collection = collection_variable(object || [])
@@ -64,16 +59,15 @@ module ViewComponent
64
59
  end
65
60
 
66
61
  def component_options(item, iterator)
67
- item_options = {component.collection_parameter => item}
68
- item_options[component.collection_counter_parameter] = iterator.index if component.counter_argument_present?
69
- item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
62
+ item_options = {component.__vc_collection_parameter => item}
63
+ item_options[component.__vc_collection_counter_parameter] = iterator.index if component.__vc_counter_argument_present?
64
+ item_options[component.__vc_collection_iteration_parameter] = iterator.dup if component.__vc_iteration_argument_present?
70
65
 
71
66
  @options.merge(item_options)
72
67
  end
73
68
 
74
69
  def rendered_spacer(view_context)
75
70
  if @spacer_component
76
- @spacer_component.set_original_view_context(__vc_original_view_context)
77
71
  @spacer_component.render_in(view_context)
78
72
  else
79
73
  ""