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.

terminal

rails navigator

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

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

terminal

sudo gem install nifty-generators

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

terminal

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

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.

terminal

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.

ruby

<%= 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.

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.

ruby

classPage < 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.

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.

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.

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:

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.

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.