15: Pages and Subpages




Learning Rails show

Summary: <h2>Goals</h2> <p>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.</p> <p>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.</p> <h2>Setup</h2> <p>We begin with the code with which we ended Lesson 14. These zip files contain the beginning and ending states of the code:</p> <ul> <li><a href="/learningrails_14.zip">Learning Rails example app code as of the end of Lesson 14</a></li> <li><a href="/learningrails_15.zip">Learning Rails example app code as of the end of Lesson 15</a></li> </ul> <h2>Model association</h2> <p>To associate subpages with their parent pages, we could create a subpage model, and then we could write in the page.rb model:</p> <pre> has_many :subpages</pre> <p>And in the subpage.rb model:</p> <pre> belongs_to :page</pre> <p>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.</p> <h2>Extending the Page table</h2> <p>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:</p> <pre> script/generate migration AddSubpages</pre> <p>These three lines in the “up” method create the new fields:</p> <pre> def self.up add_column :pages, :parent_id, :integer add_column :pages, :navlabel, :string add_column :pages, :position, :integer end </pre> <p>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:</p> <pre> def self.down remove_column :pages, :parent_id remove_column :pages, :navlabel remove_column :pages, :position end </pre> <p>Once we’ve created and saved the migration file, we apply the migration:</p> <pre> rake db:migrate</pre> <h2>Defining the association</h2> <p>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:</p> <pre> has_many :subpages, :class_name =&gt; 'Page', :foreign_key =&gt; 'parent_id'</pre> <p>The has_many declaration allows us to then write, typically in our controllers, <code>page.subpages</code>, to retrieve all the pages that have the current page as their parent.</p> <p>Since this is a self-referential association, the “belongs_to” side of the relationship also goes in the page model:</p> <pre> belongs_to :parent, :class_name =&gt; 'Page', :foreign_key =&gt; 'parent_id'</pre> <p>This declaration allows us to then write <code>page.parent</code> to find the parent page, if there is one.</p> <h2>Writing Custom Finders</h2> <p>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.</p> <p>Here’s a method to provide the complete set of navigation tabs:</p> <pre> def self.find_main Page.find(:all, :conditions =&gt; ['parent_id IS NULL'], :order =&gt; 'position') end </pre> <p>And here’s a variant that only shows tabs for site visitors (i.e., no pages that have their “admin” attribute set):</p> <pre> def self.find_main_public Page.find(:all, :conditions =&gt; ["parent_id IS NULL and admin != ?", true], :order =&gt; 'position') end </pre> <h2>Creating main navigation tabs</h2> <p>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:</p> <pre> def get_pages_for_tabs if logged_in? @tabs = Page.find_main else @tabs = Page.find_main_public end end </pre> <h2>Using the navlabel text</h2> <p>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:</p> <pre> &lt;li&gt;&lt;%= link_to page.title, view_page_path(page.name) %&gt;&lt;/li&gt;</pre> <p>to use the new attribute:</p> <pre> &lt;li&gt;&lt;%= link_to page.navlabel, view_page_path(page.name) %&gt;&lt;/li&gt;</pre> <h2>Providing access to the new attributes</h2> <p>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.</p> <p>Copy the guts out of either the new or edit view:</p> <pre> &lt;p&gt; &lt;b&gt;Name&lt;/b&gt;&lt;br /&gt; &lt;%= f.text_field :name %&gt; &lt;/p&gt; &lt;p&gt; &lt;b&gt;Title&lt;/b&gt;&lt;br /&gt; &lt;%= f.text_field :title %&gt; &lt;/p&gt; &lt;p&gt; &lt;b&gt;Body&lt;/b&gt;&lt;br /&gt; &lt;%= f.text_area :body %&gt; &lt;/p&gt; &lt;p&gt; &lt;b&gt;Admin?&lt;/b&gt;&lt;br /&gt; &lt;%= f.check_box :admin %&gt; &lt;/p&gt; </pre> <p>Paste this into a new file in views/pages, called “_form.rhtml.erb”. The underscore identifies it as a partial.</p> <p>Now, in both the new and edit views, all this text can be replaced with:</p> <pre> &lt;%= render :partial =&gt; 'form', :locals =&gt; {:f =&gt; f} %&gt;</pre> <p>To create the <span class="caps">HTML</span> pop-up menu for choosing the parent page, we use the collection_select method:</p> <pre> &lt;p&gt; &lt;b&gt;Parent Page&lt;/b&gt;&lt;br /&gt; &lt;%= f.collection_select :parent_id, Page.find(:all), :id, :title, :include_blank =&gt; true %&gt; &lt;/p&gt; </pre> <p>Add a field to specify the position:</p> <pre> &lt;p&gt; &lt;b&gt;Position&lt;/b&gt;&lt;br /&gt; &lt;%= f.text_field :position, :size =&gt; '3' %&gt; &lt;/p&gt; </pre> <p>And a field for the nav label:</p> <pre> &lt;p&gt; &lt;b&gt;Nav Label&lt;/b&gt;&lt;br /&gt; &lt;%= f.text_field :navlabel %&gt; &lt;/p&gt; </pre> <h2>Creating subpages</h2> <p>Now we can use the page admin interface to set the navlabel and position for each of the existing pages.</p> <p>Then let’s create some subpages. For our example we create two pages, Services and Products, that have About Us as their parent page.</p> <p>The main navbar should still show only the main pages, labeled and sorted according to our new navlabel and position attributes.</p> <h2>Creating second-level navigation links</h2> <p>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:</p> <pre> @subpages = @page.subpages</pre> <p>At the top of views/viewer/show, we’ll add a simple list of the any subpages:</p> <pre> &lt;% unless @subpages.empty? %&gt; &lt;div id='subnav'&gt; &lt;ul&gt; &lt;% for page in @subpages %&gt; &lt;li&gt;&lt;%= link_to page.navlabel, view_page_path(page.name) %&gt;&lt;/li&gt; &lt;% end %&gt; &lt;/ul&gt; &lt;/div&gt; &lt;% end %&gt; </pre> <p>And some minimal styling for this div, to put in the stylesheet:</p> <pre> #subnav { width: 200px; float: left; border-right: 1px solid black; margin-right: 20px; } </pre> <p>The About Us page now shows links for its two subpages, and clicking on those links displays those pages.</p> <p>Bugfix: there’s been a spurious <code>&lt;div&gt;</code> tag after the navigation links in the application layout, which we’ve deleted in the code for this lesson.</p> <h2>Wrapping up</h2> <p>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.</p> <p>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.</p>