Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ nav_order: 6

## main

* Add `counter:` and `iteration:` options to `with_collection_parameter`, and make its parameter name optional, to explicitly declare whether a component receives the `_counter` and `_iteration` arguments when rendered as a collection.

*Ben Sheldon*

* Update development and documentation dependencies, baseline existing Audition findings, and accommodate updated lint rules.

*Joel Hawksley*
Expand Down
10 changes: 9 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,22 @@ Render a component for each element in a collection ([documentation](/guide/coll
render(ProductsComponent.with_collection(@products, foo: :bar))
```

### `.with_collection_parameter(parameter)`
### `.with_collection_parameter(parameter = nil, counter: nil, iteration: nil)`

Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):

```ruby
with_collection_parameter :item
```

By default, the `_counter` and `_iteration` arguments are passed when the
initializer's signature accepts them. Set `counter:` or `iteration:` to
explicitly always (`true`) or never (`false`) pass them:

```ruby
with_collection_parameter :item, counter: true, iteration: true
```

## Instance methods

### `#around_render` → [void]
Expand Down
18 changes: 16 additions & 2 deletions docs/guide/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ end
Since 2.5.0
{: .label }

ViewComponent defines a counter variable matching the parameter name above, followed by `_counter`. To access the variable, add it to `initialize` as an argument:
ViewComponent defines a counter variable matching the parameter name above, followed by `_counter`. To access the variable, declare it with `counter: true` and add it to `initialize` as an argument:

```ruby
class ProductComponent < ViewComponent::Base
with_collection_parameter counter: true

erb_template <<-ERB
<li>
<%= @counter %> <%= @product.name %>
Expand All @@ -94,10 +96,12 @@ Since 2.33.0

ViewComponent defines an iteration variable matching the parameter name above, followed by `_iteration`. This gives contextual information about the iteration to components within the collection (`#size`, `#index`, `#first?`, and `#last?`).

To access the variable, add it to `initialize` as an argument:
To access the variable, declare it with `iteration: true` and add it to `initialize` as an argument:

```ruby
class ProductComponent < ViewComponent::Base
with_collection_parameter iteration: true

erb_template <<-ERB
<li class="<%= "featured" if @iteration.first? %>">
<%= @product.name %>
Expand All @@ -111,6 +115,16 @@ class ProductComponent < ViewComponent::Base
end
```

## Declaring the counter and iteration

`counter:` and `iteration:` can be combined with a custom parameter name, and are inherited by subclasses:

```ruby
with_collection_parameter :item, counter: true, iteration: true
```

Set either to `false` to never pass it. If neither is declared, ViewComponent passes `_counter` and `_iteration` when it finds them in the component's `initialize` signature.

## Spacer components

Since 3.20.0
Expand Down
26 changes: 22 additions & 4 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def config

# Config option that strips trailing whitespace in templates before compiling them.
class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false
class_attribute :__vc_collection_counter, instance_accessor: false, instance_predicate: false, default: nil
class_attribute :__vc_collection_iteration, instance_accessor: false, instance_predicate: false, default: nil

attr_accessor :__vc_original_view_context
attr_reader :current_template
Expand Down Expand Up @@ -713,9 +715,21 @@ def __vc_compiler
# with_collection_parameter :item
# ```
#
# By default, the `_counter` and `_iteration` arguments are passed when the
# initializer's signature accepts them. Set `counter:` or `iteration:` to
# explicitly always (`true`) or never (`false`) pass them:
#
# ```ruby
# with_collection_parameter :item, counter: true, iteration: true
# ```
#
# @param parameter [Symbol] The parameter name used when rendering elements of a collection.
def with_collection_parameter(parameter)
@__vc_provided_collection_parameter = parameter
# @param counter [Boolean] Whether to pass the `_counter` argument.
# @param iteration [Boolean] Whether to pass the `_iteration` argument.
def with_collection_parameter(parameter = nil, counter: nil, iteration: nil)
@__vc_provided_collection_parameter = parameter if parameter
self.__vc_collection_counter = counter unless counter.nil?
self.__vc_collection_iteration = iteration unless iteration.nil?
@__vc_initialize_parameters = nil
end

Expand Down Expand Up @@ -776,7 +790,7 @@ def __vc_collection_counter_parameter

# @private
def __vc_counter_argument_present?
__vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
__vc_collection_argument_present?(__vc_collection_counter, __vc_collection_counter_parameter)
end

# @private
Expand All @@ -786,11 +800,15 @@ def __vc_collection_iteration_parameter

# @private
def __vc_iteration_argument_present?
__vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
__vc_collection_argument_present?(__vc_collection_iteration, __vc_collection_iteration_parameter)
end

private

def __vc_collection_argument_present?(provided, parameter)
provided.nil? ? __vc_initialize_parameter_names.include?(parameter) : provided
end

def __vc_splatted_keyword_argument_present?
__vc_initialize_parameters.flatten.include?(:keyrest)
end
Expand Down
54 changes: 54 additions & 0 deletions test/sandbox/test/collection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,59 @@ def test_collection_escapes_html_unsafe_around_render_output
assert_includes @rendered_content, "&lt;script&gt;alert(1)&lt;/script&gt;"
assert_predicate @rendered_content, :html_safe?
end

class ExplicitCountersComponent < ViewComponent::Base
with_collection_parameter :item, counter: true, iteration: true

def initialize(**)
assign(**)
end

def call
"<p>#{@item.name} #{@counter.inspect} #{@iteration&.last?.inspect}</p>".html_safe
end

private

def assign(item:, **options)
@item = item
@counter = options[:item_counter]
@iteration = options[:item_iteration]
end
end

class InheritedExplicitCountersComponent < ExplicitCountersComponent
end

class DisabledCountersComponent < ExplicitCountersComponent
with_collection_parameter counter: false, iteration: false

def initialize(item:, item_counter: nil, item_iteration: nil)
@item = item
@counter = item_counter
@iteration = item_iteration
end
end

def test_with_collection_parameter_passes_counter_and_iteration
render_inline(ExplicitCountersComponent.with_collection(@products))

assert_selector("p", text: "Radio clock 0 false")
assert_selector("p", text: "Mints 1 true")
end

def test_with_collection_parameter_counter_and_iteration_are_inherited
render_inline(InheritedExplicitCountersComponent.with_collection(@products))

assert_selector("p", text: "Radio clock 0 false")
assert_selector("p", text: "Mints 1 true")
end

def test_with_collection_parameter_counter_and_iteration_false_skips_them
render_inline(DisabledCountersComponent.with_collection(@products))

assert_selector("p", text: "Radio clock nil nil")
assert_selector("p", text: "Mints nil nil")
end
end
end
Loading