form_for com recursos aninhados

125

Eu tenho uma pergunta em duas partes sobre form_for e recursos aninhados. Digamos que estou escrevendo um mecanismo de blog e quero relacionar um comentário a um artigo. Eu defini um recurso aninhado da seguinte maneira:

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

O formulário de comentários está na exibição show.html.erb dos artigos, abaixo do próprio artigo, por exemplo:

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

Isso gera um erro, "ID chamado para zero, o que seria errado etc." Eu também tentei

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

Que processa corretamente, mas relaciona f.text_area ao campo 'text' do artigo, em vez do comentário, e apresenta o html para o atributo article.text nessa área de texto. Então, parece que eu também entendi errado. O que eu quero é um formulário cujo 'submit' chame a ação create no CommentsController, com um article_id nos parâmetros, por exemplo, uma solicitação de postagem para / articles / 1 / comments.

A segunda parte da minha pergunta é: qual é a melhor maneira de criar a instância do comentário? Estou criando um comentário @ na ação show do ArticlesController, para que um objeto de comentário esteja no escopo do auxiliar form_for. Em seguida, na ação create do CommentsController, crio um novo @comment usando os parâmetros fornecidos no form_for.

Obrigado!

Dave Sims
fonte

Respostas:

228

Travis R está correto. (Eu gostaria de poder votar novamente.) Acabei de conseguir isso. Com estas rotas:

resources :articles do
  resources :comments
end

Você obtém caminhos como:

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

roteado para controladores em

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

assim como em http://guides.rubyonrails.org/routing.html#nested-resources , sem espaços para nome especiais.

Mas parciais e formas se tornam complicadas. Observe os colchetes:

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

Mais importante, se você deseja um URI, pode precisar de algo assim:

article_comment_path(@article, @comment)

Alternativamente:

[@article, @comment]

conforme descrito em http://edgeguides.rubyonrails.org/routing.html#creating-paths-and-urls-from-objects

Por exemplo, dentro de uma coleção parcial com comment_itemfornecido para iteração,

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

O que o jamuraa diz pode funcionar no contexto do artigo, mas não funcionou para mim de várias outras maneiras.

Há muita discussão relacionada a recursos aninhados, por exemplo, http://weblog.jamisbuck.org/2007/2/5/nesting-resources

Curiosamente, eu acabei de aprender que os testes unitários da maioria das pessoas não estão realmente testando todos os caminhos. Quando as pessoas seguem a sugestão do jamisbuck, acabam com duas maneiras de obter recursos aninhados. Seus testes de unidade geralmente obtêm / postam da maneira mais simples:

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

Para testar a rota que eles preferem, eles precisam fazer o seguinte:

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

Aprendi isso porque meus testes de unidade começaram a falhar quando eu mudei disso:

resources :comments
resources :articles do
  resources :comments
end

para isso:

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

Acho que não há problema em ter rotas duplicadas e perder alguns testes de unidade. (Por que testar? Como, mesmo que o usuário nunca veja as duplicatas, seus formulários podem se referir a eles, implicitamente ou por meio de rotas nomeadas.) Ainda assim, para minimizar a duplicação desnecessária, recomendo:

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

Desculpe pela resposta longa. Muitas pessoas não estão cientes das sutilezas, eu acho.

cdunn2001
fonte
É trabalho, mas eu tive que modificar o controlador como jamuraa disse.
Marcus Becker
O jeito do Jam funciona, mas você pode acabar com rotas extras que provavelmente não conhece. É melhor ser explícito.
cdunn2001
Eu tinha recursos aninhados, @result dentro @course. Embora, [@result, @course]trabalhou, mas form_for(@result, url: { action: "create" }) também funciona. Isso precisa apenas do último nome do modelo e do nome do método.
Anwar
@ cdunn2001 Por favor, você pode explicar por que precisamos mencionar "@article" aqui assim e o que isso significa? o que a sintaxe abaixo faz? : <% = form_for [@ artigo, @ comentário] do | f | %>
Arpit Agarwal
1
Travis / @ cdunn2001 acertou. Não defina o pai e o recurso quando estiver usando rotas aninhadas sem duplicatas; caso contrário, ele pensará que todas as ações estão aninhadas. Da mesma forma, se você aninhou tudo, sempre configure AT.parent. Além disso, se você tiver um formulário comum com um botão Cancelar com rotas parcialmente aninhadas, use um caminho como o seguinte para que funcione conforme o que você definiu (observe a pluralização de filho): <% = link_to 'Cancel', parent_children_path (AT.parent || AT.child.parent)%>
iheggie
54

Certifique-se de ter os dois objetos criados no controlador: @poste @commentpara a postagem, por exemplo:

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

Então, em vista:

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

Certifique-se de definir explicitamente a matriz no form_for, não apenas com vírgula, como você tem acima.

Travis Reeder
fonte
A resposta de Travis é um pouco antiga, mas acredito que seja a mais correta para o Rails 3.2.X. Se você deseja que todos os elementos do construtor de formulários preencham os campos Comentário, basta usar uma matriz, não são necessários auxiliares de URL.
19413 Karl
1
Defina apenas o objeto pai onde a ação está aninhada. Se você apenas parcialmente aninhada o recurso (por exemplo, como por exemplo), em seguida, definindo o objeto pai fará com que form_for a falhar (reconfirmado com trilhos 5.1 agora)
iheggie
35

Você não precisa fazer coisas especiais no formulário. Você acabou de criar o comentário corretamente na ação show:

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

e crie um formulário para ele na exibição do artigo:

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

por padrão, esse comentário irá para a createação de CommentsController, na qual você provavelmente desejará redirect :backinserir, para que seja roteado de volta à Articlepágina.

jamuraa
fonte
10
Eu tive que usar o form_for([@article, @new_comment])formato. Eu acho que é porque eu estou mostrando a visão comments#new, não article#new_comment. Eu acho que no article#new_commentRails é inteligente o suficiente para descobrir o que o objeto de comentário está aninhado e, portanto, você não precisa especificá-lo?
Sopa