homeASCIIcasts

162: Tree Based Navigation 

(view original Railscast)

In this episode we’re going to write a basic content management system that has a hierarchy of pages. We’ll create a new Rails application from scratch and use acts_as_tree to help us define the relationships between the pages. The acts_as_tree plugin has been around for a while, but is still useful if you want to create a tree-like structure within a single model.

Setting Up Our Application

We’ll start by creating a new Rails app, which we’ll call navigator.

rails navigator

That done we’ll navigate to the application’s directory and install the plugin from its Github page.

cd navigator
script/plugin install git://github.com/rails/acts_as_tree.git

Next we’ll create a layout for our site. To generate the layout files we’ll use one of Ryan Bates’s nifty generators. If you don’t have the gem for the generators installed you can install it with

sudo gem install nifty-generators

We can then run the layout generator to create the application’s layout file, stylesheet and a layout helper.

script/generate nifty_layout

Creating The Page Model

Now that our application has a layout and will look a little better we can concentrate on creating the model for the pages. We want to create a resource called Page that can have tree-like behaviour and have sub-pages. A model that uses acts_as_tree needs to have an integer field called parent_id and we’ll also give Page a string field called name and a text field called content. To make creating the Page resource easier we can use another of Ryan’s nifty generators to create a scaffold

script/generate nifty_scaffold page parent_id:integer name:string content:text

We now have a model, a controller, view files and all of the related items we need for our Page resource. To finish setting it up we’ll need to run the database migration that the scaffold created.

rake db:migrate

Among the view files generated by the scaffold will be a form to create or edit a Page. This form will have textboxes for each of the string or integer fields in the Page model, but a page’s parent_id will always be the id of another page, so we’ll replace the textbox for that field with a dropdown list that contains all of the pages.

The relevant section of /app/views/pages/_form.html.erb looks like this.

    <%= f.label :parent_id %><br />
    <%= f.text_field :parent_id %>

The text_field can be replaced by a collection_select. This is generally what you want to use if you’re defining a belongs_to relationship, as we are here, where each Page belongs to another Page.

  <%= f.label :parent_id %><br />
  <%= f.collection_select :parent_id, Page.all(:order => "name"), :id, :name, :include_blank => true %>

The collection_select method takes a number of parameters. We need to pass it the name of the field; the collection of Page models, ordered by date; the fields from Page it should use for the value and text for each item and finally we’ll tell it to include a blank item at the top of the list.

The page form now has a dropdown for the parent.

The form now has a dropdown for the parent page. Obviously as we don’t have any pages yet it will be empty, but as we create pages it will fill up.

And here is the list page after we’ve added some pages. The first three pages have no parent_id so are root nodes, while the other ones are child nodes.

The pages list after adding some pages.

Creating a Menu Structure

We’ll use this data to create a menu system for our application. We want the root notes to appear in a horizontal menu across the top of each page and the child nodes for the page we’re on to appear down the left-hand side. To start we’ll have to update our Page model. Although we’ve defined a parent_id column we haven’t added the functionality to the model to enable a Page to know what its parents and children are. Adding acts_as_tree to the model will do this.

class Page < ActiveRecord::Base
  acts_as_tree
end

The root menu will be the same on every page so we’ll put it in the application’s layout file.

<ul id="menu">
  <% for page in Page.roots %>
    <li><%= link_to h(page.name), page %></li>
  <% end %>
</ul>

In the code above we call the class method roots on Page which is one of the methods that acts_as_tree provides and which will return an array of all of the pages that have no parent_id. We can then loop through this array to create an unordered list of links. Each link will show the page’s name and link to the show action for that page.

A list of bulleted links doesn’t look too pretty so we’ll add some CSS to style the menu.

#menu { list-style: none; margin: 0; padding: 0; float: left; width: 100%; }
#menu li { margin: 0 2px; padding: 0; float: left; }
#menu li a { display: block; padding: 4px 8px; text-decoration: none; border: solid 1px black; color: black; background-color: #AEBBE2; }
#menu li a:hover { color: white; background-color: #4A63B8; }

We now have a menu at the top of each page that will take us to a root page when we click on a link.

All of the page’s fields are shown by default.

Tidying Up The View

By default the show view lists all of the fields for a model, but we only really want to show the content field. We’ll tidy up the view code before we carry on.

<% title @page.name %>
<%= simple_format(@page.content) %>
<p>
  <%= link_to "Edit", edit_page_path(@page) %> |
  <%= link_to "Destroy", @page, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", pages_path %>
</p>

The simple_format method will add basic formatting to the content by turning single line breaks into <br/> elements and wrapping text separated by double line breaks in paragraph tags. Note that we’ve updated the page’s title so that the name is shown rather than the word “Page”.

Writing The Sub Menu

Having tidied the view up a little we can now create the sub menu. Like the main menu it will be rendered as an unordered list.

<ul id="submenu">
  <% for page in @page.children %>
  <li><%= link_to h(page.name), page %></li>
  <% end %>
</ul>

The difference with this menu is that instead of rendering the all of the root pages, it uses the children method to find the immediate children of the current page.

As we’re using the nifty_layout generated code the menu will appear below the page’s title, which we don’t want. We can pass false as a second argument to the title method to stop it adding the title to the layout file and then manually re-add the title below the menu. The show code will now look like this:

<% title @page.name, false %>
<ul id="submenu">
  <% for page in @page.children %>
  <li><%= link_to h(page.name), page %></li>
  <% end %>
</ul>
<h1><%= @page.name %></h1>
<%= simple_format(@page.content) %>
<p>
  <%= link_to "Edit", edit_page_path(@page) %> |
  <%= link_to "Destroy", @page, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", pages_path %>
</p>

To finish the submenu off we’ll add a dash of CSS so that the links appear on the left of the page.

#submenu { float: left; list-style: none; border: solid 1px black; padding: 15px 14px; margin: 0 20px 0 0; }

When we reload the page now we’ll see both menus, with the root pages at the top and the children of the current page on the left.

The sub menu now appears on the left.

It looks like we’re nearly there, but there’s one other thing that needs tidying up. If we visit a page that doesn’t have child pages the empty menu will still appear on the left.

The sub menu still appears when it is empty.

We can fix this by hiding the menu if the page has no children. A small change to the view code will sort this out.

<% unless @page.children.empty? %>
<ul id="submenu">
  <% for page in @page.children %>
  <li><%= link_to h(page.name), page %></li>
  <% end %>
</ul>
<% end %>

Adding a Breadcrumb Trail

Although we can now navigate down through the hierarchy of pages, there’s no way that anyone using our application can see where the page they’re on is relative to other pages or navigate back up the tree, except by going straight back to the root nodes. To fix this we’ll add a breadcrumb trail to each page. Again, this is done by modifying the show action’s view code. We’ll add the breadcrumb trail between the main menu and the page’s title.

<div>
<% for page in @page.ancestors.reverse %>
  <%= link_to h(page.name), page %> >
<% end %>
</div>

To find the path back up to the root node we’re using the ancestors method, which returns an array. The first element is the page’s parent, the second the grandparent and so on until a root page is found. We want our breadcrumb to start at the root so we’ve reversed the array and then looped through it creating a link to each page separated by a greater than sign.

Each page now has a breadcrumb control.

We now have a useful way of navigating up and down the hierarchy of pages.

A More General Use

We’ve shown a fairly specific use for acts_as_tree here, and most Rails apps aren’t content management systems but a more complex set of controllers and actions. This technique can still be used create a menu system for an application, however. We can still create a Page model, but instead of the content field have a url field that contains the path to that page. The links can then use that url field to direct to the correct page in the application.

If we were using this in a production app it would be a good idea to cache the menus. Menu systems are an ideal candidate for fragment caching as once the menu structure has been defined for your application it will rarely change. Fragment caching was covered back in Railscast 90, which is well worth a look for more information.