Idiomatic way to put a separator when rendering collections in HEEX

Do you have an idiomatic way to add separator when rendering a list in an HEEX template, like so:

<%= for item <- @items do %>
<span><%= item.name %></span>
<%= if not (item == Enum.at(@items, -1)) do %>
<span>, </span>
<% end %>
<% end %>

Or is this version fine?

Not fine. You’re iterating items every iteration.

Yeah, that’s what I thought, but I was wondering if elixir would optimize the Enum call.

You can use Enum.intersperse before you iterate the list or depending on the usecase use css instead.

1 Like

Enum.join might help you. You can Enum.join((for item <- 1..4, do: "<span>#{item}</span>"), ",")

I didn’t realize we could use join like this with template parts.

I usually use css, but for this particular case, I cannot as the separator is more complicated (I used a comma for the example).

After trying different approaches, the most readable solution I could find is this:

<%= for item <- Enum.intersperse(items, :sep) do %>
  <%= if item == :sep do %>
    <span>, </span>
  <% else %>
    <%= item.name %>
  <% end %>
<% end %>
5 Likes

Escapes me why you find that more readable than

iex> items = [%{name: "Joe"}, %{name: "Jose"}, %{name: "Chris"}]
iex> (for item <- items, do: item.name) |> Enum.join(", ")
"Joe, Jose, Chris"

as @cmo suggested…?

Because the ", " is for my example, in practice it is a whole HTML snippet.

1 Like

No idea if this is the correct solution, but your question made me wonder if something like what I’m about to propose works, and it does!

Template:

 Names: <%= render_names(@names) %>

Code:

  def render_names([]), do: ""

  def render_names([head]) do
    assigns = %{head: head}
    ~H[<span><%= @head %></span>]
  end

  def render_names([head | tail]) do
    assigns = %{head: head, tail: tail}

    ~H"""
    <span><%= @head %></span><span>,</span>
    <%= render_names(@tail) %>
    """
  end
1 Like

As far as I understand, you’re better off using function components with assign and assign_new inside of them, rather than building an assigns map yourself. This is to keep change tracking happy.

1 Like

I also feel that it would be better to use a component, but I don’t like the recursion for that. I try to write templates as “content-creator-compatible” as possible.

It would be nice if we had an easy way to determine the last iteration in a loop in eex so we could just match on that in the component.

This is mandatory, see Assigns and HEEx templates — Phoenix LiveView v0.20.2

FWIW, I like your approach and I can’t come up with anything that I personally consider cleaner. :slight_smile:

2 Likes

I’d prefer a component, but for that we’d need sth like jinja’s loop.last.
Or is there a way to (efficiently) know that we are at the last element in a for …?

Don’t know if this is really cleaner, but this is how I solved it a while ago:

<%= for {item, idx} <- Stream.with_index(@items) do %>
<%= if idx != 0 do %>
<span>, </span>
<% end %>
<span><%= item.name %></span>
<% end %>

This also works really well when you have to construct iolists. :slight_smile::+1:

2 Likes

I was pointed here from my own thread Rendering components from a view - #4 by cmo

Enum.intersperse/2 is a great solution

# template.html.heex
<%= for component <- intersperse_widgets(@widgets) %>
  <%= component %>
<% end%>
# my_view.ex
def intersperse_widgets(widgets) do
  widgets
  |> Enum.map(fn widget -> component(&widget_component/1, widget: widget) end)
  |> Enum.intersperse(component(&separator_component/1))
end

defp widget_component(assigns) do
  ~H"""
  <div> <%= @widget.id %> </div>
  """
end

defp separator_component(assigns), do: ~H"<hr>"

Likely breaks change tracking when used in the template though. I’d suggest using it wherever you assign @widgets.

I’m a little lost here. If these two are equal

<MyApp.Weather.city name="KrakĂłw" />
<%= component(&MyApp.Weather.city/1, name: "KrakĂłw") %>

Then why would this behave differently?

def widget_components(widgets) do
  widgets
  |> Enum.map(fn widget -> component(&widget_component/1, widget: widget) end)
end
<%= for component <- widget_components(@widgets) %>
  <%= component %>
<% end%>

Looking at the docs again it might actually work today, but I usually avoid using Enum in templates, given anonymous functions (e.g with Enum.map) making it easy to break change tracking and most often for works just as well without the footguns.

1 Like

In the meantime Phoenix.Component.intersperse/1 function came up: Phoenix.Component — Phoenix LiveView v0.18.18

1 Like