#162 Tree Based Navigation
- Download:
- source codeProject Files in Zip (111 KB)
- mp4Full Size H.264 Video (15.5 MB)
- m4vSmaller H.264 Video (11 MB)
- webmFull Size VP8 Video (27.5 MB)
- ogvFull Size Theora Video (21.3 MB)
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 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.
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.
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.
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.
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.
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.