Index of All Lessons

View Screencast (Quicktime)

Right-click on the button above and choose Save As to save file to your computer


Support the Course

There’s no charge for the course, but we greatly appreciate any donations.

Suggested donations:

  • One or two lessons: $5
  • Several lessons $10
  • Entire course: $25 – $50

We hope you’ve found the course to be valuable, and we appreciate any support you care to provide.


Sign Up Now!

If you aren’t already receiving our course lessons via email, sign up now to be sure you don’t miss anything.

Every few days, we’ll send you an email with a link to the next episode, plus a list of additional resources for advancing your knowledge.

There’s no cost and no obligation. And we’ll never share your email address with any third party.


We’ll send you the first lesson right away.


Want to help spread the word? We’d be grateful if you would include a link to the course in your blog, web site, or emails.

Pages and Subpages

12 comments

Goals

In this lesson we’re adding a hierarchy to our pages. Instead of a single pool of pages with a navigation button for each, we want to have subpages as well, which don’t appear in the top navigation bar but are listed in second-level navigation on their parent page.

Please note that, while we’ve tried to make these notes complete, they aren’t the full tutorial; that’s in the screencast, which you can access via the link on the left.

Setup

We begin with the code with which we ended Lesson 14. These zip files contain the beginning and ending states of the code:

Model association

To associate subpages with their parent pages, we could create a subpage model, and then we could write in the page.rb model:

has_many :subpages

And in the subpage.rb model:

belongs_to :page

But to do it this way creates a lot of duplication, since the subpage model would need to behave just like the page model. So we use what’s called a self-referential association: a page object has many page objects, and a page can have (belong to) a parent. This requires a more complex declaration in the page.rb model, as we’ll see, but once that’s done, the self-referential model works the same as it would if subpage was a separate model.

Extending the Page table

We need to add some fields to the Page model. As with any change to the database structure, we create new migration, with whatever name we’d like:

script/generate migration AddSubpages

These three lines in the “up” method create the new fields:

def self.up
  add_column :pages, :parent_id, :integer
  add_column :pages, :navlabel, :string
  add_column :pages, :position, :integer
end

And the corresponding three lines in the “down” method allow us to set the database back to its prior condition, should we want to roll back the database:

def self.down
  remove_column :pages, :parent_id
  remove_column :pages, :navlabel
  remove_column :pages, :position
end

Once we’ve created and saved the migration file, we apply the migration:

rake db:migrate

Defining the association

In the Page class, we need to specify the relationship between parent pages and subpages. We have a field, parent_id, that we created in the previous migration to serve as the foreign key for the association. Since this is a self-referential association, the default naming schemes don’t apply, and we need to explicitly specify the class name and foreign key field name:

has_many :subpages, :class_name => 'Page', :foreign_key => 'parent_id'

The has_many declaration allows us to then write, typically in our controllers, page.subpages, to retrieve all the pages that have the current page as their parent.

Since this is a self-referential association, the “belongs_to” side of the relationship also goes in the page model:

belongs_to :parent, :class_name => 'Page', :foreign_key => 'parent_id'

This declaration allows us to then write page.parent to find the parent page, if there is one.

Writing Custom Finders

Until now, we’ve mostly used standard find methods directly in our controllers. It’s a better design practice, however, to push logic for finding into the model. By writing a custom method to serve as a special kind of find, we can encapsulate more of how the page model works in that single file.

Here’s a method to provide the complete set of navigation tabs:

def self.find_main
  Page.find(:all, :conditions => ['parent_id IS NULL'], :order => 'position')
end

And here’s a variant that only shows tabs for site visitors (i.e., no pages that have their “admin” attribute set):

def self.find_main_public
  Page.find(:all, :conditions => ["parent_id IS NULL and admin != ?", true], :order => 'position')
end

Creating main navigation tabs

Now we need to update the method that tells the layout what tabs to display. We created this method in an earlier lesson and put it in controllers/application.rb, which is “mixed in” to every other controller. Let’s modify get_pages_for_tabs in application_controller to use the new finds:

def get_pages_for_tabs
  if logged_in?
    @tabs = Page.find_main
  else
    @tabs = Page.find_main_public
  end
end

Using the navlabel text

We have a new attribute in the page model for the navigation button text, so let’s change the line of code in views/layouts/application.rhtml.erb from:

<li><%= link_to page.title, view_page_path(page.name) %></li>

to use the new attribute:

<li><%= link_to page.navlabel, view_page_path(page.name) %></li>

Providing access to the new attributes

We need to update the admin forms for the Page scaffold to let us set and modify the new attributes. Instead of changing both new and edit views (in views/pages), we create a form partial that is used for both the new and edit views.

Copy the guts out of either the new or edit view:

    <p>
      <b>Name</b><br />
      <%= f.text_field :name %>
    </p>

    <p>
      <b>Title</b><br />
      <%= f.text_field :title %>
    </p>

    <p>
      <b>Body</b><br />
      <%= f.text_area :body %>
    </p>

    <p>
      <b>Admin?</b><br />
      <%= f.check_box :admin %>
    </p>

Paste this into a new file in views/pages, called “_form.rhtml.erb”. The underscore identifies it as a partial.

Now, in both the new and edit views, all this text can be replaced with:

<%= render :partial => 'form', :locals => {:f => f} %>

To create the HTML pop-up menu for choosing the parent page, we use the collection_select method:

<p>
    <b>Parent Page</b><br />
    <%= f.collection_select :parent_id, Page.find(:all), :id, :title, :include_blank => true %>
</p>

Add a field to specify the position:

<p>
  <b>Position</b><br />
  <%= f.text_field :position, :size => '3' %>
</p>

And a field for the nav label:

<p>
  <b>Nav Label</b><br />
  <%= f.text_field :navlabel %>
</p>

Creating subpages

Now we can use the page admin interface to set the navlabel and position for each of the existing pages.

Then let’s create some subpages. For our example we create two pages, Services and Products, that have About Us as their parent page.

The main navbar should still show only the main pages, labeled and sorted according to our new navlabel and position attributes.

Creating second-level navigation links

To create the second-level menu, we need to find the subpages in the viewer controller’s show method, by adding the following between the two existing lines:

@subpages = @page.subpages

At the top of views/viewer/show, we’ll add a simple list of the any subpages:

<% unless @subpages.empty? %>
    <div id='subnav'>
        <ul>
            <% for page in @subpages %>
                <li><%= link_to page.navlabel, view_page_path(page.name) %></li>
            <% end %>
        </ul>
    </div>
<% end %>

And some minimal styling for this div, to put in the stylesheet:

#subnav {
    width: 200px;
    float: left;
    border-right: 1px solid black;
    margin-right: 20px;
}

The About Us page now shows links for its two subpages, and clicking on those links displays those pages.

Bugfix: there’s been a spurious <div> tag after the navigation links in the application layout, which we’ve deleted in the code for this lesson.

Wrapping up

We now have a usable two-level page structure. Before putting this into real use, we’d want better styling for the level-two navigation links, and some indication of what the parent page is when we’re on a subpage.

In our next lesson, we’ll take are of a few of these lingering details, preparing to move on to the contact form and resources page in later lessons.


Add a Comment

Have a comment or question about this lesson? Add it here.






Comments on This Lesson

From: Nicholas       Date: 07/30/08 05:05 AM

Subject: Required a tutrorial on Receiving email

Required a tutrorial on Receiving email , something similar to your add a comment.

looking forward to read it. hope you’ll add it soon. thank you -Nicholas I

From: Walter       Date: 07/18/08 04:04 AM

Subject: Tracking down a bug in Parent.id in pages#index.html.erb

I’m getting the Parent_id off by one (+1) even though I have @pages[page.parent_id.to_i – 1].name in the parent column of pages#index.html.erb. I had to rollback my db since it developed some error when a page was set to a blank index then deleted. Should it fail it will fail the entire index and the db then has to be reset. Oddly if I go to Edit the page, the correct parent page is selected and is not offset. How is the parent_id.name used in the drop down list differ from the view? Maybe I’m getting a conflict? As always thanks for terrific screencasts.

From: Erwin Odendaal       Date: 07/06/08 06:06 AM

Subject: different stylesheet

Easier than expected… Added: <% if @page.parent %> <%= stylesheet_link_tag ‘sub’ %> <% end %> to the head-section of application.html.erb

From: Erwin Odendaal       Date: 07/02/08 08:08 AM

Subject: different stylesheets

I want to link another stylesheet to the subpages. So when a page has a parent it needs to call the style sub.css (for example). But I just can’t figure it out. :-( I presume you need to check if there’s no parent_id?

From: Michael Slater       Date: 06/09/08 09:09 AM

Subject: collection select syntax

Alexei, yes, it’s a small bit of rails magic. Whenever you use a form_for block, you get the form context from the block variable, f. Since we wrote f.collection_select, we don’t specify the page object; it is implicitly specified through the block variable.

From: Alexei       Date: 06/09/08 01:01 AM

Subject: collection_select

I looked down at different wikis and official API for rails and seems like the syntax for collection_select is collection_select(:page, :parent_id, Page.find(:all), :id, :title, :include_blank => ‘true’) Does your variant (without first :page) uses some rails magic?

From: Erik       Date: 05/29/08 01:13 PM

Subject: Menus

Loved the lesson as always. One other thing you could add as a ‘to-do’ is to somehow generate drop-down menus for the subpages. This is a frequently used feature in many sites and would be a helpful addition to a rails site as well. I would imagine using javascript to invoke a mouseover event that would display a bit of html that would show the buttons below the parent tabs.

From: Tim Morgan       Date: 05/21/08 02:14 PM

Subject: Lesson feedback

Hi guys

Thanks again for another great installment.

My comments.

===

Last time we did a migration, we were careful to initialize the new fields. Accordingly, I added the following to my ‘up’ method.

    Page.find(:all).each do |page|
      page.update_attributes :position =&gt; page.id, :navlabel =&gt; page.title
    end

===

It would seem that self-refering classes would be a pattern. Is there a macro that would generate the following…

  has_many :subpages, :class_name =&gt; self.name, :foreign_key =&gt; 'parent_id'
  belongs_to :parent, :class_name =&gt; self.name, :foreign_key =&gt; 'parent_id'

from, say
  self_reference :subpages

===

I refactored ‘get_pages_for_tabs’ / ‘find_main’ as follows:

//application.rb
  def get_pages_for_tabs
    @tabs = Page.find_main logged_in?
  end
// page.rb
  def self.find_main( logged_in )
    if logged_in
      Page.find(:all, 
        :conditions =&gt; ['parent_id IS NULL'], :order =&gt; 'position')
    else
      Page.find(:all, 
        :conditions =&gt; ["parent_id IS NULL and admin != ?", true], :order =&gt; 'position')
    end
  end

===

Navbar is growing into an application level helper. I also refactored the ‘edit’ button into the navabar, so I can find it in a consistant location. Remove it from viewer/show.html.erb altogether and

// application.html.erb
&lt;%- if logged_in? -%&gt;
                    &lt;li&gt;&lt;a href='#' id='edit'&gt;[Edit]&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;%= link_to "Log Out", logout_path %&gt;&lt;/li&gt;
&lt;%- else -%&gt;
                    &lt;li&gt;&lt;%= link_to "Log In", login_path %&gt;&lt;/li&gt;
&lt;%- end -%&gt;

But I still get the tooltip ‘edit’ popping up from the in_place_editing plugin. Is there an option to shut this off inside the Javascript?

===

Here’s a question for restful development. In following the development of the database, we are filling it with some testing records. I was wondering: Is it possible to get a data dump from the database, to fill a SQL script to re-initialize? Then I remembered restfull. Using the url ‘pages.xml’ or ‘users.xml’, we get an xml dump of the data. ‘pages.json’ will send formated as json

Is there a simple SQL formatter for this?

===

Again, really enjoying the series! Great work, guys

From: Michael Slater       Date: 05/20/08 08:08 AM

Subject: HTML error corrected

Scott, thanks for pointing out the error in the HTML layout. I’ve corrected this in the zip file. As for the Akita technique, this works but it isn’t clear to me that it is much of an improvement.

From: Scott Gardner       Date: 05/19/08 01:13 PM

Subject: Correction: Another version of the _form partial

The button name local variable is a hash, so the = should be => new.html.erb :locals => {:button_name => “Create”} edit.html.erb :locals => {:button_name => “Update”}

From: Scott Gardner       Date: 05/19/08 12:12 PM

Subject: Another version of the _form partial

I got the idea from the Akita On Rails tutorial to move the entire form to a partial, and then assign the button name to a local variable, e.g.:

_form.html.erb <% form_for(@page) do |f| %>

  1. ... <% end %>
<%= render :partial => ‘form’, :locals => {:button_name = “Create”} %>

<%= render :partial => ‘form’, :locals => {:button_name = “Update”} %>

edit.html.erb

From: Scott Gardner       Date: 05/19/08 12:12 PM

Subject: Mismatched divs in source code download > application.html.erb

You are missing a closing div in this file. Starting from the footer id: # ...

Learning Rails Sample Application

 

Hosting Provided By

EngineYard.com fully managed Rails hosting

Sponsored By

New Relic Rails Performance Monitoring

FiveRuns Tuneup

Peepcode Screencasts