view_component 2.34.0 → 2.38.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.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/vendor/prism.css +196 -0
  4. data/app/assets/vendor/prism.min.js +12 -0
  5. data/app/controllers/view_components_controller.rb +1 -1
  6. data/app/helpers/preview_helper.rb +19 -0
  7. data/app/views/view_components/_preview_source.html.erb +17 -0
  8. data/app/views/view_components/preview.html.erb +6 -2
  9. data/{CHANGELOG.md → docs/CHANGELOG.md} +145 -2
  10. data/lib/rails/generators/abstract_generator.rb +46 -0
  11. data/lib/rails/generators/component/component_generator.rb +9 -5
  12. data/lib/rails/generators/component/templates/component.rb.tt +1 -1
  13. data/lib/rails/generators/erb/component_generator.rb +12 -12
  14. data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -1
  15. data/lib/rails/generators/haml/component_generator.rb +6 -16
  16. data/lib/rails/generators/slim/component_generator.rb +6 -16
  17. data/lib/rails/generators/stimulus/component_generator.rb +26 -0
  18. data/lib/rails/generators/stimulus/templates/component_controller.js.tt +7 -0
  19. data/lib/view_component.rb +1 -0
  20. data/lib/view_component/base.rb +144 -84
  21. data/lib/view_component/collection.rb +6 -2
  22. data/lib/view_component/compile_cache.rb +1 -0
  23. data/lib/view_component/compiler.rb +87 -53
  24. data/lib/view_component/content_areas.rb +57 -0
  25. data/lib/view_component/engine.rb +19 -2
  26. data/lib/view_component/instrumentation.rb +5 -1
  27. data/lib/view_component/preview.rb +19 -8
  28. data/lib/view_component/previewable.rb +16 -18
  29. data/lib/view_component/slot_v2.rb +34 -27
  30. data/lib/view_component/slotable.rb +2 -1
  31. data/lib/view_component/slotable_v2.rb +58 -24
  32. data/lib/view_component/test_helpers.rb +7 -1
  33. data/lib/view_component/translatable.rb +6 -5
  34. data/lib/view_component/version.rb +1 -1
  35. data/lib/view_component/with_content_helper.rb +5 -2
  36. data/lib/yard/mattr_accessor_handler.rb +19 -0
  37. metadata +76 -39
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module AbstractGenerator
5
+ def copy_view_file
6
+ unless options["inline"]
7
+ template "component.html.#{engine_name}", destination
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def destination
14
+ File.join(destination_directory, "#{destination_file_name}.html.#{engine_name}")
15
+ end
16
+
17
+ def destination_directory
18
+ if options["sidecar"]
19
+ File.join(component_path, class_path, destination_file_name)
20
+ else
21
+ File.join(component_path, class_path)
22
+ end
23
+ end
24
+
25
+ def destination_file_name
26
+ "#{file_name}_component"
27
+ end
28
+
29
+ def file_name
30
+ @_file_name ||= super.sub(/_component\z/i, "")
31
+ end
32
+
33
+ def component_path
34
+ ViewComponent::Base.view_component_path
35
+ end
36
+
37
+ def stimulus_controller
38
+ if options["stimulus"]
39
+ File.join(destination_directory, destination_file_name).
40
+ sub("#{component_path}/", "").
41
+ gsub("_", "-").
42
+ gsub("/", "--")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,32 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rails/generators/abstract_generator"
4
+
3
5
  module Rails
4
6
  module Generators
5
7
  class ComponentGenerator < Rails::Generators::NamedBase
8
+ include ViewComponent::AbstractGenerator
9
+
6
10
  source_root File.expand_path("templates", __dir__)
7
11
 
8
12
  argument :attributes, type: :array, default: [], banner: "attribute"
9
13
  check_class_collision suffix: "Component"
10
14
  class_option :inline, type: :boolean, default: false
15
+ class_option :stimulus, type: :boolean, default: ViewComponent::Base.generate_stimulus_controller
16
+ class_option :sidecar, type: :boolean, default: false
11
17
 
12
18
  def create_component_file
13
- template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
19
+ template "component.rb", File.join(component_path, class_path, "#{file_name}_component.rb")
14
20
  end
15
21
 
16
22
  hook_for :test_framework
17
23
 
18
24
  hook_for :preview, type: :boolean
19
25
 
26
+ hook_for :stimulus, type: :boolean
27
+
20
28
  hook_for :template_engine do |instance, template_engine|
21
29
  instance.invoke template_engine, [instance.name]
22
30
  end
23
31
 
24
32
  private
25
33
 
26
- def file_name
27
- @_file_name ||= super.sub(/_component\z/i, "")
28
- end
29
-
30
34
  def parent_class
31
35
  defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
32
36
  end
@@ -8,7 +8,7 @@ class <%= class_name %>Component < <%= parent_class %>
8
8
  <%- end -%>
9
9
  <%- if initialize_call_method_for_inline? -%>
10
10
  def call
11
- content_tag :h1, "Hello world!"
11
+ content_tag :h1, "Hello world!"<%= ", data: { controller: \"#{stimulus_controller}\" }" if options["stimulus"] %>
12
12
  end
13
13
  <%- end -%>
14
14
 
@@ -1,33 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators/erb"
4
+ require "rails/generators/abstract_generator"
4
5
 
5
6
  module Erb
6
7
  module Generators
7
8
  class ComponentGenerator < Base
9
+ include ViewComponent::AbstractGenerator
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
12
  class_option :sidecar, type: :boolean, default: false
10
13
  class_option :inline, type: :boolean, default: false
14
+ class_option :stimulus, type: :boolean, default: false
15
+
16
+ def engine_name
17
+ "erb"
18
+ end
11
19
 
12
20
  def copy_view_file
13
- unless options["inline"]
14
- template "component.html.erb", destination
15
- end
21
+ super
16
22
  end
17
23
 
18
24
  private
19
25
 
20
- def destination
21
- if options["sidecar"]
22
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.erb")
23
- else
24
- File.join("app/components", class_path, "#{file_name}_component.html.erb")
26
+ def data_attributes
27
+ if options["stimulus"]
28
+ " data-controller=\"#{stimulus_controller}\""
25
29
  end
26
30
  end
27
-
28
- def file_name
29
- @_file_name ||= super.sub(/_component\z/i, "")
30
- end
31
31
  end
32
32
  end
33
33
  end
@@ -1 +1 @@
1
- <div>Add <%= class_name %> template here</div>
1
+ <div<%= data_attributes %>>Add <%= class_name %> template here</div>
@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Haml
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.haml", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.haml")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.haml")
24
- end
13
+ def engine_name
14
+ "haml"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -5,27 +5,17 @@ require "rails/generators/erb/component_generator"
5
5
  module Slim
6
6
  module Generators
7
7
  class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ include ViewComponent::AbstractGenerator
9
+
8
10
  source_root File.expand_path("templates", __dir__)
9
11
  class_option :sidecar, type: :boolean, default: false
10
12
 
11
- def copy_view_file
12
- if !options["inline"]
13
- template "component.html.slim", destination
14
- end
15
- end
16
-
17
- private
18
-
19
- def destination
20
- if options["sidecar"]
21
- File.join("app/components", class_path, "#{file_name}_component", "#{file_name}_component.html.slim")
22
- else
23
- File.join("app/components", class_path, "#{file_name}_component.html.slim")
24
- end
13
+ def engine_name
14
+ "slim"
25
15
  end
26
16
 
27
- def file_name
28
- @_file_name ||= super.sub(/_component\z/i, "")
17
+ def copy_view_file
18
+ super
29
19
  end
30
20
  end
31
21
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stimulus
4
+ module Generators
5
+ class ComponentGenerator < ::Rails::Generators::NamedBase
6
+ include ViewComponent::AbstractGenerator
7
+
8
+ source_root File.expand_path("templates", __dir__)
9
+ class_option :sidecar, type: :boolean, default: false
10
+
11
+ def create_stimulus_controller
12
+ template "component_controller.js", destination
13
+ end
14
+
15
+ private
16
+
17
+ def destination
18
+ if options["sidecar"]
19
+ File.join(component_path, class_path, "#{file_name}_component", "#{file_name}_component_controller.js")
20
+ else
21
+ File.join(component_path, class_path, "#{file_name}_component_controller.js")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ import { Controller } from "stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("Hello, Stimulus!", this.element);
6
+ }
7
+ }
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "action_view"
3
4
  require "active_support/dependencies/autoload"
4
5
 
@@ -4,6 +4,7 @@ require "action_view"
4
4
  require "active_support/configurable"
5
5
  require "view_component/collection"
6
6
  require "view_component/compile_cache"
7
+ require "view_component/content_areas"
7
8
  require "view_component/previewable"
8
9
  require "view_component/slotable"
9
10
  require "view_component/slotable_v2"
@@ -12,6 +13,7 @@ require "view_component/with_content_helper"
12
13
  module ViewComponent
13
14
  class Base < ActionView::Base
14
15
  include ActiveSupport::Configurable
16
+ include ViewComponent::ContentAreas
15
17
  include ViewComponent::Previewable
16
18
  include ViewComponent::SlotableV2
17
19
  include ViewComponent::WithContentHelper
@@ -76,22 +78,28 @@ module ViewComponent
76
78
  @virtual_path ||= virtual_path
77
79
 
78
80
  # For template variants (+phone, +desktop, etc.)
79
- @variant ||= @lookup_context.variants.first
81
+ @__vc_variant ||= @lookup_context.variants.first
80
82
 
81
83
  # For caching, such as #cache_if
82
84
  @current_template = nil unless defined?(@current_template)
83
85
  old_current_template = @current_template
84
86
  @current_template = self
85
87
 
86
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if block && defined?(@_content_set_by_with_content)
88
+ if block && defined?(@__vc_content_set_by_with_content)
89
+ raise ArgumentError.new(
90
+ "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
91
+ "which means that ViewComponent doesn't know which content to use.\n\n" \
92
+ "To fix this issue, use either `with_content` or a block."
93
+ )
94
+ end
87
95
 
88
- @_content_evaluated = false
89
- @_render_in_block = block
96
+ @__vc_content_evaluated = false
97
+ @__vc_render_in_block = block
90
98
 
91
99
  before_render
92
100
 
93
101
  if render?
94
- render_template_for(@variant).to_s + _output_postamble
102
+ render_template_for(@__vc_variant).to_s + _output_postamble
95
103
  else
96
104
  ""
97
105
  end
@@ -106,7 +114,8 @@ module ViewComponent
106
114
  ""
107
115
  end
108
116
 
109
- # Called before rendering the component. Override to perform operations that depend on having access to the view context, such as helpers.
117
+ # Called before rendering the component. Override to perform operations that
118
+ # depend on having access to the view context, such as helpers.
110
119
  #
111
120
  # @return [void]
112
121
  def before_render
@@ -115,7 +124,7 @@ module ViewComponent
115
124
 
116
125
  # Called after rendering the component.
117
126
  #
118
- # @deprecated Use `before_render` instead. Will be removed in v3.0.0.
127
+ # @deprecated Use `#before_render` instead. Will be removed in v3.0.0.
119
128
  # @return [void]
120
129
  def before_render_check
121
130
  # noop
@@ -146,20 +155,42 @@ module ViewComponent
146
155
  end
147
156
  end
148
157
 
149
- # The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
158
+ # The current controller. Use sparingly as doing so introduces coupling
159
+ # that inhibits encapsulation & reuse, often making testing difficult.
150
160
  #
151
161
  # @return [ActionController::Base]
152
162
  def controller
153
- raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
154
- @controller ||= view_context.controller
163
+ if view_context.nil?
164
+ raise(
165
+ ViewContextCalledBeforeRenderError,
166
+ "`#controller` cannot be used during initialization, as it depends " \
167
+ "on the view context that only exists once a ViewComponent is passed to " \
168
+ "the Rails render pipeline.\n\n" \
169
+ "It's sometimes possible to fix this issue by moving code dependent on " \
170
+ "`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
171
+ )
172
+ end
173
+
174
+ @__vc_controller ||= view_context.controller
155
175
  end
156
176
 
157
- # A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
177
+ # A proxy through which to access helpers. Use sparingly as doing so introduces
178
+ # coupling that inhibits encapsulation & reuse, often making testing difficult.
158
179
  #
159
180
  # @return [ActionView::Base]
160
181
  def helpers
161
- raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
162
- @helpers ||= controller.view_context
182
+ if view_context.nil?
183
+ raise(
184
+ ViewContextCalledBeforeRenderError,
185
+ "`#helpers` cannot be used during initialization, as it depends " \
186
+ "on the view context that only exists once a ViewComponent is passed to " \
187
+ "the Rails render pipeline.\n\n" \
188
+ "It's sometimes possible to fix this issue by moving code dependent on " \
189
+ "`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
190
+ )
191
+ end
192
+
193
+ @__vc_helpers ||= controller.view_context
163
194
  end
164
195
 
165
196
  # Exposes .virtual_path as an instance method
@@ -180,38 +211,23 @@ module ViewComponent
180
211
  # @private
181
212
  def format
182
213
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
183
- if defined?(@variant)
184
- @variant
214
+ if defined?(@__vc_variant)
215
+ @__vc_variant
185
216
  end
186
217
  end
187
218
 
188
- # Assign the provided content to the content area accessor
189
- #
190
- # @private
191
- def with(area, content = nil, &block)
192
- unless content_areas.include?(area)
193
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
194
- end
195
-
196
- if block_given?
197
- content = view_context.capture(&block)
198
- end
199
-
200
- instance_variable_set("@#{area}".to_sym, content)
201
- nil
202
- end
203
-
204
219
  # Use the provided variant instead of the one determined by the current request.
205
220
  #
206
221
  # @param variant [Symbol] The variant to be used by the component.
207
222
  # @return [self]
208
223
  def with_variant(variant)
209
- @variant = variant
224
+ @__vc_variant = variant
210
225
 
211
226
  self
212
227
  end
213
228
 
214
- # The current request. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.
229
+ # The current request. Use sparingly as doing so introduces coupling that
230
+ # inhibits encapsulation & reuse, often making testing difficult.
215
231
  #
216
232
  # @return [ActionDispatch::Request]
217
233
  def request
@@ -223,31 +239,62 @@ module ViewComponent
223
239
  attr_reader :view_context
224
240
 
225
241
  def content
226
- return @_content if defined?(@_content)
227
- @_content_evaluated = true
228
-
229
- @_content = if @view_context && @_render_in_block
230
- view_context.capture(self, &@_render_in_block)
231
- elsif defined?(@_content_set_by_with_content)
232
- @_content_set_by_with_content
233
- end
242
+ @__vc_content_evaluated = true
243
+ return @__vc_content if defined?(@__vc_content)
244
+
245
+ @__vc_content =
246
+ if @view_context && @__vc_render_in_block
247
+ view_context.capture(self, &@__vc_render_in_block)
248
+ elsif defined?(@__vc_content_set_by_with_content)
249
+ @__vc_content_set_by_with_content
250
+ end
234
251
  end
235
252
 
236
253
  def content_evaluated?
237
- @_content_evaluated
254
+ @__vc_content_evaluated
238
255
  end
239
256
 
240
- # The controller used for testing components.
241
- # Defaults to ApplicationController, but can be configured
242
- # on a per-test basis using `with_controller_class`.
243
- # This should be set early in the initialization process and should be a string.
257
+ # Set the controller used for testing components:
258
+ #
259
+ # config.view_component.test_controller = "MyTestController"
260
+ #
261
+ # Defaults to ApplicationController. Can also be configured on a per-test
262
+ # basis using `with_controller_class`.
263
+ #
244
264
  mattr_accessor :test_controller
245
265
  @@test_controller = "ApplicationController"
246
266
 
247
- # Configure if render monkey patches should be included or not in Rails <6.1.
267
+ # Set if render monkey patches should be included or not in Rails <6.1:
268
+ #
269
+ # config.view_component.render_monkey_patch_enabled = false
270
+ #
248
271
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
249
272
 
273
+ # Enable or disable source code previews in component previews:
274
+ #
275
+ # config.view_component.show_previews_source = true
276
+ #
277
+ # Defaults to `false`.
278
+ #
279
+ mattr_accessor :show_previews_source, instance_writer: false, default: false
280
+
281
+ # Always generate a Stimulus controller alongside the component:
282
+ #
283
+ # config.view_component.generate_stimulus_controller = true
284
+ #
285
+ # Defaults to `false`.
286
+ #
287
+ mattr_accessor :generate_stimulus_controller, instance_writer: false, default: false
288
+
289
+ # Path for component files
290
+ #
291
+ # config.view_component.view_component_path = "app/my_components"
292
+ #
293
+ # Defaults to "app/components".
294
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
295
+
250
296
  class << self
297
+ # @private
251
298
  attr_accessor :source_location, :virtual_path
252
299
 
253
300
  # EXPERIMENTAL: This API is experimental and may be removed at any time.
@@ -257,6 +304,7 @@ module ViewComponent
257
304
  # strings starting without the "dot", example: `["erb", "haml"]`.
258
305
  #
259
306
  # For example, one might collect sidecar CSS files that need to be compiled.
307
+ # @private TODO: add documentation
260
308
  def _sidecar_files(extensions)
261
309
  return [] unless source_location
262
310
 
@@ -277,11 +325,12 @@ module ViewComponent
277
325
  # end
278
326
  #
279
327
  # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
280
- nested_component_files = if name.include?("::") && component_name != filename
281
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
282
- else
283
- []
284
- end
328
+ nested_component_files =
329
+ if name.include?("::") && component_name != filename
330
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
331
+ else
332
+ []
333
+ end
285
334
 
286
335
  # view files in the same directory as the component
287
336
  sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
@@ -291,16 +340,24 @@ module ViewComponent
291
340
  (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
292
341
  end
293
342
 
294
- # Render a component collection.
343
+ # Render a component for each element in a collection ([documentation](/guide/collections)):
344
+ #
345
+ # render(ProductsComponent.with_collection(@products, foo: :bar))
346
+ #
347
+ # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
348
+ # @param args [Arguments] Arguments to pass to the ViewComponent every time.
295
349
  def with_collection(collection, **args)
296
350
  Collection.new(self, collection, **args)
297
351
  end
298
352
 
299
353
  # Provide identifier for ActionView template annotations
354
+ #
355
+ # @private
300
356
  def short_identifier
301
357
  @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
302
358
  end
303
359
 
360
+ # @private
304
361
  def inherited(child)
305
362
  # Compile so child will inherit compiled `call_*` template methods that
306
363
  # `compile` defines
@@ -318,11 +375,14 @@ module ViewComponent
318
375
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
319
376
 
320
377
  # Removes the first part of the path and the extension.
321
- child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
378
+ child.virtual_path = child.source_location.gsub(
379
+ %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
380
+ )
322
381
 
323
382
  super
324
383
  end
325
384
 
385
+ # @private
326
386
  def compiled?
327
387
  compiler.compiled?
328
388
  end
@@ -331,50 +391,39 @@ module ViewComponent
331
391
  #
332
392
  # Do as much work as possible in this step, as doing so reduces the amount
333
393
  # of work done each time a component is rendered.
394
+ # @private
334
395
  def compile(raise_errors: false)
335
396
  compiler.compile(raise_errors: raise_errors)
336
397
  end
337
398
 
399
+ # @private
338
400
  def compiler
339
- @_compiler ||= Compiler.new(self)
401
+ @__vc_compiler ||= Compiler.new(self)
340
402
  end
341
403
 
342
404
  # we'll eventually want to update this to support other types
405
+ # @private
343
406
  def type
344
407
  "text/html"
345
408
  end
346
409
 
410
+ # @private
347
411
  def format
348
412
  :html
349
413
  end
350
414
 
415
+ # @private
351
416
  def identifier
352
417
  source_location
353
418
  end
354
419
 
355
- def with_content_areas(*areas)
356
- ActiveSupport::Deprecation.warn(
357
- "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
358
- "Use slots (https://viewcomponent.org/guide/slots.html) instead."
359
- )
360
-
361
- if areas.include?(:content)
362
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
363
- end
364
-
365
- areas.each do |area|
366
- define_method area.to_sym do
367
- content unless content_evaluated? # ensure content is loaded so content_areas will be defined
368
- instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
369
- end
370
- end
371
-
372
- self.content_areas = areas
373
- end
374
-
375
- # Support overriding collection parameter name
376
- def with_collection_parameter(param)
377
- @provided_collection_parameter = param
420
+ # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
421
+ #
422
+ # with_collection_parameter :item
423
+ #
424
+ # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
425
+ def with_collection_parameter(parameter)
426
+ @provided_collection_parameter = parameter
378
427
  end
379
428
 
380
429
  # Ensure the component initializer accepts the
@@ -382,6 +431,7 @@ module ViewComponent
382
431
  # validate that the default parameter name
383
432
  # is accepted, as support for collection
384
433
  # rendering is optional.
434
+ # @private TODO: add documentation
385
435
  def validate_collection_parameter!(validate_default: false)
386
436
  parameter = validate_default ? collection_parameter : provided_collection_parameter
387
437
 
@@ -393,29 +443,35 @@ module ViewComponent
393
443
  # the component.
394
444
  if initialize_parameters.empty?
395
445
  raise ArgumentError.new(
396
- "#{self} initializer is empty or invalid."
446
+ "The #{self} initializer is empty or invalid." \
447
+ "It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
448
+ "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
449
+ "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
397
450
  )
398
451
  end
399
452
 
400
453
  raise ArgumentError.new(
401
- "#{self} initializer must accept " \
402
- "`#{parameter}` collection parameter."
454
+ "The initializer for #{self} does not accept the parameter `#{parameter}`, " \
455
+ "which is required in order to render it as a collection.\n\n" \
456
+ "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
457
+ "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
403
458
  )
404
459
  end
405
460
 
406
461
  # Ensure the component initializer does not define
407
462
  # invalid parameters that could override the framework's
408
463
  # methods.
464
+ # @private TODO: add documentation
409
465
  def validate_initialization_parameters!
410
466
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
411
467
 
412
468
  raise ViewComponent::ComponentError.new(
413
- "#{self} initializer cannot contain " \
414
- "`#{RESERVED_PARAMETER}` since it will override a " \
415
- "public ViewComponent method."
469
+ "#{self} initializer cannot accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
470
+ "public ViewComponent method. To fix this issue, rename the parameter."
416
471
  )
417
472
  end
418
473
 
474
+ # @private
419
475
  def collection_parameter
420
476
  if provided_collection_parameter
421
477
  provided_collection_parameter
@@ -424,18 +480,22 @@ module ViewComponent
424
480
  end
425
481
  end
426
482
 
483
+ # @private
427
484
  def collection_counter_parameter
428
485
  "#{collection_parameter}_counter".to_sym
429
486
  end
430
487
 
488
+ # @private
431
489
  def counter_argument_present?
432
490
  initialize_parameter_names.include?(collection_counter_parameter)
433
491
  end
434
492
 
493
+ # @private
435
494
  def collection_iteration_parameter
436
495
  "#{collection_parameter}_iteration".to_sym
437
496
  end
438
497
 
498
+ # @private
439
499
  def iteration_argument_present?
440
500
  initialize_parameter_names.include?(collection_iteration_parameter)
441
501
  end