Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yield the Slot instance into the captured block and allow access to the view_context #471

Closed
markstanley-nps opened this issue Sep 9, 2020 · 4 comments

Comments

@markstanley-nps
Copy link

Feature request

An instance of the component is yielded into the captured block so that you can access methods on the component when rendering the page, but you can't do this with the Slot object (the below is not possible for the slot)

<%= render(BoxComponent.new) do |component| %>
  <%= component.some_function %>
  <% component.slot(:header, classes: "my-class-name") do |slot| %>
    <%= slot.some_function %>
    This is my header!
  <% end %>
  ....
<% end %>

Slotable can be changed to allow this, and also to make the view_context available in the slot. It doesn't seem to break any of the existing tests and makes slots behave more like components when rendering

      # Instantiate Slot class, accommodating Slots that don't accept arguments
      slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
      slot_instance.view_context = view_context

      # Capture block and assign to slot_instance#content
      slot_instance.content = view_context.capture(slot_instance,&block).to_s.strip.html_safe if block_given?

Motivation

ViewComponent is allowing me to dry up and simplify my existing user interface code (so thanks for developing what is a great library) and this change will help when handling tables of data.

At the moment I am trying to set up components that handle the display of rows of data. I have a method of doing this using the with_collection method, but using slots seems to be a more natural pattern. However I would have to pass the data object into each component within the slot. The above change allows me to access the data component from within the slot instance and makes slots behave more like the component.

I am trying to get to something like the below.

  <%= render(TableComponent.new(caption: 'Test Models')) do |t| %>
    <% @test_models.each do |o| %>
      <%= t.slot(:row, object: o) do |r| %>
        <%= r.table_cell(method: :email) %>
        <%= r.table_cell(method: :text) %>
        <%= r.table_cell(method: :checkbox) %>
      <% end %>
    <% end %>
  <% end %>

In the above the table_cell method is a method on the slot that renders the table cell component using the view context and passing in the specific object. This avoids the need for the developer to repeat the object for every call.

@joelhawksley
Copy link
Member

@markstanley-nps thank you for taking the time to file this issue! Rendering tables with ViewComponents is something a lot of folks are looking to do, and it's cool to see your implementation ❤️ .

I do wonder if the collection behavior proposed here would be useful to you in this case.

Seeing how you have a proposed change here, might you be willing to open a PR?

@markstanley-nps
Copy link
Author

Thanks for responding.

The collection behaviour here would simplify the interface for the developer when using the collection slot. However, it wouldn't solve my issue that the developer needs access to the slot instance in the page.

I had a think about this last night and there are other options for my use case. One would be to pass the list of fields in to the slot as part of the initialisation, which is what we used to do in our old UI system, but this starts getting complicated and messy when you need to configure each field

    <% @test_models.each do |o| %>
      <%= t.slot(:row, object: o, fields: [:email, :text, : checkbox] ) %>
    <% end %>

Having said that a system closer to to the above may allow me to avoid the need to have individual components and related templates for the cells at least for displaying tables. Potentially removing the need to have access to the view context in the slot, and keeping the implementation closer to the original idea behind slots.

I will try out a few ideas over the next couple of days and then open a PR

@markstanley-nps
Copy link
Author

@joelhawksley I have tried out a few other ideas over the last couple of days, and for my use case I don't need the change I was proposing. The data I need to render in the table is in an array of objects so I can initialise the component with the array and the list of columns and I don't need to use slots.

If I was rendering a table with arbitrary data from the page, it is possible that the change I proposed would be useful so that you could set up the column within the rows. If I hit this issue then I will look at again. Potentially you may need to allow slots to be slotable themselves.

For anyone else playing with tables this is the key bits of the solution

On the page index.html.erb

<%= render(TableComponent.new(caption: 'Test Models', object_name: 'TestModel', objects: @test_models)) do |t| %>
  <% t.table_column(method: :text, header: true, width: 'one-half' ) %>
  <% t.table_column(method: :number, type: :number) %>
  <% t.table_column(method: :date ) %>
  <% t.table_column(method: :datetime ) %>
<% end %>

The above initialises the table component with the list of objects (test_models) and the list of columns. The table component has methods rows and columns returning the arrays of rows and columns created by the above.

The template for the component is then as below

<table>
  <caption><%= caption %></caption>
  <thead>
    <% if columns.any?%>
      <tr>
        <% columns.each do |col| %>
          <th scope="col" class="<%= 'numeric' if col.numeric? %>"><%= col.label_text %></th>
        <% end %>
      </tr>
    <%end%>
  </thead>
  <tbody>
    <% if rows.any?%>
      <% rows.each do |row| %>
        <% if columns.any?%>
          <tr>
            <% columns.each do |col| %>
              <% if col.header? %>
                <th scope="row"><%= col.cell(object: row.object).formatted_value %></th>
              <% else %>
                <td class="<%= 'numeric' if col.numeric? %>"><%= col.cell(object: row.object).formatted_value %></td>
              <%end%>
            <% end %>
          </tr>
        <%end%>
      <% end %>
    <%end%>
  </tbody>
</table>

@joelhawksley
Copy link
Member

@markstanley-nps thanks for the follow-up! @andrewmcodes might find this relevant ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants