Enumerate a content_tag

Hi, another simple question, but I simply cannot figure this out.
I’d like to enumerate and use a counter to change the id of the tag. Cannot get this to work. I think it’s to do with the return value of Enum.reduce. Not sure Enum.reduce is the right function for this.
Help much appreciated,

  <%= Enum.reduce(@data.things, 1, fn (el, acc) -> %>
    <%= content_tag(:div, class: "panel", aria_hidden: "true", role: "tabpanel", id: "thing#{acc}") do %>
      <%= render MyAppWeb.ThisView, "show_thing.html", conn: @conn, thing: el %>
    <% end %>
  <% end ) %>

Enum.reduce returns the last accumulator. In your example you want the counter as an accumulator but your function passed into Enum.reduce returns a content tag. You want to keep track of both the counter and the result.

You can use Enum.map_reduce instead which keeps track both of the result and the accumulator. It returns a tuple of {result, acc} though so you need to handle this in your template.

As an alternative if you are not scared of doing two iterations over your data you can use Enum.with_index/2 to add an index to your data and then use a simple map function.

<%= Enum.map(Enum.with_index(@data.things, 1), fn ({el, x}) -> %>
  <%= content_tag(..., id: "thing#{x}") do
      <= rendder MyAppWeb.ThisView, "show_thing.html", conn: @conn, thing: el %>
     <% end %>
 <% end ) %>

And here perhaps we can swap out Enum.with_index with Stream.with_index to avoid iterating twice.

I am sure there are more ways :smiley:

I generally prefer for blocks in views, a bit less noisy:

<%= for {el, x} <- Enum.with_index(@data.things, 1) do %>
  <%= content_tag(..., id: "thing#{x}") do
    render MyAppWeb.ThisView, "show_thing.html", conn: @conn, thing: el
  end %>
<% end %>
3 Likes

Martin @cmkarlsson & Jose @josevalim many thanks for the replies. I should have studied the Enum library more closely, but having never used with_index it wouldn’t have been immediately clear to me. Thank-you (it was 01:05 I’d been fixing bugs all day and this was one bug too many!).

Just for other beginners, I’m going to paste some of the code that creates the tabs (rather than the above that renders the panels). To explain, tabs variable is a list of link elements and the code adds a variable number of new links into the list depending on the size of data.things. The reduce returns a tuple the first element of which is the counter and the second the tabs list. I’m going to try and refactor using with_index although it’s not leaping off the page at me just now!

tabs =  if things? == false do
          Enum.reduce(data.things, {0, tabs}, fn(_, acc) ->
            acc = put_elem(acc, 0, elem(acc, 0) + 1)
            i = elem(acc, 0)
            link = [ link( "Thing #{i}",
                          aria_controls: "thing#{i}",
                          to: "#thing#{i}",
                          role: "tab",
                          class: "mdc-tab" )
                  | elem(acc, 1) ]
            put_elem(acc, 1, link)
          end)
          |> elem(1)
        else
          tabs
        end

Generally speaking, avoid the put_elem and elem helpers as much as possible. Only use them when the number of elements is unknown. Pattern matching on the tuple to get the values out would lead to clearer and more performant code.

1 Like

Cool, a little inspiration and I learn something …

{_, tabs} = if !things? do
              Enum.reduce(data.things, {1, tabs}, fn(_, {i, tabs}) ->
                tabs = [ link( "Thing #{i}",
                              aria_controls: "thing#{i}",
                              to: "#thing#{i}",
                              role: "tab",
                              class: "mdc-tab" )
                      | tabs ]
                { i + 1, tabs }
              end)
            else
              tabs
            end

Shorter still, and I’m playing about with layout, and I’m using Enum.any? rather than Enum.empty? to get the value of things? so that I could remove the bang (!). Finishes up being a simple piece of code considering the monstrous mess it started as, ha, ha :smile:

{_, tabs} = if things? do
              Enum.reduce(data.things, {1, tabs}, fn(_, {i, tabs}) ->
                { i + 1,  
                  [ link( "Thing #{i}",
                          aria_controls: "thing#{i}",
                          to: "#thing#{i}",
                          role: "tab",
                          class: "mdc-tab" ) | tabs ]
                }
              end)
            else
              tabs
            end