How to pass down slot to child componenet

Hello, i have two components: <.table /> and <.simple_table />

Both have almost exactly the same api, only difference is that <.table /> has pagination/filter and some extra stuff around simple table. <.table /> internally calls <.simple_table />.

My issue is that they both have slot :col. If i use <.table /> and provide :col slot, i can pass it down like so <.simple_table :col={@col} /> and technically it works. Everything renders as it should, but it raises warning and in our CI we use --warnings-as-errors so pipeline cannot pass and it cannot be deployed.

warning: undefined attribute "col" for component Table.simple_table/1
warning: missing required slot "col" for component Table.simple_table/1

Hi! Your code snippets for those two components would be much helpful.

This should work in table for passing forward all the slots:

<:col :for={col <- @col} {col} />
** (exit) an exception was raised:
    ** (BadFunctionError) expected a function, got: nil
        (phoenix_live_view 0.18.2) lib/phoenix_component.ex:927: Phoenix.Component.__render_slot__/

My understanding is that even tho <:col :for={col <- @col} {col} /> col contains inner_block callback function, <:col> slot does not have it, so it is overwritten with nil

for more context <.simple_table />:

<table>
  <tr>
    <%= for col <- @col do %>
      <th><%= col.label %></th>
    <% end %>
  </tr>
  <%= for row <- @rows do %>
    <tr>
      <%= for col <- @col do %>
        <td><%= render_slot(col, row) %></td>
      <% end %>
    </tr>
  <% end %>
</table>
<:col :for={col <- @col} {col}><%= render_slot(col) %></:col>

I tried it so myself as well :smiley: it does not work as well.

** (KeyError) key :id not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map

First col definition:

<.table data={@data}>
  <:col :let={row} label="Id">
    <%= row.id %>
  </:col>
  ...
</.table>

In sub component :col slot is called like so: <%= render_slot(col, row) %>. When i call it like you showed, it tries to render inner block there rather than pass down its definition so It needs row data as well.

To have row i need to iterate over data as well. In that case, it is questionable do i even want to call simple_table at all, maybe just duplicate logic inline.

You can add let support to the markup as well:

<:col :for={col <- @col} {col} :let={row}><%= render_slot(col, row) %></:col>

There’s really nothing special here. It’s just wireing functionality from both ends together. Just try to ignore the itch of “passing the slots through”. You want each component to handle it’s own slots, even though they might be called the same.

2 Likes

Thanks mate, it worked. I tried multiple solutions but somehow couldn’t grasp this. So simple.

Just try to ignore the itch of “passing the slots through”

Hmmm. Any component with growing complexity, might and with time will have multiple subcomponents. Handling slots at top level would mean to duplicate logic. Of course one shouldn’t sprinkle it left and right, but this is valid use case (IMHO) that can occur time to time.

If you’re really passing things through 1:1 it’ll be the one liner I’ve shown – and it’ll be explicit what it does. If not, you’ll need to customize things further anyways. So I’m not sure what benefit you’re invisioning from passing slots forward in a more direct manner.

If you’re really passing things through 1:1 it’ll be the one liner I’ve shown – and it’ll be explicit what it does

Thats what i meant. Thanks for help. :bowing_man: