-
Notifications
You must be signed in to change notification settings - Fork 487
Description
Feature request
In the case of some complex components, I want to create a abstracted API. The example I have is a TableComponent
When I use a component, I can use the component instance in a block like:
<%= render TableComponent.new(rows: @products) do |table|
<%= table.with_column(label: "Name") { |column| … }
<% end >
The current API does not allow you to iterate through the rows and render all column with the data of said rows by declaring a slot like this. So in order to achieve that I had to create a work around (that I will present later). I wondered if others would find it a worthwhile addition to allow controlling what is passed into the block, so one can do:
<%= render TableComponent.new(rows: @products) do |table|
<%= table.with_column(label: "Name", &:name)
<%= table.with_column(label: "Price") { |product| number_to_currency(product.price) }
<% end >
Motivation
I some instances allowing to control what is passed into the slot block when rendering it, can allow for some really nice api, when iterating through collections.
Workaround
You can make the above API work by writing you own methods like:
class TableComponent < ViewComponent::Base
def initialize(rows:)
@rows = rows
@columns = []
end
def column(label, &block)
@columns << Column.new(label, &block)
end
def before_render
content # to ensure block is called
end
class Column
attr_reader :label, :td_block
def initialize(label, &td_block)
@label = label
@td_block = td_block
end
end
endThen in the component template you can do:
<tr>
<% columns.each do |column| %>
<th><%= column.label %>
<% end %>
</tr>
<% @rows.each do |row| %>
<tr>
<% @columns.each do |column| %>
<td>
<%= view_context.capture(row, &column.td_block) %>
</td>
<% end %>
</tr>
<% end %>So this works wonderfully (though it took a while to figure it out).
So I got a solution for me, but wondered whether we might add this functionality to the core library, so it could be used like this:
Proposal
Allow to declare slots like normal
class TableComponent < ViewComponent::Base
renders_many :columns, TableComponent::Column
...
class Column < ViewComponent::Base
def initialize(label)
@label = label
end
def title(label)
content_tag :h2, label
end
end
endIn the component template file, allow to pass args, that will be passed to the block upon rendering:
<% @rows.each do |row| %>
<tr>
<% columns.each do |column| %>
<td>
<%= column(row) %>
</td>
<% end %>
</tr>
<% end %>So in this case you would pass the row data into the block instead of the component, you could still pass the component as needed.
column(column, row)So you can use it in the view like this:
<%= TableComponent(rows: @products).new |table| %>
<%= table.with_column |column, product| %>
<%= column.title_style product.style %>
<%= truncate(product.style) %>
<% end %>
<% end %>It would not be hard to adjust the view component code to adjust this. Though I am not sure if this kind of abstraction is what view component is striving for, especially since in this example you see the Column is not really a view component, but only an object to hold the block and params to configure the column, so it is muddying the responsability of the ViewComponent a bit.
ps: I would be happy to make a PR to enable this, I am also fine if this doesn't fit the vision of ViewComponent and the proposal is rejected.