kuon
December 2, 2021, 9:02pm
1
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?
cmo
December 2, 2021, 9:33pm
2
Not fine. You’re iterating items every iteration.
kuon
December 2, 2021, 9:47pm
3
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
cmo
December 2, 2021, 9:54pm
5
Enum.join
might help you. You can Enum.join((for item <- 1..4, do: "<span>#{item}</span>"), ",")
kuon
December 2, 2021, 10:02pm
6
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).
kuon
December 2, 2021, 10:15pm
7
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
Sebb
December 2, 2021, 10:37pm
8
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…?
kuon
December 2, 2021, 10:48pm
9
Because the ", " is for my example, in practice it is a whole HTML snippet.
1 Like
fceruti
December 2, 2021, 11:39pm
10
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
cmo
December 3, 2021, 12:12am
11
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
Sebb
December 3, 2021, 6:40am
12
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.
2 Likes
Sebb
December 3, 2021, 12:05pm
14
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
…?
aziz
December 3, 2021, 12:44pm
15
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.
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
MMore
April 25, 2023, 2:00am
20
In the meantime Phoenix.Component.intersperse/1
function came up: Phoenix.Component — Phoenix LiveView v0.18.18
1 Like