view_component 2.32.0 → 2.36.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 (34) 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/test_mailer/test_email.html.erb +1 -0
  8. data/app/views/view_components/_preview_source.html.erb +17 -0
  9. data/app/views/view_components/preview.html.erb +6 -2
  10. data/{CHANGELOG.md → docs/CHANGELOG.md} +127 -1
  11. data/lib/rails/generators/abstract_generator.rb +29 -0
  12. data/lib/rails/generators/component/component_generator.rb +5 -5
  13. data/lib/rails/generators/erb/component_generator.rb +7 -16
  14. data/lib/rails/generators/haml/component_generator.rb +6 -16
  15. data/lib/rails/generators/slim/component_generator.rb +6 -16
  16. data/lib/view_component.rb +2 -0
  17. data/lib/view_component/base.rb +152 -85
  18. data/lib/view_component/collection.rb +7 -2
  19. data/lib/view_component/compile_cache.rb +1 -0
  20. data/lib/view_component/compiler.rb +87 -53
  21. data/lib/view_component/content_areas.rb +57 -0
  22. data/lib/view_component/engine.rb +31 -3
  23. data/lib/view_component/instrumentation.rb +21 -0
  24. data/lib/view_component/preview.rb +19 -8
  25. data/lib/view_component/previewable.rb +16 -18
  26. data/lib/view_component/slot_v2.rb +34 -27
  27. data/lib/view_component/slotable.rb +2 -1
  28. data/lib/view_component/slotable_v2.rb +58 -24
  29. data/lib/view_component/test_helpers.rb +7 -1
  30. data/lib/view_component/translatable.rb +6 -5
  31. data/lib/view_component/version.rb +1 -1
  32. data/lib/view_component/with_content_helper.rb +5 -2
  33. data/lib/yard/mattr_accessor_handler.rb +19 -0
  34. metadata +76 -39
@@ -1,8 +1,12 @@
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"
@@ -10,7 +14,7 @@ module Rails
10
14
  class_option :inline, type: :boolean, default: false
11
15
 
12
16
  def create_component_file
13
- template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
17
+ template "component.rb", File.join(component_path, class_path, "#{file_name}_component.rb")
14
18
  end
15
19
 
16
20
  hook_for :test_framework
@@ -23,10 +27,6 @@ module Rails
23
27
 
24
28
  private
25
29
 
26
- def file_name
27
- @_file_name ||= super.sub(/_component\z/i, "")
28
- end
29
-
30
30
  def parent_class
31
31
  defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
32
32
  end
@@ -1,32 +1,23 @@
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
11
14
 
12
- def copy_view_file
13
- unless options["inline"]
14
- template "component.html.erb", destination
15
- end
16
- end
17
-
18
- private
19
-
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")
25
- end
15
+ def engine_name
16
+ "erb"
26
17
  end
27
18
 
28
- def file_name
29
- @_file_name ||= super.sub(/_component\z/i, "")
19
+ def copy_view_file
20
+ super
30
21
  end
31
22
  end
32
23
  end
@@ -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
@@ -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
 
@@ -8,6 +9,7 @@ module ViewComponent
8
9
  autoload :Base
9
10
  autoload :Compiler
10
11
  autoload :ComponentError
12
+ autoload :Instrumentation
11
13
  autoload :Preview
12
14
  autoload :PreviewTemplateError
13
15
  autoload :TestHelpers
@@ -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) + _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
@@ -136,6 +145,7 @@ module ViewComponent
136
145
  # This prevents an exception when rendering a partial inside of a component that has also been rendered outside
137
146
  # of the component. This is due to the partials compiled template method existing in the parent `view_context`,
138
147
  # and not the component's `view_context`.
148
+ #
139
149
  # @private
140
150
  def render(options = {}, args = {}, &block)
141
151
  if options.is_a? ViewComponent::Base
@@ -145,23 +155,46 @@ module ViewComponent
145
155
  end
146
156
  end
147
157
 
148
- # 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.
149
160
  #
150
161
  # @return [ActionController::Base]
151
162
  def controller
152
- raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
153
- @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
154
175
  end
155
176
 
156
- # 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.
157
179
  #
158
180
  # @return [ActionView::Base]
159
181
  def helpers
160
- raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
161
- @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
162
194
  end
163
195
 
164
196
  # Exposes .virtual_path as an instance method
197
+ #
165
198
  # @private
166
199
  def virtual_path
167
200
  self.class.virtual_path
@@ -174,41 +207,31 @@ module ViewComponent
174
207
  end
175
208
 
176
209
  # For caching, such as #cache_if
210
+ #
177
211
  # @private
178
212
  def format
179
213
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
180
- if defined?(@variant)
181
- @variant
214
+ if defined?(@__vc_variant)
215
+ @__vc_variant
182
216
  end
183
217
  end
184
218
 
185
- # Assign the provided content to the content area accessor
186
- # @private
187
- def with(area, content = nil, &block)
188
- unless content_areas.include?(area)
189
- raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
190
- end
191
-
192
- if block_given?
193
- content = view_context.capture(&block)
194
- end
195
-
196
- instance_variable_set("@#{area}".to_sym, content)
197
- nil
198
- end
199
-
200
- # @private TODO: add documentation
219
+ # Use the provided variant instead of the one determined by the current request.
220
+ #
221
+ # @param variant [Symbol] The variant to be used by the component.
222
+ # @return [self]
201
223
  def with_variant(variant)
202
- @variant = variant
224
+ @__vc_variant = variant
203
225
 
204
226
  self
205
227
  end
206
228
 
207
- # 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.
208
231
  #
209
232
  # @return [ActionDispatch::Request]
210
233
  def request
211
- @request ||= controller.request
234
+ @request ||= controller.request if controller.respond_to?(:request)
212
235
  end
213
236
 
214
237
  private
@@ -216,31 +239,54 @@ module ViewComponent
216
239
  attr_reader :view_context
217
240
 
218
241
  def content
219
- return @_content if defined?(@_content)
220
- @_content_evaluated = true
221
-
222
- @_content = if @view_context && @_render_in_block
223
- view_context.capture(self, &@_render_in_block)
224
- elsif defined?(@_content_set_by_with_content)
225
- @_content_set_by_with_content
226
- 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
227
251
  end
228
252
 
229
253
  def content_evaluated?
230
- @_content_evaluated
254
+ @__vc_content_evaluated
231
255
  end
232
256
 
233
- # The controller used for testing components.
234
- # Defaults to ApplicationController, but can be configured
235
- # on a per-test basis using `with_controller_class`.
236
- # 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
+ #
237
264
  mattr_accessor :test_controller
238
265
  @@test_controller = "ApplicationController"
239
266
 
240
- # 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
+ #
241
271
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
242
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
+ # Path for component files
282
+ #
283
+ # config.view_component.view_component_path = "app/my_components"
284
+ #
285
+ # Defaults to "app/components".
286
+ mattr_accessor :view_component_path, instance_writer: false, default: "app/components"
287
+
243
288
  class << self
289
+ # @private
244
290
  attr_accessor :source_location, :virtual_path
245
291
 
246
292
  # EXPERIMENTAL: This API is experimental and may be removed at any time.
@@ -250,6 +296,7 @@ module ViewComponent
250
296
  # strings starting without the "dot", example: `["erb", "haml"]`.
251
297
  #
252
298
  # For example, one might collect sidecar CSS files that need to be compiled.
299
+ # @private TODO: add documentation
253
300
  def _sidecar_files(extensions)
254
301
  return [] unless source_location
255
302
 
@@ -270,11 +317,12 @@ module ViewComponent
270
317
  # end
271
318
  #
272
319
  # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
273
- nested_component_files = if name.include?("::") && component_name != filename
274
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
275
- else
276
- []
277
- end
320
+ nested_component_files =
321
+ if name.include?("::") && component_name != filename
322
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
323
+ else
324
+ []
325
+ end
278
326
 
279
327
  # view files in the same directory as the component
280
328
  sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
@@ -284,16 +332,24 @@ module ViewComponent
284
332
  (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
285
333
  end
286
334
 
287
- # Render a component collection.
335
+ # Render a component for each element in a collection ([documentation](/guide/collections)):
336
+ #
337
+ # render(ProductsComponent.with_collection(@products, foo: :bar))
338
+ #
339
+ # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
340
+ # @param args [Arguments] Arguments to pass to the ViewComponent every time.
288
341
  def with_collection(collection, **args)
289
342
  Collection.new(self, collection, **args)
290
343
  end
291
344
 
292
345
  # Provide identifier for ActionView template annotations
346
+ #
347
+ # @private
293
348
  def short_identifier
294
349
  @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
295
350
  end
296
351
 
352
+ # @private
297
353
  def inherited(child)
298
354
  # Compile so child will inherit compiled `call_*` template methods that
299
355
  # `compile` defines
@@ -311,11 +367,14 @@ module ViewComponent
311
367
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
312
368
 
313
369
  # Removes the first part of the path and the extension.
314
- child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
370
+ child.virtual_path = child.source_location.gsub(
371
+ %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
372
+ )
315
373
 
316
374
  super
317
375
  end
318
376
 
377
+ # @private
319
378
  def compiled?
320
379
  compiler.compiled?
321
380
  end
@@ -324,50 +383,39 @@ module ViewComponent
324
383
  #
325
384
  # Do as much work as possible in this step, as doing so reduces the amount
326
385
  # of work done each time a component is rendered.
386
+ # @private
327
387
  def compile(raise_errors: false)
328
388
  compiler.compile(raise_errors: raise_errors)
329
389
  end
330
390
 
391
+ # @private
331
392
  def compiler
332
- @_compiler ||= Compiler.new(self)
393
+ @__vc_compiler ||= Compiler.new(self)
333
394
  end
334
395
 
335
396
  # we'll eventually want to update this to support other types
397
+ # @private
336
398
  def type
337
399
  "text/html"
338
400
  end
339
401
 
402
+ # @private
340
403
  def format
341
404
  :html
342
405
  end
343
406
 
407
+ # @private
344
408
  def identifier
345
409
  source_location
346
410
  end
347
411
 
348
- def with_content_areas(*areas)
349
- ActiveSupport::Deprecation.warn(
350
- "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
351
- "Use slots (https://viewcomponent.org/guide/slots.html) instead."
352
- )
353
-
354
- if areas.include?(:content)
355
- raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
356
- end
357
-
358
- areas.each do |area|
359
- define_method area.to_sym do
360
- content unless content_evaluated? # ensure content is loaded so content_areas will be defined
361
- instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
362
- end
363
- end
364
-
365
- self.content_areas = areas
366
- end
367
-
368
- # Support overriding collection parameter name
369
- def with_collection_parameter(param)
370
- @provided_collection_parameter = param
412
+ # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
413
+ #
414
+ # with_collection_parameter :item
415
+ #
416
+ # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
417
+ def with_collection_parameter(parameter)
418
+ @provided_collection_parameter = parameter
371
419
  end
372
420
 
373
421
  # Ensure the component initializer accepts the
@@ -375,6 +423,7 @@ module ViewComponent
375
423
  # validate that the default parameter name
376
424
  # is accepted, as support for collection
377
425
  # rendering is optional.
426
+ # @private TODO: add documentation
378
427
  def validate_collection_parameter!(validate_default: false)
379
428
  parameter = validate_default ? collection_parameter : provided_collection_parameter
380
429
 
@@ -386,29 +435,35 @@ module ViewComponent
386
435
  # the component.
387
436
  if initialize_parameters.empty?
388
437
  raise ArgumentError.new(
389
- "#{self} initializer is empty or invalid."
438
+ "The #{self} initializer is empty or invalid." \
439
+ "It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
440
+ "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
441
+ "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
390
442
  )
391
443
  end
392
444
 
393
445
  raise ArgumentError.new(
394
- "#{self} initializer must accept " \
395
- "`#{parameter}` collection parameter."
446
+ "The initializer for #{self} does not accept the parameter `#{parameter}`, " \
447
+ "which is required in order 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."
396
450
  )
397
451
  end
398
452
 
399
453
  # Ensure the component initializer does not define
400
454
  # invalid parameters that could override the framework's
401
455
  # methods.
456
+ # @private TODO: add documentation
402
457
  def validate_initialization_parameters!
403
458
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)
404
459
 
405
460
  raise ViewComponent::ComponentError.new(
406
- "#{self} initializer cannot contain " \
407
- "`#{RESERVED_PARAMETER}` since it will override a " \
408
- "public ViewComponent method."
461
+ "#{self} initializer cannot accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
462
+ "public ViewComponent method. To fix this issue, rename the parameter."
409
463
  )
410
464
  end
411
465
 
466
+ # @private
412
467
  def collection_parameter
413
468
  if provided_collection_parameter
414
469
  provided_collection_parameter
@@ -417,14 +472,26 @@ module ViewComponent
417
472
  end
418
473
  end
419
474
 
475
+ # @private
420
476
  def collection_counter_parameter
421
477
  "#{collection_parameter}_counter".to_sym
422
478
  end
423
479
 
480
+ # @private
424
481
  def counter_argument_present?
425
482
  initialize_parameter_names.include?(collection_counter_parameter)
426
483
  end
427
484
 
485
+ # @private
486
+ def collection_iteration_parameter
487
+ "#{collection_parameter}_iteration".to_sym
488
+ end
489
+
490
+ # @private
491
+ def iteration_argument_present?
492
+ initialize_parameter_names.include?(collection_iteration_parameter)
493
+ end
494
+
428
495
  private
429
496
 
430
497
  def initialize_parameter_names