Nested resources with independent views in Ruby on Rails
This is an archive of blog post I wrote during my first venture (beginning of 8thcolor).
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.
For example, in our own PullReview, Code Reviews and Users are resources with their related routes and actions.
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? %> ## <%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved: <% @comment.errors.full\_messages.each do \|msg\| %> <%= msg %> <% end %> <% end %> <%= f.label :commenter %> <%= f.text\_field :commenter %> <%= f.label :body %> <%= f.text\_area :body %> <%= f.submit %> <% end %>
edit.html.erb
: update link# Editing comment <%= render 'form' %> <%= link\_to 'Show', [@comment.post, @comment] %> \| <%= link\_to 'Back', post\_comments\_path(@comment.post) %>
index.html.erb
: remove:post
member and update link# Listing comments <% @comments.each do \|comment\| %> <% end %> CommenterBodyPost<%= comment.commenter %><%= comment.body %><%= link\_to 'Show', [comment.post, comment] %><%= link\_to 'Edit', edit\_post\_comment\_path(comment.post, comment)%><%= link\_to 'Destroy', [comment.post, comment], :confirm => 'Are you sure?', :method => :delete %> <%= link\_to 'New Comment', new\_post\_comment\_path %>
new.html.erb
: update link# New comment <%= render 'form' %> <%= link\_to 'Back', post\_comments\_path(@comment.post)%>
show.html.erb
: remove :post member and update link<%= notice %> **Commenter:** <%= @comment.commenter %> **Body:** <%= @comment.body %> <%= link\_to 'Edit', edit\_post\_comment\_path(@comment.post, @comment) %> \| <%= 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
:<%= notice %> **Name:** <%= @post.name %> **Title:** <%= @post.title %> **Content:** <%= @post.content %> <%= link\_to 'Comments', post\_comments\_path(@post) %> \| <%= link\_to 'Edit', edit\_post\_path(@post) %> \| <%= link\_to 'Back', posts\_path %>
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ร !
If you have any comment, question, or feedback, please share them with me.