Function/component as expression in heex

Hi!
This is my first post here. :grin: First of all, i appriciate for this community. I read posts here all day, and learned so much. Thank you! :blush:
I was looking forward to the new Phoenix version, so I could finally start my new project.
I’m a bit stuck. But I don’t know if there’s even a description of this anywhere in the documentation.
I would like create a dynamic drawer menu. The menu items could come from this list:

def main_menu do
    [
      %{id: "home", name: "Home", icon: &Heroicons.home/1, to: "/"},
      %{id: "invoices", name: "Invoices", icon: &Heroicons.list_bullet/1, to: "/invoices"},
      %{id: "partners", name: "Partners", icon: &Heroicons.users/1, to: "/partners"},
      %{id: "products", name: "Products", icon: &Heroicons.archive_box/1, to: "/products"},
      %{id: "settings", name: "Settings", icon: &Heroicons.adjustments_vertical/1, to: "/settings"},
    ]
  end

I loop through this list and try to display:

attr :menu, :list, default: Drawer.main_menu(), doc: "list of menu items"

def drawer_menu(assigns) do
    ~H"""
    <%= for item <- @menu do %>
      <li>
        <a href={item.to}>
          <%= item.name %>
        </a>
      </li>
    <% end %>
    """
end

I would like to show the Heroicon above the name, but i don’t know how could i achive this. I tried with <%= %>, but thrown error. <% %> also don’t work.
Some idea? Thank you! :slight_smile:

1 Like

Do those icons return heex or are they components themselves? <%= item.icon.() %> might work if it’s heex, otherwise component can be used for dynamic components.

Another approach to consider: use slots and keep the markup inline. Something like (apologies for syntax errors etc, I have not run this code):

attr :to
attr :id
slot :icon
slot :inner_block, required: true

def drawer_menu_item(assigns) do
  ~H"""
  <li id={@id}>
    <a href={@to}>
      <%= render_slot(@icon) %>
      <%= render_slot(@inner_block) %>
    </a>
  </li>
  """
end

and then call it with:

<ul class="drawer-menu">
  <.drawer_menu_item id="home" to="/">
    <icon:><Heroicons.home /></:icon>
    Home
  </.drawer_menu_item>
  <.drawer_menu_item id="invoices" to="/invoices">
    <icon:><Heroicons.list_bullet /></:icon>
    Invoices
  </.drawer_menu_item>
  <.drawer_menu_item id="partners" to="/partners">
    <icon:><Heroicons.users /></:icon>
    Partners
  </.drawer_menu_item>
</ul>

Pros + cons of this approach:

  • pro: it’s vastly easier to deal with future requests like “can we make just the users icon a little bigger” (by passing options to Heroicons.users) or “can we make one word of the name in this menu item bold” (by adding markup directly to the name)
  • pro: it’s trivial to add things like separators between groups of items, since the contents of the ul are just markup
  • con: it’s a little more verbose, especially with long closing tags like </.drawer_menu_item>
  • con: if something else needs the structured data from main_menu, this makes for bad duplication
2 Likes

@cmo
I tried component, didn’t throw error, but neither shown anything.
Képernyőkép 2022-11-24 083627

The code was:

<li>
      <a href={item.to}>
         <% HTMLEngine.component(
            item.icon,
            [solid: :true, class: "h-5 w-5"],
            {__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}
          ) %>
          <%= item.name %>
      </a>
</li>

Same with <% item.icon.() %>
It would be nice to have a solution like svelte:component.

I think i will choose the solution of @al2o3cr . I haven’t had time to try it yet. When it’s done, I’ll come back and check it as solution.
Thank you for your replies.

It won’t render anything if you use <% ... %>. You need to use <%= ... %>

With <%= … />, got this: &Heroicons.home/1 with arity 1 called with no arguments
If i pass arguments, then the assigns map is missing.

I tried also:

def drawer_menu(assigns) do
    assigns =
      assigns
      |> Map.put(:solid, :true)
      |> Map.put(:class, "h-5 w-5")

    ~H"""
    <%= for item <- @menu do %>
      <li>
        <a href={item.to}>
          <%= item.icon.(assigns) %>
          <%= item.name %>
        </a>
      </li>
    <% end %>
    """
  end

Got this: lists in Phoenix.HTML and templates may only contain integers representing bytes, binaries or other lists, got invalid entry: %{icon: &Heroicons.home/1, id: “home”, name: “Home”, to: “/”}

Quite late to the party… but just had the same problem for HeroIcons.

Nice & clean solution it to use apply/3 like:

def icon(assigns) do
  apply(Heroicons, String.to_existing_atom(assigns.name), [assigns])
end

which you can then use in ~H sigil, e.g.

<div>
  <.icon name="document_text" class="w-5 h-5" />
</div>
1 Like

Thank you. I’ll try it as soon as I get home. :blush:

Thing to add - you don’t need to convert strings to atoms (in icon/1) and you could pass the atom directly from the template, e.g <.icon name={:document_text}> but I prefer this way as it’s a simpler for front-end folks to operate on strings.