form_for with nested resources
Asked Answered
S

3

136

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!

Stoll answered 9/1, 2010 at 19:50 Comment(0)
M
241

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.

Mora answered 6/1, 2011 at 5:8 Comment(6)
It is Work but, I had to modify the controller like jamuraa said.Flowing
Jam's way works, but you can end up with extra routes that you probably don't know about. It's better to be explicit.Mora
I had nested resources, @result inside @course. Though, [@result, @course] worked, but form_for(@result, url: { action: "create" }) also works. This only needs the last model name and the method name.Butacaine
@Mora Please can you explain why we have to mention "@article" here like this and what this means? what does the below syntax do? : <%= form_for [@article, @comment] do |f| %>Skyler
Travis / @Mora got it right. Don't set both the parent and the resource when you are using nested routes without duplicates, otherwise it will think all the actions are nested. Likewise if you nested everything, then always set AT.parent. Also if you have a common form with a cancel button with partially nested routes then use a path like the following so it works whichever you have set (Note the pluralization of child): <%= link_to 'Cancel', parent_children_path(AT.parent|| AT.child.parent) %>Warrington
To give some feedback: This answer does not read very well, as it contains multiple instances of "what X said". This is the accepted answer and it requires me to scroll down every single time and understand the referenced answer first (and find it). Or I have to try to guess from the context...Sadism
M
58

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.

Montelongo answered 27/12, 2010 at 7:35 Comment(2)
Travis's is a bit of an old answer, but I believe it to be the most correct for Rails 3.2.X. If you want all elements of the form builder to populate the Comment fields, just use an array, url helpers are not required.Lindeberg
Only set the parent object where the action is nested. If you only partially nested the resource (eg as per example), then setting the parent object will cause form_for to fail (reconfirmed with rails 5.1 just now)Warrington
S
35

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.

Skepticism answered 10/1, 2010 at 0:8 Comment(1)
I had to use the form_for([@article, @new_comment]) format. I think this is because I'm showing the view for comments#new, not article#new_comment. I figure in article#new_comment Rails is smart enough to work out what the comment object is nested in and such you don't have to specify it?Rhea

© 2022 - 2024 — McMap. All rights reserved.