form_for with nested resources

Ruby on-RailsForm ForNested Resources

Ruby on-Rails Problem Overview


I have a two-part question about form_for and nested resources. Let's say I'm writing a blog engine and I want to relate a comment to an article. I've defined a nested resource as follows:

map.resources :articles do |articles|
    articles.resources :comments
end

The comment form is in the show.html.erb view for articles, underneath the article itself, for instance like this:

<%= render :partial => "articles/article" %>
<% form_for([ :article, @comment]) do |f| %>
    <%= f.text_area :text %>
    <%= submit_tag "Submit" %>
<%  end %>

This gives an error, "Called id for nil, which would mistakenly etc." I've also tried

<% form_for @article, @comment do |f| %>

Which renders correctly but relates f.text_area to the article's 'text' field instead of the comment's, and presents the html for the article.text attribute in that text area. So I seem to have this wrong as well. What I want is a form whose 'submit' will call the create action on CommentsController, with an article_id in the params, for instance a post request to /articles/1/comments.

The second part to my question is, what's the best way to create the comment instance to begin with? I'm creating a @comment in the show action of the ArticlesController, so a comment object will be in scope for the form_for helper. Then in the create action of the CommentsController, I create new @comment using the params passed in from the form_for.

Thanks!

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Travis R is correct. (I wish I could upvote ya.) I just got this working myself. With these routes:

resources :articles do
  resources :comments
end

You get paths like:

/articles/42
/articles/42/comments/99

routed to controllers at

app/controllers/articles_controller.rb
app/controllers/comments_controller.rb

just as it says at http://guides.rubyonrails.org/routing.html#nested-resources, with no special namespaces.

But partials and forms become tricky. Note the square brackets:

<%= form_for [@article, @comment] do |f| %>

Most important, if you want a URI, you may need something like this:

article_comment_path(@article, @comment)

Alternatively:

[@article, @comment]

as described at http://edgeguides.rubyonrails.org/routing.html#creating-paths-and-urls-from-objects

For example, inside a collections partial with comment_item supplied for iteration,

<%= link_to "delete", article_comment_path(@article, comment_item),
      :method => :delete, :confirm => "Really?" %>

What jamuraa says may work in the context of Article, but it did not work for me in various other ways.

There is a lot of discussion related to nested resources, e.g. http://weblog.jamisbuck.org/2007/2/5/nesting-resources

Interestingly, I just learned that most people's unit-tests are not actually testing all paths. When people follow jamisbuck's suggestion, they end up with two ways to get at nested resources. Their unit-tests will generally get/post to the simplest:

# POST /comments
post :create, :comment => {:article_id=>42, ...}

In order to test the route that they may prefer, they need to do it this way:

# POST /articles/42/comments
post :create, :article_id => 42, :comment => {...}

I learned this because my unit-tests started failing when I switched from this:

resources :comments
resources :articles do
  resources :comments
end

to this:

resources :comments, :only => [:destroy, :show, :edit, :update]
resources :articles do
  resources :comments, :only => [:create, :index, :new]
end

I guess it's ok to have duplicate routes, and to miss a few unit-tests. (Why test? Because even if the user never sees the duplicates, your forms may refer to them, either implicitly or via named routes.) Still, to minimize needless duplication, I recommend this:

resources :comments
resources :articles do
  resources :comments, :only => [:create, :index, :new]
end

Sorry for the long answer. Not many people are aware of the subtleties, I think.

Solution 2 - Ruby on-Rails

Be sure to have both objects created in controller: @post and @comment for the post, eg:

@post = Post.find params[:post_id]
@comment = Comment.new(:post=>@post)

Then in view:

<%= form_for([@post, @comment]) do |f| %>

Be sure to explicitly define the array in the form_for, not just comma separated like you have above.

Solution 3 - Ruby on-Rails

You don't need to do special things in the form. You just build the comment correctly in the show action:

class ArticlesController < ActionController::Base
  ....
  def show
    @article = Article.find(params[:id])
    @new_comment = @article.comments.build
  end
  ....
end

and then make a form for it in the article view:

<% form_for @new_comment do |f| %>
   <%= f.text_area :text %>
   <%= f.submit "Post Comment" %>
<% end %>

by default, this comment will go to the create action of CommentsController, which you will then probably want to put redirect :back into so you're routed back to the Article page.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDave SimsView Question on Stackoverflow
Solution 1 - Ruby on-Railscdunn2001View Answer on Stackoverflow
Solution 2 - Ruby on-RailsTravis ReederView Answer on Stackoverflow
Solution 3 - Ruby on-RailsjamuraaView Answer on Stackoverflow