Nested resources with independent views in Ruby on Rails

Introduction

In REST-style architecture , a resource is simply a source of information, the one you want to expose. The resource is referenced thanks to a global identifier such as an URI. When you deal with information structure such as composition, you’ll use nested resources. The reference of the embedded resource is then built over the reference of the composite resource.

Ruby on Rails allows you to set up nested resources. For instance in the “Getting started” guide where you build a very simple blog, Post and Comment are nested resources. Indeed, it is impossible to consider a lone comment without any post. A Comment belongs to a Post. Resources being referenced by URIs, the setup of nested resources in RoR is done through routing as following:

resources :posts do
 resources :comments
end

But in the example of the guide, a Comment hasn’t got its own view. A Comment is managed through the views of its Post. It’s totaly suited to a blog, but in another case, maybe you would like to program nested resources with independant views, i.e. each resource has their views. This is the goal of this tutorial. I start from the same blog example, but this time I’ll generate scaffolds for both models, Post and Comment.

Same first steps: Post model

First, create the blog application:

rails new blog
cd blog

Generate a scaffolded Post resource:

rails generate scaffold Post name:string title:string content:text

Add some validation to it (app/models/post.rb):

class Post < ActiveRecord::Base
 validates :name,  :presence => true
 validates :title, :presence => true, :length => { :minimum => 5 }
end

Until now, it’s totally the same than in the guide. The next step starts the fork!

Fork: Comment Model

Generate a scaffolded Comment resource:

rails generate scaffold Comment commenter:string body:text post:references

Edit the app/models/post.rb file to add the other side of the association:

class Post < ActiveRecord::Base
  validates :name,  :presence => true
  validates :title, :presence => true, :length => { :minimum => 5 }

  has_many :comments
end

Setup nested resources (config/routes.rb):

resources :posts do
  resources :comments
end

Add some validations to the Comment resource (app/models/comment.rb):

class Comment < ActiveRecord::Base
  validates :commenter, :presence => true
  validates :body, :presence => true

  belongs_to :post
end

Comment controller

Edit the Comment controller app/controllers/comments_controller.rb:

class CommentsController < ApplicationController
  # GET /posts/:post_id/comments
  # GET /posts/:post_id/comments.xml
  def index
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you get all the comments of this post
    @comments = post.comments

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @comments }
    end
  end

  # GET /posts/:post_id/comments/:id
  # GET /comments/:id.xml
  def show
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @comment }
    end
  end

  # GET /posts/:post_id/comments/new
  # GET /posts/:post_id/comments/new.xml
  def new
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you build a new one
    @comment = post.comments.build

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @comment }
    end
  end

  # GET /posts/:post_id/comments/:id/edit
  def edit
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])
  end

  # POST /posts/:post_id/comments
  # POST /posts/:post_id/comments.xml
  def create
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you create the comment with arguments in params[:comment]
    @comment = post.comments.create(params[:comment])

    respond_to do |format|
      if @comment.save
        #1st argument of redirect_to is an array, in order to build the correct route to the nested resource comment
        format.html { redirect_to([@comment.post, @comment], :notice => 'Comment was successfully created.') }
        #the key :location is associated to an array in order to build the correct route to the nested resource comment
        format.xml  { render :xml => @comment, :status => :created, :location => [@comment.post, @comment] }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /posts/:post_id/comments/:id
  # PUT /posts/:post_id/comments/:id.xml
  def update
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])

    respond_to do |format|
      if @comment.update_attributes(params[:comment])
        #1st argument of redirect_to is an array, in order to build the correct route to the nested resource comment
        format.html { redirect_to([@comment.post, @comment], :notice => 'Comment was successfully updated.') }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @comment.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /posts/:post_id/comments/1
  # DELETE /posts/:post_id/comments/1.xml
  def destroy
    #1st you retrieve the post thanks to params[:post_id]
    post = Post.find(params[:post_id])
    #2nd you retrieve the comment thanks to params[:id]
    @comment = post.comments.find(params[:id])
    @comment.destroy

    respond_to do |format|
      #1st argument reference the path /posts/:post_id/comments/
      format.html { redirect_to(post_comments_url) }
      format.xml  { head :ok }
    end
  end
end

The important changes are the following:

  • Retrieve a Comment
        post = Post.find(params[:post_id])
        @comment = post.comments.find(params[:id])
  • Retrieve all Comments
        post = Post.find(params[:post_id])
        @comments = post.comments
  • Building of a new Comment
        post = Post.find(params[:post_id])
        @comment = post.comments.build
  • Creation of a new Comment
        post = Post.find(params[:post_id])
        @comment = post.comments.create(params[:comment])
  • Redirection to the Comment resource
        redirect_to([@comment.post, @comment], :notice => 'Comment was successfully created.')
  • Redirection to the list of Comments
        redirect_to(post_comments_url)

Comment Views

Edit the views app/views/comments/:

  • _form.html.erb: remove the :post member
    <%= form_for([@comment.post, @comment]) do |f| %>
      <% if @comment.errors.any? %>
        <div id="error_explanation">
          <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
    
          <ul>
          <% @comment.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
          <% end %>
          </ul>
        </div>
      <% end %>
    
      <div class="field">
        <%= f.label :commenter %><br />
        <%= f.text_field :commenter %>
      </div>
      <div class="field">
        <%= f.label :body %><br />
        <%= f.text_area :body %>
      </div>
      <div class="actions">
        <%= f.submit %>
      </div>
    <% end %>
  • edit.html.erb: update link
    <h1>Editing comment</h1>
    
    <%= render 'form' %>
    
    <!-- /posts/:post_id/comments/:id -->
    <%= link_to 'Show', [@comment.post, @comment] %> |
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post) %>
  • index.html.erb: remove :post member and update link
    <h1>Listing comments</h1>
    
    <table>
      <tr>
        <th>Commenter</th>
        <th>Body</th>
        <th>Post</th>
        <th></th>
        <th></th>
        <th></th>
      </tr>
    
    <% @comments.each do |comment| %>
      <tr>
        <td><%= comment.commenter %></td>
        <td><%= comment.body %></td>
    
        <!-- /posts/:post_id/comments/:id -->
        <td><%= link_to 'Show', [comment.post, comment] %></td>
        <!-- /posts/:post_id/comments/:id/edit -->
        <td><%= link_to 'Edit', edit_post_comment_path(comment.post, comment)%></td>
        <!-- /posts/:post_id/comments/:id -->
        <td><%= link_to 'Destroy', [comment.post, comment], :confirm => 'Are you sure?', :method => :delete %></td>
      </tr>
    <% end %>
    </table>
    
    <br />
    
    <%= link_to 'New Comment', new_post_comment_path %> <!-- /posts/:post_id/comments/new -->
  • new.html.erb: update link
    <h1>New comment</h1>
    
    <%= render 'form' %>
    
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post)%>
  • show.html.erb: remove :post member and update link
    <p id="notice"><%= notice %></p>
    
    <p>
      <b>Commenter:</b>
      <%= @comment.commenter %>
    </p>
    
    <p>
      <b>Body:</b>
      <%= @comment.body %>
    </p>
    
    <!-- /posts/:post_id/comments/:id/edit -->
    <%= link_to 'Edit', edit_post_comment_path(@comment.post, @comment) %> |
    <!-- /posts/:post_id/comments/ -->
    <%= link_to 'Back', post_comments_path(@comment.post) %>

And finally, add a link to the show view of post app/views/posts/show.html.erb:

<p id="notice"><%= notice %></p>

<p>
  <b>Name:</b>
  <%= @post.name %>
</p>

<p>
  <b>Title:</b>
  <%= @post.title %>
</p>

<p>
  <b>Content:</b>
  <%= @post.content %>
</p>

<!-- /posts/:post_id/comments/ -->
<%= link_to 'Comments', post_comments_path(@post) %> |
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>

As summary, the following table contains the paths used for the Comment resource:

Path of a given Comment "comment"[comment.post, comment]
Path of all Comments of a given Post "post"post_comments_path(post)
Path of edition of a given Comment "comment"edit_post_comment_path(comment.post, comment)

Final commands

Check the routes:

rake routes
    post_comments GET    /posts/:post_id/comments(.:format)          {:controller=>"comments", :action=>"index"}
                  POST   /posts/:post_id/comments(.:format)          {:controller=>"comments", :action=>"create"}
 new_post_comment GET    /posts/:post_id/comments/new(.:format)      {:controller=>"comments", :action=>"new"}
edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format) {:controller=>"comments", :action=>"edit"}
     post_comment GET    /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"show"}
                  PUT    /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"update"}
                  DELETE /posts/:post_id/comments/:id(.:format)      {:controller=>"comments", :action=>"destroy"}
            posts GET    /posts(.:format)                            {:controller=>"posts", :action=>"index"}
                  POST   /posts(.:format)                            {:controller=>"posts", :action=>"create"}
         new_post GET    /posts/new(.:format)                        {:controller=>"posts", :action=>"new"}
        edit_post GET    /posts/:id/edit(.:format)                   {:controller=>"posts", :action=>"edit"}
             post GET    /posts/:id(.:format)                        {:controller=>"posts", :action=>"show"}
                  PUT    /posts/:id(.:format)                        {:controller=>"posts", :action=>"update"}
                  DELETE /posts/:id(.:format)                        {:controller=>"posts", :action=>"destroy"}

Create and migrate the db:

rake db:create
rake db:migrate

Run the server:

rails server

Voilà!





13 thoughts on “Nested resources with independent views in Ruby on Rails

  1. k33

    Can’t you easily refactor your controller by first defining a method for grabbing the post id?

    before_filter :get_post
    ...
    def get_post
    post = Post.find(params[:post_id])
    end

    Reply
    1. toch Post author

      rajeev without more information, I cannot help you. Give me at least the complete error output.

      Have you defined your models before? Have you migrated?

      Reply
  2. Angie

    Thank you so much, this was the best explanation of nested resources. I particularly got a lot out of the constant repeating of what I was trying to achieve, with the # comment out parts of how it related back to the routes. I do realize that it can be refactored as k33 said but it brought me trouble since I did not fully understand what I was trying to do. I hope to find more of your blogs for the next parts of my project. I hope one day a real lightbulb will turn on, but at present one tiny step at a time is great.

    Reply
  3. Pingback: Variable calling in rails viewsQueryBy | QueryBy, ejjuit, query, query by, queryby.com, android doubt, ios question, sql query, sqlite query, nodejsquery, dns query, update query, insert query, kony, mobilesecurity, postquery, queryposts.com, sapquery, jq

  4. Pingback: Variable calling in rails views | Technology & Programming Answers

  5. Ali

    Toch,

    Thank you very much for this article! I’m a front-end dev who’d trying to learn Rails and this article was super helpful for nested resources. I spent A LOT of time looking at other blog posts/articles, but this one hit it on the head! My only suggestion – in your comments_controller.rb it would be easier to declare your post variable as a private variable so it can be re-used throughout the controller, instead of declaring it every time within each method:

    private
    def post
    Post.find(params[:post_id])
    end

    Thanks again!
    Ali

    Reply
    1. toch Post author

      Ali,

      Thanks! Happy to hear it’s still useful. At that time, I struggled a lot to find information about that that’s why I shared it.

      You’re right for the method, it’s just that I wanted to stick close to the Rails guide.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>