1

I have the following records in my Rails app:

id: 1
name: 'About'
slug: 'about'
permalink: '/about'
parent_id: null

id: 2
name: 'Team'
slug: 'team'
permalink: '/about/team'
parent_id: 1

id: 3
name: 'Cameron'
slug: 'cameron'
permalink: '/about/team/cameron'
parent_id: 2

And I show them in a list like so:

<ul>
<% @pages.each do |page| %>
  <li>
    <%= page.parent.title rescue '' %><br>
    <%= page.permalink %>
  </li>
<% end %>
</ul>

This creates a list like:

<ul>
    <li>About<br>/about</li>
    <li>Team<br>/about/team</li>
    <li>Cameron<br>/about/team/cameron</li>
</ul>

But I want to create a nested list like the following, which uses the parent_id to group them up.

<ul>
    <li>About<br>/about</li>
    <li>-- Team<br>/about/team</li>
    <li>---- Cameron<br>/about/team/cameron</li>
</ul>

Is there a quick and easy way to group them up in the controller?


The best idea I have come up with is to do this in the controller:

@pages = Page.where(parent_id: nil)

And then this in the view:

<% @pages.each do |page| %>
    <%= render 'page_row', :page => page, :sub => 0 %>
    <% page.pages.each do |page| %>
        <%= render 'page_row', :page => page, :sub => 1 %>
        <% page.pages.each do |page| %>
            <%= render 'page_row', :page => page, :sub => 2 %>
            <% page.pages.each do |page| %>
                <%= render 'page_row', :page => page, :sub => 3 %>
            <% end %>
        <% end %>
    <% end %>
<% end %>

Using a partial to create the nest:

<%
  $i = 0
  $num = sub
  prefix = ''
  while $i < $num do
    prefix += '---'
    $i +=1
  end
%>
<li>
<%= prefix %> <%= page.parent.title rescue '' %><br>
        <%= page.permalink %>
</li>

But by manually looping like I have been doing, it limits the nesting to only 4 levels deep and the code isn't very nice.

How can I achieve the same result but without manually looping to create the nested view of pages?

Cameron
  • 27,963
  • 100
  • 281
  • 483

3 Answers3

3

What I would do is create a reference in the model to the same model, that is, something along the lines of:

has_many :pages, foreign_key: 'parent_id'

Then get all of the top level parents:

@parents = Page.where(parent_id: nil)

Then just load all of the children for each parent in the view:

@parents.each { |p| p.pages.each { |c| ... } }

This kind of structure is also explained in this SO thread.

Community
  • 1
  • 1
David S.
  • 730
  • 1
  • 7
  • 23
  • And this only handles two levels. – Cameron Nov 10 '16 at 16:19
  • You can keep looking up children recursively until you hit the bottom of the tree. – David S. Nov 10 '16 at 16:23
  • How do you do that? Could you show an example of how to keep looping until you reach the bottom. Thanks. – Cameron Nov 10 '16 at 16:33
  • It depends on how you structure your data. Do these categories have more than one child per level, or do they just linearly chain downward with only one per level? That is, do we need to do a breadth first traversal or can we just walk all the way down? – David S. Nov 10 '16 at 16:35
  • 1
    be careful with this approach, recursion through associations, as it will be both slow and open you to a possible race condition. – engineerDave Nov 16 '16 at 21:30
2

I think you're on the right track but instead of looping a fixed number of times the entire view should be recursive.

In the controller: @top_level_pages = Page.where(parent_id: nil)

In the view

<%= render 'pages', pages: @top_level_pages, level: 0 %>

In a pages partial

<% pages.each do |page| %>
  <li>
    <%= '-' * level %><%= page.name %><br>
    <%= page.permalink %>
  </li>
  <%= render 'pages', pages: pages.pages, level: level + 1 %>
<% end %>
Jack Noble
  • 2,018
  • 14
  • 12
0

It seems to me that you should be creating a new class that is responsible for building a tree structure (JSON, or a hash) that encapsulates the data in a way that reduces the logic required in the view layer.

{id: 1,
 name: 'About',
 slug: 'about',
 children: [ 
             {
             id: 2,
             name: 'Team',
             slug: 'team',
             children: [
                         {
                         id: 3
                         name: 'Cameron'
                         slug: 'cameron'
                         ...

In the view I would use a partial to define the presentation for each level -- to generate the next lower level you call the partial again and pass in :children. Let the partial deal with iterating through the elements of that level, and where :children is present, pass them again to the partial.

David Aldridge
  • 51,479
  • 8
  • 68
  • 96