How to pass slots to another component?

Hi,
I have the following function component structure hiercharchy:

  • page with table
    • data_table component
      • some CSS around the following components
      • table component
      • pagination component

The table component expects data as slots, e.g. like this:
<:col let={col} label="Creation Date" field={:inserted_at}><%= col.inserted_at %></:col>

I have different pages with different kind of data, so the structure varies. How can I pass the data (like columns) to the table component FROM my data_table component?

I was trying to pass it as attribute and then do sth like this:

for col <- my_data do
  <:col let={col} label="Creation Date" field={:inserted_at}><%= col.inserted_at %></:col>
end

But that does not work as I get A slot entry must be a direct child of a component error. Any ideas?

2 Likes

Can you post snippets of the whole thing like how you want to wrap 3 things - data_table, table and slot.

1 Like

Hmm I’m also curious about this, was building something similar and ended up copying the table component markup (it consists of <.table>, <.thead>, <.tr> function components so it was easy to do).

You could pass col as an assign like so:

<.table col={[... list ... ]}>

Since I think a slot gets turned into an assign anyway but with a special data structure. But I am not sure how to create this data structure. Perhaps via inner_block/2? Haven’t tried it yet.

2 Likes

Here you go:

  def data_table(assigns) do
    ~H"""
    <div class="w-full overflow-hidden rounded-lg ring-1 ring-black ring-opacity-5">
      <div class="w-full overflow-x-auto">
        <.table
          items={@items}
          meta={@meta}
        >
          <:col let={record} label="Name" field={:name}><%= record.name %></:col>
          <:col let={record} label="Creation Date" field={:inserted_at}><%= record.inserted_at %></:col>
        </.table>
      </div>
      <.pagination meta={@meta} />
    </div>
    """
  end

The table component isn’t under my control and just expects data using those col slots. So how can I pass data through my data_table component to that component when I don’t know the fields beforehand?

1 Like

Yeah, that’s a good hint.

It actually works like this. So instead of defining those slots, you can just pass it as an attribute. That could look like this:

   data =
      for {header, field, value} <- assigns.my_data do
        value_block = inner_block(:col, do: value)
        %{__slot__: :col, label: header, field: field, inner_block: value_block}
      end

    ~H"""
        <.table
          col={data}
          meta={@meta}
        />
    """

It feels a little bit hacky though. Thank you.

2 Likes

The problem is actually how I can pass the value from let into that structure. Haven’t found a solution for that yet…

1 Like

Okay so I think I figured it out:

This is just a macro that returns a Function that takes two arguments: parent_changed, and arg. The latter is what the slot yields.

So you can just write:

inner_block = fn _parent_changed, arg ->
  "hello #{arg}"
end
1 Like

Haven’t tried that, but the solution is actually way easier than expected.
As named slots are available as an assign, we can just pass it on to the next component.

Referring to my structure shown above…:

<:col let={record} label="Name" field={:name}><%= record.name %></:col>
<:col let={record} label="Creation Date" field={:inserted_at}><%= record.inserted_at %></:col>

Those slots in my data_table becomes @col in my table component. I just need to do col={@col} there and it passes them to the next component.

1 Like

This is slightly related, hence posting here.

passing slots via @col is understood.

If I still want to do

for col <- my_data do
  <:col let={col} label="Creation Date" field={:inserted_at}><%= col.inserted_at %></:col>
end

to generate the slots in my component dynamically, I get slot entry must be a direct child of a component error.

How does one achieve multiple slots of same name (like several <:col>) ?

  1. pass as attribute, then handle it in the component.
  2. use multiple <:col>, but that does not support for comprehensions.

LiveView 0.18 added :for and :if, which means you no longer need to “wrap” slots, but you can do:

<:col :for={col <- my_data} … />

Unfortunately, I am on liveview 0.17.11

and upgrade to 0.18 doesn’t seem near in my project.

What are other ways one could do this in 0.17.11?

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#render_slot/2

This makes clear that slots can be accessed in assigns via @cols attribute. (for anyone who stumbles upon this!)