view_component 2.34.0 → 2.38.0

Sign up to get free protection for your applications and to get access to all the features.

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