Skip to content

Allow passing arguments to a slot block #1810

@RolandStuder

Description

@RolandStuder

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
end

Then 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
end

In 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions