Delegating slot inner-content and attributes to a component

I want to build a custom select.
I need to render all options (when expanded) and always the selected option additionally at the top.

<.select>
   <:opt label="label1">opt-inner-1</:opt>
   <:opt label="label2" selected>opt-inner-2</:opt>
</.select>

will render like

selected: opt-inner-2
opt-inner-1
opt-inner-2

I want the options rendered in an extra component.
But I can’t just delegate the inner-block to the option component.

I tried this:

def select(assigns) do
  assigns = assign_new(assigns, :selected, fn -> Enum.find(assigns.opt, & &1[:selected]) end)

  ~H"""
  <div>
    <div>selected: <%= @selected.label %></div>
    <%= for opt <- @opt do %>
      <.option {opt} />
    <% end %>
  </div>
  """
end

def option(assigns) do
  ~H"""
  <div style="padding: 1rem">
    <div><%= @label %></div>
    <div :if={@inner_block}>
      (want to render inner block) <%!-- <%= render_slot(@inner_block) %> --%>
    </div>
  </div>
  """
end

You’re not providing @inner_block to <.option />:
<.option {opt}><%= render_slot(opt) %></.option>

1 Like

thanks, this works. I thought I’d pass the inner_block, because opt looks like this:

<%= for opt <- @opt do %>
  <% dbg(opt) %>
opt #=> %{
  __slot__: :opt,
  inner_block: #Function<2.56207531/2 in Porter.nest/1>,
  label: "label1"
}

But its not in the right form somehow.

I did have to make an adjustment though: The inner block in <.option /> will always be truthy now, even if its not set, so I have to do:

<.select>
  <:opt label="label1">opt-inner-1</:opt>
  <:opt label="label2" selected>opt-inner-2</:opt>
  <:opt label="label3" />
</.select>

...

defmodule Components.Forms do
  use Phoenix.Component

  def select(assigns) do
    assigns = assign_new(assigns, :selected, fn -> Enum.find(assigns.opt, & &1[:selected]) end)

    ~H"""
    <div>
      <div>selected: <%= @selected.label %></div>
      <%= for opt <- @opt do %>
        <.option {opt}>
          <%= if opt.inner_block do %>
            <%= render_slot(opt) %>
          <% end %>
        </.option>
      <% end %>
    </div>
    """
  end

  def option(assigns) do
    ~H"""
    <div style="padding: 1rem">
      <div><%= @label %></div>
      <div>
        <%= render_slot(@inner_block) %>
      </div>
    </div>
    """
  end
end

LV doesn’t allow passing slots like that.

1 Like

It’s amazing how flexible and easy this all is. LV is just a masterpiece.

1 Like