Recursion: templates and views

Hi, the weekend coder here!
I have a repetitive piece of html that displays nested data. It works just fine. I have no control over the depth of the nest therefore I need to translate this into a recursive function. The trouble is I cannot ‘see it’, as in, I don’t know how to carve the code between template and view. I know this is going to be a simple few lines of code!
The code is making use of detail and summary tags to make the nested structure expandable. When there are no children it just falls back to using divs, so there is a simple ‘if’ to look ahead for children. The expandable_body function sets the html elements and classes. There is a partial which holds the summary tags and includes a case statement that matches the actual data maps.
Any hints gratefully received.

<div class="mdc-layout-grid">
  <div class="mdc-layout-grid__inner">
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-3"></div>
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-9">
      <%= Enum.map(@legislation.body, fn el -> %>
        <% children? = if length(el.children) > 0, do: :true, else: :false %>
        <%= tag(expandable_body(children?).details.element, class: expandable_body(children?).details.class) %>
        <%= render SponglWeb.LegGovUkView, "show_body_partial.html", element: el, children?: children? %>
          <%= if children? do %>
            <%= Enum.map(el.children, fn el2 -> %>
              <% children2? = if length(el2.children) > 0, do: :true, else: :false %>
              <%= tag(expandable_body(children2?).details.element, class: expandable_body(children2?).details.class) %>
                <%= render SponglWeb.LegGovUkView, "show_body_partial.html", element: el2, children?: children2? %>
                <%= if children2? do %>
                  <%= Enum.map(el2.children, fn el3 -> %>
                    <% children3? = if length(el3.children) > 0, do: :true, else: :false %>
                    <%= tag(expandable_body(children3?).details.element, class: expandable_body(children3?).details.class) %>
                      <%= render SponglWeb.LegGovUkView, "show_body_partial.html", element: el3, children?: children3? %>
                      <%= if children3? do %>
                        <%= Enum.map(el3.children, fn el4 -> %>
                          <% children4? = if length(el4.children) > 0, do: :true, else: :false %>
                          <%= tag(expandable_body(children4?).details.element, class: expandable_body(children4?).details.class) %>
                            <%= render SponglWeb.LegGovUkView, "show_body_partial.html", element: el4, children?: children4? %>
                            <%= if children4? do %>
                              <%= Enum.map(el4.children, fn el5 -> %>
                                <%= tag(expandable_body(:false).details.element, class: expandable_body(:false).details.class) %>
                                  <%= render SponglWeb.LegGovUkView, "show_body_partial.html", element: el5, children?: :false %>
                                <%= tag(:"/#{expandable_body(:false).details.element}") %>
                              <% end) %>
                            <% end %>
                          <%= tag(:"/#{expandable_body(children4?).details.element}") %>
                        <% end) %>
                      <% end %>
                    <%= tag(:"/#{expandable_body(children3?).details.element}") %>
                  <% end) %>
                <% end %>
              <%= tag(:"/#{expandable_body(children2?).details.element}") %>
            <% end) %>
          <% end %>
        <%= tag(:"/#{expandable_body(children?).details.element}") %>
      <% end) %>
    </div>
  </div>
</div>
1 Like

What if you don’t use the template at all? There are a couple tips that can help you:

  1. content_tag will help you do the content blocks that you want without having to wrap the starting and closing tag

  2. length(list) has to traverse the whole list, prefer to do list == [] instead if you want to check for emptiness

  3. You don’t need to do if children3? do before the Enum.map. Just call Enum.map. If there are no nodes, then Enum.map will return an empty list, which is equivalent to nothing in your templates

Here is a possible recursive function that you could have in your view. I haven’t tested this in anyway:

def recurse_legislation(nodes) do
  for node <- nodes do
    children? = node.children != []
    expandable = expandable_body(children?)

    content_tag(expandable.details.element, class: expandable.details.class) do
      recurse_legislation(node.children)
    end
  end)
end

And then in your template:

 <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-9">
  <%= recurse_legislation(@legislation.body) %>
</div>
6 Likes

Big thank-you to @josevalim for helping me. My a-ha moment was realising that I could use content_tag in my views as well as my templates! But now I cannot understand how to render the content within the recursively generated <details> tags.

I genuinely thought I’d just be able to nest content_tag, but it’s not so easy. I’ve brought my code into the view from the template partial. Running the following with the |> Phoenix.HTML.Safe.to_iodata |> IO.iodata_to_binary |> IO.puts uncommented I can see all my content in the console, but nothing rendered :frowning:

What’s missing?

  def recurse_legislation(nodes) do

    for node <- nodes do

      children? = node.children != []
      expandable = expandable_body(children?)

      content_tag(expandable.details.element, class: expandable.details.class) do

        [span1, span2] =

          case node do

            %{class: class, paragraph: paragraph, title: title} ->
              [
                content_tag(:span, paragraph, class: [class, " ", expandable.span_1st]),
                content_tag(:span, title, class: [class, " ", expandable.span_2nd])
              ]

            %{class: class, paragraph: paragraph, text: text} ->
              [
                content_tag(:span, paragraph, class: [class, " ", expandable.span_1st]),
                content_tag(:span, class: [class, " ", expandable.span_2nd]) do
                  Enum.map(text, fn elem ->
                    case elem do
                      {:txt, txt} ->
                        txt
                      {:ftn, {id, no}} ->
                        link = link("[#{no}]", to: "#cite_note_#{id}")
                        content_tag(:span, link, id: "cite_ref_#{id}")
                      {:ctn, ctn} ->
                        link("#{ctn.txt}", to: "#{ctn.uri}")
                    end
                  end)
                end
              ]

            %{class: class, number: number, title: title} ->
              [
                content_tag(:span, number, class: [class, " ", expandable.span_1st]),
                content_tag(:span, title, class: [class, " ", expandable.span_2nd])
              ]

            %{class: class, text: text} ->
              [
                content_tag(:span, "", class: [class, " ", expandable.span_empty]),
                content_tag(:span, class: [class, " ", expandable.span_2nd]) do
                  Enum.map(text, fn elem ->
                    case elem do
                      {:txt, txt} ->
                        txt
                      {:ftn, {id, no}} ->
                        link = link("[#{no}]", to: "#cite_note_#{id}")
                        content_tag(:span, link, id: "cite_ref_#{id}")
                      {:ctn, ctn} ->
                        link("#{ctn.txt}", to: "#{ctn.uri}")
                    end
                  end)
                end
              ]
          end

        [content_tag(expandable.summary.element, [span1, span2], class: expandable.summary.class)]
        #|> Phoenix.HTML.Safe.to_iodata |> IO.iodata_to_binary |> IO.puts

        recurse_legislation(node.children)

      end
    end
  end

You need to make sure to return the content_tag result. For example, you cannot do this:

content_tag ... do
end
content_tag ...
recurse_legislation(node.children)

You need to do this:

part1 = content_tag ... do
end
part2 = content_tag ...
part3 = recurse_legislation(node.children)
[part1, part2, part3]

That’s the difference between functions and templates. The templates do this kind of capturing for you. In functions you have to do it by hand.

3 Likes

Newbie mistake! :smile: @josevalim, thank-you for reminding me that the recursive function needed to accumulate the value.

Changing the very last part to the following achieves what I needed:

    element = content_tag(expandable.summary.element, [span1, span2], class: expandable.summary.class)
    #|> Phoenix.HTML.Safe.to_iodata |> IO.iodata_to_binary |> IO.puts
    [element | recurse_legislation(node.children)]

Anyone happening on this post. The function creates a nested <details> <summary> structure with option to change the summary element when there are no children. And wow, I’ve learnt a few more things about views.

Thanks-again!

1 Like

Thank you both for the discussion above - I have a similar use case and this was invaluable. I’m also stuck on newbie error related to the content_tag, any help would be appreciated.

tl;dr:
I have this piece of code below. As you can in the comment, I need to combine the output from the content_tag and the recurse_tree (which returns another content_tag)

defp branch(node, children, page_list) do
  content_tag(:a, node.title)  # <> Print this AND the List below <- NOT WORKING
  recurse_tree(children, page_list)
end

Full spiel
I am attempting to set up this Bulma menu. The code in the tl;dr is essentially this node:

  <ul class="menu-list">
    <li>
      <a>Parent</a> <!-- Print this AND the List below -->
      <ul>
        <li><a>Child</a></li>
      </ul>
    </li>
  </ul>

The data structure for my pages is like so:

Here is what I have in my view (in context here):

  # Called from the template
  def page_tree(page_list) do 
    parent_nodes = page_list |> Enum.filter(& &1.parent_id == 0) # start with the root nodes
    menu = recurse_tree(parent_nodes, page_list) 
    menu
  end

  defp recurse_tree(nodes, page_list) do
    for node <- nodes do
      # for each of the nodes at this level, find the next children down and call this function again
      children = page_list |> Enum.filter(& &1.parent_id == node.id)
      if length(children) > 0 do
        content_tag(:ul) do
          content_tag(:li) do
            branch(node, children, page_list)
          end
        end
      else 
        leaf(node)
      end
    end 
  end

  defp branch(node, children, page_list) do
    content_tag(:a, node.title)  # <> Print this AND the List below <- NOT WORKING
    recurse_tree(children, page_list)
  end
  
  defp leaf(node) do
    content_tag(:li) do
      content_tag(:a, node.title) 
    end
  end

I have most of the code working, but I need to somehow combine the output from the two tags that are generated in branch/3. I’d love any pointers.

(P.S. @josevalim, I see you all over the forums and online - I don’t know how you have enough hours in the day but you’ve already solved a lot of issues that I’ve encountered through your blogging. Thanks for your contributions to the community)

Easiest way, just put them in a list:

defp branch(node, children, page_list) do
  [
    content_tag(:a, node.title),
    recurse_tree(children, page_list),
  ]
end

:slight_smile:

Why: Templates use an ‘enhanced’ IOLists, so it is a basics of a binary, charlist, or a list of binary/charlist/lists. The ‘enhanced’ part is that it also accepts a 2-tuple of {:safe, iolist} that is put ‘raw’ into the thing, unescaped (content_tag and other such things do that for you, you should almost never use that form yourself directly). :slight_smile:

1 Like

Wow, that simple… and it works perfectly, thanks @OvermindDL1!
I noticed the {:safe, _} tuple when I logged the output, but stupidly was trying to combine the 2 tuples into a single tuple. Thanks for the pointer.

Final code for anyone stubmling across this thread:

# In the template:
<%= page_tree(@posts) %>
# In the view:
  def page_tree(page_list) do
    parent_nodes = page_list |> Enum.filter(& &1.parent_id == 0) # start with the root nodes
    recurse_tree(parent_nodes, page_list) 
  end

  defp recurse_tree(nodes, page_list) do
    for node <- nodes do
      children = page_list |> Enum.filter(& &1.parent_id == node.id)
      if length(children) > 0 do
            branch(node, children, page_list)
      else 
        leaf(node)
      end
    end 
  end
  
  defp branch(node, children, page_list) do
    content_tag(:li) do
      [
        content_tag(:a, node.title),  
        content_tag(:ul) do 
          recurse_tree(children, page_list)
        end 
      ]
    end
  end
  
  defp leaf(node) do
    content_tag(:li) do
      content_tag(:a, node.title, href: '') 
    end
  end
2 Likes

Glad to be of help. ^.^

Also, in case anyone comes across this, the iolist that is the second element of the {:safe, iolist} tuple must be a proper IOlist, I.E. it cannot contain any ‘safe’ bits. The phoenix templates and generaters and all handle that for you though if you use them. :slight_smile: