Uma nova funcionalidade adicionada ao Rails 2.3 é a possibilidade de atualizar os atributos em modelos aninhados diretamente.
Para mostrar essa funcionalidade vamos trabalhar numa aplicação de gerenciamento de contatos, permitindo cadastrar pessoas, contatos e cidades. A estrutura é bastante simples: uma pessoa possui muitos contatos e pertence a uma cidade. O código foi baseado no exemplo de gerenciamento de formulários complexos criado pelo Eloy Duran.
Durante o desenvolvimento da aplicação vou criar também alguns testes, contudo eles não serão disponibilizados aqui para não estender demais o post. Quem tiver interesse poderá conferir o código fonte no github. Já aviso a todos que não sou um dos maiores peritos em testes, estou aprendendo, portanto qualquer dica sempre será bem-vinda =).
Bom, vamos lá! Utilizando o terminal, navegue até o diretório onde deseja criar a aplicação e execute os comandos:
rails nested_attributes cd nested_attributes
Já sabemos que vamos precisar de um modelo pessoa, cidade e contato, então vamos criá-los utilizando os generators do Rails:
script/generate model City name:string script/generate scaffold Person name:string city:references script/generate model Contact kind:string description:string person:references
Olhos mais atentos devem ter percebido que o modelo de pessoa foi gerado com scaffold: apenas para facilitar a nossa vida já criando o controller e as views necessárias para a manutenção.
Agora vamos criar e migrar nossa base de dados:
rake db:create rake db:migrate
Vamos trabalhar inicialmente nos modelos, criando as validações e relacionamentos. Primeiro o modelo de cidades (app/models/city.rb
) deve ter muitas pessoas, e deve validar a presença do nome.
class City < ActiveRecord::Base has_many :people validates_presence_of :name end [/sourcecode] Continuando, nosso modelo de pessoas (<code>app/models/person.rb</code>) deve requerer o nome, possuir muitos contatos e pertencer a uma cidade. class Person < ActiveRecord::Base belongs_to :city has_many :contacts validates_presence_of :name end [/sourcecode] E por fim temos o modelo de contatos (<code>app/models/contact.rb</code>), que além de pertencer a uma pessoa deve validar a presença do tipo e da descrição. class Contact < ActiveRecord::Base KINDS = %w(home_phone work_phone email url) belongs_to :person validates_presence_of :kind, :description validates_inclusion_of :kind, :in => KINDS end
Também criamos aqui uma constante para facilitar o trabalho de gerenciar os tipos de contatos possíveis: telefone residencial, telefone comercial, email e url, respectivamente. Se você desejar mais algum tipo de contato é só adicionar nesta lista.
Neste ponto a estrutura de nossos modelos está pronta. Todas as validações e relacionamentos necessários foram criados. Podemos então partir para o que realmente nos interessa: atributos aninhados.
Atributos Aninhados: Os Modelos
Para que um modelo possa criar e salvar dados de modelos relacionados, devemos dar esta permissão a ele explicitamente através do comando accepts_nested_attributes_for
. Este comando permite dois parâmetros adicionais: allow_destroy
, que quando verdadeiro permitirá a exclusão de registros aninhados através de uma flag :_delete
(veja mais abaixo); e reject_if
, uma proc que deve validar se um registro aninhado será ou não criado. Vamos fazer isto no nosso modelo de pessoas, adicionando as seguintes linhas:
accepts_nested_attributes_for :contacts, :allow_destroy => true, :reject_if => proc { |contact| contact['description'].blank? } accepts_nested_attributes_for :city, :reject_if => proc { |city| city['name'].blank? }
Para o modelo de contatos estamos permitindo excluir registros, e também ignoramos registros sem o campo descrição. Para a cidade a opção :allow_destroy
não foi informada, pois não pretendemos permitir a exclusão de uma cidade a partir da pessoa. Também ignoramos a cidade se o nome estiver em branco.
Para fazer alguns testes com essas novas opções abra um console no terminal (script/console
) e vá digitando os comandos abaixo e analisando os resultados:
# City person = Person.new(:name => 'Carlos') #<Person id: nil, name: "Carlos", city_id: nil, created_at: nil, updated_at: nil> person.city_attributes = { :name => 'Rio do Sul' } #{:name=>"Rio do Sul"} person.city #<City id: nil, name: "Rio do Sul", created_at: nil, updated_at: nil> person.city.new_record? #true person.save #true person.city.new_record? #false # Contacts person.contacts_attributes = { 'new_1' => { :kind => 'email', :description => 'teste@example.com' }} #{"new_1"=>{:kind=>"email", :description=>"teste@example.com"}} person.contacts_attributes = { 'new_2' => { :kind => 'email', :description => 'teste2@example.com' }} #{"new_2"=>{:kind=>"email", :description=>"teste2@example.com"}} person.contacts.size #2 person.contacts #[#<Contact id: nil, kind: "email", description: "teste@example.com", person_id: nil, created_at: nil, updated_at: nil>, #<Contact id: nil, kind: "email", description: "teste2@example.com", person_id: nil, created_at: nil, updated_at: nil>] person.save #true person.contacts #[#<Contact id: 5, kind: "email", description: "teste@example.com", person_id: 6, # created_at: "2009-03-28 00:36:49", updated_at: "2009-03-28 00:36:49">, # <Contact id: 6, kind: "email", description: "teste2@example.com", person_id: 6, # created_at: "2009-03-28 00:36:49", updated_at: "2009-03-28 00:36:49">] # Deleting a contact contact_id = person.contacts.first.id #5 person.contacts_attributes = { contact_id.to_s => { :id => contact_id.to_s, :_delete => true } } #{"5"=>{:id=>"5", :_delete=>true}} person.contacts.first.marked_for_destruction? #true person.save #true person.reload.contacts.size #1 person.contacts #[#<Contact id: 6, kind: "email", description: "teste2@example.com", person_id: 6, # created_at: "2009-03-28 00:36:49", updated_at: "2009-03-28 00:36:49">]
Se desejar ver mais exemplos de utilização das estruturas aninhadas dê uma olhada nos testes do projeto no github.
Apenas ativar a opção accepts_nested_attributes_for
no modelo nos dá uma série de funcionalidades “de graça”: o Rails agora irá salvar automaticamente os registros atribuídos através desta opção. Além disso, o modelo verifica se existe erros nos modelos filhos e carrega quaisquer mensagens de erros para ele, também para facilitar a visualização de forma amigável na view. E o melhor é que tudo acontece dentro de uma transação única, o que significa que se algum dos registros gerar um erro no momento de salvar ou de algum callback, toda a transação será cancelada.
Atributos Aninhados: As Views
Toda essa funcionalidade que o Active Record disponibiliza não seria de tanta ajuda se não tivéssemos facilitadores para gerenciar isso nos formulários. Para isso ganhamos novas funcionalidades dentro de views: tudo acontece utilizando o método fields_for
, de uma maneira um pouquinho diferente de como já o utilizávamos.
A primeira modificação que precisamos fazer é criar um partial
com o formulário para inclusão/edição de pessoas. Como geramos esse modelo com o scaffold, a estrutura html inicial já está pronta, então crie um partial
com o nome _form.html.erb
dentro de app/views/people/
com o código abaixo:
<% form_for(@person) do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.submit 'Submit' %> </p> <% end %>
Percebam que removi a parte gerada para o campo city, pois trabalharemos nele mais tarde. Outra mudança apenas estética: o submit ganhou um caption mais “genérico”, ao invés de “Create” ou “Update” como é gerado nos templates new e edit.
Devemos agora alterar estes últimos dois para gerar nosso partial
:
# new.html.erb <h1>New person</h1> <%= render :partial => 'form' %> <%= link_to 'Back', people_path %> # edit.html.erb <h1>Editing person</h1> <%= render :partial => 'form' %> <%= link_to 'Show', @person %> | <%= link_to 'Back', people_path %>
Vamos rodar nossa aplicação para vermos como está ficando. Primeiro remova o arquivo index.html
que está dentro da pasta public
. Depois, abra o arquivo config/routes.rb
e modifique-o conforme abaixo:
ActionController::Routing::Routes.draw do |map| map.resources :people map.root :people end
Isto configura o root de nossa aplicação para a listagem de pessoas. Agora vá para o terminal e rode o servidor (script/server
), depois acesse no navegador http://localhost:3000
e deverá ver a listagem com as pessoas que cadastrou anteriormente. Acesse a tela de edição e de inclusão de nova pessoa para verificar se tudo correu bem com nosso partial
.
Pronto. Com tudo funcionando podemos nos concentrar no gerenciamento de contatos para a pessoa no mesmo formulário.
Contatos
A criação de um formulário aninhado utiliza o método fields_for
, sendo chamado a partir da instância do formulário que está sendo criado, ou formulário “pai”, e identificando qual o modelo aninhado que será gerado. Acho que ficou um pouco complicado de entender não é? Para simplificar vamos olhar como fica nosso partial _form.html.erb
:
<% form_for(@person) do |f| %> ........ <fieldset id="contacts"> <legend>Contacts</legend> <% f.fields_for :contacts do |contacts_form| -%> <%= render :partial => 'contact', :locals => { :f => contacts_form } %> <% end -%> </fieldset> <p> <%= f.submit 'Submit' %> </p> <% end %>
Adicionamos um fieldset para agrupar os contatos, e utilizamos o método fields_for
a partir da instância do form pessoa (f.fields_for
), informando que estamos gerenciando os contatos (:contacts
). Para cada contato da pessoa será gerado o partial _contact.html.erb
, que iremos criar agora. Perceba também que estamos passando para o partial
a instância do formulário atual de contato (contacts_form
). Crie um partial
chamado _contact.html.erb
em app/views/people/
e coloque o seguinte código (lembre-se que a variável f
aqui se refere a instância contacts_form
):
<div class="contact"> <p> <%= f.label :kind %> <%= f.select :kind, Contact::KINDS.map { |kind| [kind.humanize, kind] } %> <%= f.label :description %> <%= f.text_field :description %> <% unless f.object.new_record? -%> <%= f.check_box :_delete %><%= f.label :_delete, 'Delete' %> <% end -%> </p> </div>
Criamos aqui um select com opções do tipo de contato que estamos criando, e um input para o usuário entrar com o telefone ou e-mail por exemplo, além de uma checkbox para excluir um contato se o mesmo não for um novo registro (f.object
nos dá acesso ao objeto ActiveRecord do formulário que está sendo processado, no nosso caso um contato). Isso já facilita bastante a manutenção: para excluir um contato é só marcar a checkbox e enviar o formulário, se tudo correr bem o contato deixará de existir.
Para testar essa funcionalidade crie algumas pessoas e contatos no console e abra a tela de edição: você verá os contatos no mesmo formulário para alterá-los, excluí-los, etc. Para permitir a inclusão de mais contatos nessa mesma tela, altere os métodos new
e edit
do controller e adicione a seguinte linha para criar alguns contatos em memória, gerando assim alguns formulários de contatos:
3.times { @person.contacts.build }
Até aqui tudo bem. Contudo esbarramos num pequeno problema: a melhor maneira de utilizar essa funcionalidade é permitir que o usuário possa criar quantos contatos ele queira, sem ter que incomodar o servidor a cada momento apenas para gerar uma nova linha no formulário. Da maneira que programamos até aqui, o usuário poderá criar apenas 3 contatos por vez, ou quantos nós decidirmos que seja ideal. Para conseguir isto de forma dinâmica temos que utilizar um pouco de javascript e alguns helpers.
Primeiro temos que modificar o nosso layout people.html.erb
em app/views/layouts
para que carregue os javascripts padrão, e também vamos adicionar um método para poder escrever algum código javascript manualmente a partir dos templates que usam esse layout. Localize a linha que adiciona o stylesheet do scaffold e coloque o seguinte código acima dela:
<%= javascript_include_tag :defaults %> <% javascript_tag do %> <%= yield(:javascript) %> <% end -%>
Vamos adicionar um link para criar novos contatos, e teremos que programar este link para gerar um novo formulário com javascript.
<fieldset id="contacts"> ...... <p><%= link_to 'Add contact', '#contacts', :class => 'add' %></p> </fieldset>
Antes de fazermos com que o o link adicione o formulário, vamos apenas programá-lo para adicionar algum texto no local onde o mesmo deveria ser posicionado: desta forma só precisamos gerar um novo formulário e colocá-lo no lugar do texto, e tudo estará funcionando. A classe 'add'
será usada para identificarmos o link e manipularmos a função click
dele. O segundo parâmetro do link_to
, que seria o caminho para onde o link apontaria, vai ser utilizado mais tarde para identificar o template correto a ser renderizado (por enquanto vamos renderizar apenas um texto, ok?). Abra o application.js
e adicione o seguinte código nele:
var NestedAttributesJs = { add : function(e) { element = Event.findElement(e); element.insert( { before: 'AQUI VAI O NOSSO FORMULARIO<br/>' } ) } } Event.observe(window, 'load', function(){ $$('.add').each(function(link){ link.observe('click', NestedAttributesJs.add); }); });
O que acontece aqui: após todo o DOM da página ser carregado, usamos o método $$('.add')
para encontrar todos os elementos com a classe add
(lembram do nosso link?), percorrendo então o array de elementos encontrados (apenas um por enquanto) com o método each
e adicionando um observador para o evento click
. Em resumo, quando cada elemento com essa classe for clicado, irá disparar a função NestedAttributesJs.add
. Dentro desta função buscamos o elemento atual passado como parâmetro (nesse caso o nosso link que está sendo clicado) e inserimos antes dele um texto e uma quebra de linha html. É exatamente no lugar deste texto que vamos renderizar nosso formulário, mas para fazermos isto temos que ter um template do formulário já gerado em alguma variável javascript, para apenas mudar o nosso texto temporário por esta variável. Vamos criar alguns helpers para nos ajudar nesta tarefa, mas antes faça alguns testes no seu navegador clicando no link e vendo onde o texto está sendo inserido. Após, abra o PeopleHelper
e adicione o seguinte código nele:
def nested_attributes_for(form_builder, *args) content_for :javascript do content = "" args.each do |association| content << "\nvar #{association}_template='#{generate_template(form_builder, association.to_sym)}';" end content end end def generate_html(form_builder, method, options = {}) options[:object] ||= form_builder.object.class.reflect_on_association(method).klass.new options[:partial] ||= method.to_s.singularize options[:form_builder_local] ||= :f form_builder.fields_for(method, options[:object], :child_index => 'NEW_RECORD') do |f| render(:partial => options[:partial], :locals => { options[:form_builder_local] => f }) end end def generate_template(form_builder, method, options = {}) escape_javascript generate_html(form_builder, method, options = {}) end
São três métodos para estudarmos aqui:
generate_html
: cria um novo objeto da classe que estamos gerando (o mesmo que Contact.new no nosso caso), e opartial
para esse novo objeto utilizando ofields_for
. O resultado é umpartial
vazio para incluir um registro aninhado. Um pouco de atenção ao parâmetro:child_index => 'NEW_RECORD'
: ele será bastante importante. Este parâmetro diz aofields_for
para utilizar a stringNEW_RECORD
como índice do registro ao gerar este formulário, ao invés de utilizar um contador, como estávamos fazendo manualmente ao criar novos contatos aninhados ('new_1'
,'new_2'
). Se tiver dúvidas quanto a esta parte, volte um pouco até onde falamos sobre os testes no console e verifique como estávamos criando novos contatos;generate_template
: dispara o métodogenerate_html
, escapando caracteres javascript (lembre-se: tudo isto irá parar em uma variável javascript);nested_attributes_for
: cria uma ou mais variáveis javascript, cada uma contendo o respectivo template gerado para formulário, jogando o conteúdo no cabeçalho do documento (adicionamos esta possibilidade anteriormente comyield(:javascript)
no layout).
Com estes novos métodos helpers podemos gerar agora nossa variável javascript contendo o formulário vazio para a criação de novos contatos. Vale lembrar que estes métodos funcionarão posteriormente em qualquer formulário aninhado que você precise, seguindo as mesmas convenções de nomes que seguimos aqui. Adicione ao partial _form.html.erb
logo abaixo do form_for
:
<% nested_attributes_for f, :contacts -%>
Ao atualizar a página agora e visualizar o código fonte, poderá ver que no cabeçalho será criada uma variável javascript chamada contacts_template
que possui o html necessário para gerar o novo formulário. Aproveite para analisar também como a string NEW_RECORD
foi posicionada dentro do nosso template gerado.
<script type="text/javascript"> //<![CDATA[ var contacts_template='<div class=\"contact\">\n <p>\n <label for=\"person_contacts_attributes_NEW_RECORD_kind\">Kind<\/label>\n <select id=\"person_contacts_attributes_NEW_RECORD_kind\" name=\"person[contacts_attributes][NEW_RECORD][kind]\"><option value=\"home_phone\">Home phone<\/option>\n<option value=\"work_phone\">Work phone<\/option>\n<option value=\"email\">Email<\/option>\n<option value=\"url\">Url<\/option><\/select>\n <label for=\"person_contacts_attributes_NEW_RECORD_description\">Description<\/label>\n <input id=\"person_contacts_attributes_NEW_RECORD_description\" name=\"person[contacts_attributes][NEW_RECORD][description]\" size=\"30\" type=\"text\" />\n\n <\/p>\n<\/div>\n\n'; //]]> </script>
Vamos alterar agora nosso javascript para utilizar o template. Você se lembra quando criamos o link para adicionar um novo contato, passamos o parâmetro #contacts
como se fosse o caminho para o link? Vamos utilizar a propriedade href do link, que possui esse texto, remover o # e obter nosso template que se chama contacts_template
. Por isso a importância da convenção =). Substitua o código do application.js
pelo abaixo:
var NestedAttributesJs = { add : function(e) { element = Event.findElement(e); template = eval(element.href.replace(/.*#/, '') + '_template'); element.insert( { before: NestedAttributesJs.replace_ids(template) } ); }, replace_ids : function(template){ var new_id = new Date().getTime(); return template.replace(/NEW_RECORD/g, new_id); } } Event.observe(window, 'load', function(){ $$('.add').each(function(link){ link.observe('click', NestedAttributesJs.add); }); });
O que mudou: dentro do método NestedAttributesJs.add
estamos atribuindo à variável template
o conteúdo da nossa variável contacts_template
através do método eval
. Agora um ponto importante: adicionamos um método chamado replace_ids
, que substitui no nosso template todas as ocorrências de NEW_RECORD
pelo número de milisegundos desde a meia-noite de primeiro de janeiro de 1970, gerados pela função Date().getTime()
. A idéia é que seja sempre gerado um número novo a cada registro adicionado ao formulário. Se você verificar o código gerado através da extensão Web Developer do Firefox, verá que ao invés de person[contacts_attributes][NEW_RECORD]
temos algo como person[contacts_attributes][1238198708416]
nos campos do novo formulário. Desta forma o Rails saberá identificar que este é um novo registro. Caso isto não seja feito, se você criar 3 contatos no seu formulário e enviá-lo para o servidor, o Rails receberá três contatos com NEW_RECORD
e salvará apenas um deles.
Bom, neste ponto já devemos ter nossa estrutura funcionando para adicionar e remover contatos aninhados. Lembre-se que estamos ignorando contatos sem descrição, portanto você pode criar 10 contatos no formulário sem preencher sua descrição, que ao postar o Rails ignorará esses contatos.
Uma outra funcionalidade interessante seria a possibilidade de excluir um contato adicionado, mas não temos como fazer isto através de checkboxes pois o formulário está sendo criado dinamicante. Vamos criar um link para remover o html gerado para o novo registro. Para facilitar vamos mover o código que cria ou um link ou uma checkbox para remoção do registro aninhado, se o registro for ou não novo, permitindo assim reutilizar este código em outros templates posteriormente. Adicione ao PeopleHelper
:
def remove_link_unless_new_record(form_builder) unless form_builder.object.new_record? form_builder.check_box(:_delete) + form_builder.label(:_delete, 'Delete') else link_to_function('Delete', "$(this).up('.#{form_builder.object.class.name.underscore}').remove();") end end
E altere o partial _contact.html.erb
conforme abaixo:
# remova estas linhas #<% unless f.object.new_record? -%> # <%= f.check_box :_delete %><%= f.label :_delete, 'Delete' %> #<% end -%> # e adicione esta no lugar <%= remove_link_unless_new_record(f) %>
Isto já deve ser suficiente para utilizarmos este cadastro incluindo e excluindo contatos dinamicamente. Podemos partir para a seleção de uma cidade para a pessoa.
Cidade
Quando trabalhamos com este tipo de relação entre pessoa e cidade, normalmente criamos um select com as cidades disponíveis para que o usuário selecione qual a cidade que a pessoa pertence. Muitas vezes gostaríamos de permitir que uma cidade possa ser criada junto com a pessoa, caso ela não exista na lista. E o Rails agora permite fazer isto de maneira simples utilizando atributos aninhados. Para que isto funcione precisamos apenas fazer uma alteração na view, adicionando abaixo do campo nome:
<p> <%= f.label :city_id %> <%= f.collection_select :city_id, City.all(:order => 'name'), :id, :name %> or <% f.fields_for :city, City.new do |city_form| -%> <%= city_form.label :name, 'Create a new' %><%= city_form.text_field :name %> <% end -%> </p>
O código acima cria um select com as cidades disponíveis ordenadas por nome (ok eu sei, a chamada ao City.all(:order => 'name')
não deveria estar aqui, mas estou apenas simplificando pois o post já está bastante extenso), e também um input para a criação de uma nova cidade. Desta forma, o usuário tem a possibilidade de ou selecionar uma cidade pelo select ou entrar com o nome de uma nova cidade.
Para terminar
Ufa! Foi um pouco extensivo mas é isso aí. Após um pouco de trabalho temos uma manutenção totalmente funcional que trabalha com atributos aninhados.
O projeto que criamos aqui está disponível no github.
Espero ter ajudado a quem estava com dúvidas sobre a utilização desta nova funcionalidade.
Qualquer dúvida, dificuldade ou crítica por favor poste um comentário que terei prazer em responder assim que possível.
[…] blog do Carlos Antonio: Uma nova funcionalidade adicionada ao Rails 2.3 é a possibilidade de atualizar os atributos em […]
Cara, gostei desse helper, dá pra fazer bastante coisa com esses métodos.
Acho que a forma de reaproveitar isso é colocar no application_helper.
No geral, achei que você falou muita coisa pra pouca coisa.
Esse Javascript aí escrito em jquery ficou bem menor e mais fácil de ser entendido :)
Olá Daniel,
sim, concordo que a melhor opção seria mover os helpers para o application_helper, para que ficassem disponíveis em outros locais, valeu pela dica.
Quando ao post, o objetivo era mais para ser uma espécie de ‘tutorial’, e realmente ficou bastante extenso, tentei explicar mais a fundo alguns detalhes do que acontecia ao criar essa estrutura, me baseando no trabalho do Eloy. Mas é isso mesmo, vou tentar melhorar na próxima. Obrigado.
Ah, também gosto do jquery =)..
Abraços
Olá,
ótimo tutorial. Parabéns!
Tenho uma dúvida: existe verificação de existência de contato ao criar um novo registro. Mas se, ao alterar um contato, eu deixá-lo em branco, o sistema o salvará em branco mesmo, certo?
Existe algo a incluir no model que verifique isso, assim como na criação? Ou é necessário verificar no update se ele foi enviado em branco e destrui-lo?
Obrigado.
Abraços
Olá Habib,
ao alterar um contato, se você deixar os dados em branco serão retornados os erros dizendo que este contato está inválido. O Rails verifica a existência do
:id
no hash de campos (que é incluído automaticamente como hidden no seu form), dessa forma ele sabe que você está editando um registro e não incluindo um novo. Assim, deixando os campos em branco não passará na validação, nem no seu método:reject_if
.Para excluir um registro você tem que marcar o atributo
:_delete
para verdadeiro (no caso usamos um checkbox para marcar um registro a ser excluído).Se você quiser verificar se um registro está marcado para ser excluído, use o método
marked_for_destruction?
.Obrigado =).
Abraços
Olá Carlos,
obrigado pelo esclarecimento. Gostaria de tirar mais uma dúvida: é possível aplicar a mesma técnica, mas com modelos que possuem uma relação many-to-many (utilizando has_many, :through)?
Tentei seguir os mesmos passos, mas o formulário renderizado utilizando o fields_for não inclui campos hidden para os ids das duas tabelas, apenas uma delas. Assim, sempre que se edita um campo, o Rails cria uma nova entrada (por falta do outro id).
Obrigado novamente.
Abraços,
Habib
Habib,
não tenho conhecimento sobre a manutenção de um
has_many :through
através de nested attributes, não sei como o Rails se comportaria.Vou procurar fazer alguns testes aqui e posto algo a respeito..
Caso você tenha alguma novidade ficaria grato se postasse aqui nos comentários.
Obrigado e abraços.
Hi
I just found this tutorial after searching for a while. It looks quite comprehensive and complete – it looks like you did a great job. . I haven’t had a chance to review it completely – although I did download and run it, and thought in advance of doing so – I’d ask a quick question.
My question is do you have any advice on how to code a single form capable of managing 3 one-to-many relationships. For example …
Quiz -> Questions -> Answers
A Quiz has_many Questions
A Question has_many Answers
This form would have all the dynamic capabilities shown in your sample.
Thanks
Dave
Hello Dave,
To create a form capable of managing complex relationships like this you can follow the same pattern of this example, it’ll work like a charm.
I also recommend you to take a look on complex form examples from Elloy Duran, which is also linked on my post. He has the exact example you need, with 2 levels nesting.
http://github.com/alloy/complex-form-examples/tree/master
Any doubts please comment or send me an email.
Thanks,
Carlos.
Hi Carlos
I tried recreating a 3 model form by modifying the Elloy code – but what I ended up with was a form with a link for adding the 3rd model (Answers in my case) which doesn’t work. I click on it and nothing happens.
I tried to create my app using the methods described in your post above (by the way, I liked your method as it appears a bit cleaner). I was able to create an app that easily supported Quiz -> Questions – but when I tried to add the Questions -> Answer, it wasn’t clear to me exactly where I should add this statement …
and furthermore – which form ‘f’ should reference.
Perhaps you can help me resolve this issue ?
I wasn’t able to find your email, and I’d prefer to continue this conversation via email. My email is dekhaus@(mac dot com).
Thanks
Dave
olá pessoal
sou novo no rails, e tenho uma duvida muito louco, eu sou programador PHP, mas não entendo isso.
Veja bem, você criou o modelo Person e como o rails sabe quando você coloca has_many :people ????
o rails sabe o que é person, mas people, como ele sabe?
Olá Elias tudo bem?
Acontece o seguinte: o Rails tem um módulo chamado Inflector onde são definidas expressões para que ele saiba que ‘person’ no singular significa ‘people’ no plural. Da mesma forma para palavras comuns ele sabe que apenas adicionar um ‘s’ já basta.. Essas regras são definidas usando expressões regulares, e ainda é possível adicionar quantas regras você quiser.
Se você observar ao criar uma nova aplicação rails, dentro do diretório config/initializers existe um arquivo chamado inflections.rb onde você pode criar suas regras personalizadas e ver alguns exemplos (dá uma olhada, o ‘person’ > ‘people’ está lá).
Se tiver mais alguma dúvida basta postar um novo comentário ok.
Um abraço.
Carlos
Que bacana hein Carlos
Agora que eu vi.
Muito legal isso do rails.
Um abraço e obrigado
Olá
Estive com algumas difuculdades no seguinte :
Erro: contacts_template is not defined
Arquivo-fonte: http://localhost:3000/javascripts/application.js?1258843239
Linha: 4
template = eval(element.href.replace(/.*#/, ”) + ‘_template’);
A verdade que esse erro é porque eu não utilizei
achando que era desnecessário e pulei.
Pra quem tiver esse problema é só usar isso
nao usei o j a v a s c r i p t, o wordpress bloqueia javascript nos coments
Olá Carlos
Estive fazendo aqui, mas tenho uma dúvida, sabe o checkbox?
em nenhum momento vc informa se o checkbox estiver marcado entao apague o registro. Como o rails fez isso?
Olá Elias, o que ocorre é que o próprio Rails gerencia a destruição destes registros com nested attributes, quando o campo
_delete
está marcado como verdadeiro (no caso a checkbox).Abraço.
Por que no meu Firefox funciona e no IE nao? Está dando erro de Javascript…
O IE é comum ter problemas com qualquer coisa mesmo =).. Brincadeiras a parte, não me recordo se testei o javascript em todos os browsers, utilizo o firefox. Qual o erro que você está tendo no IE, você consegue identificar?
Carlos,
Usando nested-attributes como você demonstrou, é possivel fazer upload de fotos com Paperclip?
Eu tentei fazer e não consigo, aparece erro só no console, no browser aparece que foi criado com sucesso.
[2010-02-01 16:14:55] ERROR Errno::ECONNABORTED: Uma conexÒo estabelecida foi an
ulada pelo software no computador host.
C:/Ruby19/lib/ruby/1.9.1/webrick/httpserver.rb:56:in `eof?’ C:/Ruby19/lib/ruby/1.9.1/webrick/httpserver.rb:56:in `run’
C:/Ruby19/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread
‘
Depois disso, não aparece mais nada no console. Nunca aconteceu isso comigo, sabe o que pode ser?
Márcio,
não cheguei a implementar nested com upload de arquivos, mas acredito que deve funcionar sem problemas, não vejo nada inicialmente que poderia complicar o processo. Vejo que você está usando o Ruby 1.9.1, o paperclip já está 100% funcionando com essa versão do Ruby? Já chegou a tentar com 1.8.7?
Abraço, Carlos.
Carlos,
Obrigado pela resposta, mais era erro de falta de atenção mesmo.
Uma unica duvida, para se visualizar imagens do paperclip, usa-se @pessoa.imagem.url e quando se esta aninhados?.
Tenho pessoa e fotos, seria algo como @pessoa.fotos.imagem.url
Creio que terei que buscar pelo controller.
Abraço.
Se você quer mostrar todas as fotos ligadas a uma pessoa, tem que percorrer as fotos e mostrar uma a uma. Algo como:
@pessoa.fotos.each do |foto|
foto.imagem.url
end
Se for para exibir as imagens no formulário (nested), quando em edição, pode usar algo assim:
f.fields_for :fotos do |fotos_form| # código que você já deve ter...
fotos_form.object.imagem.url
end
Fechou! ;D
Obrigado pela atenção Carlos, sucesso.
Valeu Marcio!
Abraço
Carlos,
Primeiro obrigado pelo post.
Consegui implementar perfeitamente a solução. Porém ficaram umas dúvidas.
Como faço para quando estiver editando um registro de person, ja carregar os contacts existens?
Tenho que carregar em um lista diferente? ( @person.contacts.each….) ?
Segundo, como faria para editar e excluir esses itens já existentes? (preciso criar um Contacts_Controller) ?
É isso Carlos, desde já agradeço.
Um Abraço,
André
Olá André,
Basicamente quando você usa f.field_for :contacts, ele já vai carregar os inputs do fields_for para cada contact que vc tiver na coleção @person.contacts, por isso que criamos alguns contacts em branco na action new no controller, como você pode ver aqui: https://github.com/carlosantoniodasilva/nested_attributes_example/blob/master/app/controllers/people_controller.rb#L28
Com isso basta alterar os dados e mandar salvar.
Para excluir itens a gente cria um link para remover os registros, que nada mais faz do que marcar o campo _delete para true. Pode ser usato até uma checkbox pra isso. Novamente, só salvar o form que o nested attributes funciona, removendo os registros que foram marcados com _delete, então não precisa outro controller.
Dá uma olhada na app exemplo, baixa e roda ela, vai tirar suas dúvidas.
Valeu, e obrigado.
Ola.
Muito útil o seu tutorial, mas tive um problema com trecho de código do laço dos contatos “”, faltou o “=”, ficando assim
, e um outro problema, está dando um erro “Object function Event() { [native code] } has no method ‘observe'” no javascript , na função “Event.observe”, isso com o rails 3. Pelo que entendi houve algumas mudanças na forma em que o prototype é usado no rails 3, mas não consegui chegar a uma solução, tem como me dar um help?
att
Olá Clairton,
obrigado pelo feedback. Apesar de grande parte da lógica ser a mesma para se trabalhar com nested attributes no Rails desde a primeira implementação, como o tutorial é para a versão 2.3 do Rails, que já foi lançada a um bom tempo, algumas coisas acabaram sendo modificadas para a versão 3.x.
Eu recomendaria dar uma olhada nestes Railscasts (inglês) que explicam bem como funciona e também tem o source para você dar uma olhada como está implementado.
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
Se tiver mais alguma dúvida, fique a vontade para perguntar.
Abraço.
Obrigado por responder, segui os passos desses cast, e aparetemente deu certo, mas quando clico para adicionar uma nova informação não aparece html renderizado e sim como texto, por exemplo em vez de acrescentar uma nova linha, aparece “”, o que pode ser isso?
Provavelmente é alguma coisa relacionada a escapar o conteúdo a ser exibido que está causando isto. No seu comentário não aparece o código (pelo que parece você colou algo ali :D). Será que você pode postar parte do seu código em um http://pastie.org/ e mandar o link para eu dar uma olhada? A parte de helpers e views já deve resolver. Valeu.
Vai ficar meio complexo para explicar, vou tentar , :)
No partial eu tenho o codigo para adicionar o link:
“”
no application_helper
“def link_to_add_fields(name, f, association)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => “new_#{association}”) do |builder|
render(association.to_s.singularize + “_fields”, :f => builder)
end
link_to_function(name, h(“add_fields(this, ‘#{association}’, ‘#{escape_javascript(fields)}’)”))
end”
e no application.js
“function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp(“new_” + association, “g”)
$(link).parent().before(content.replace(regexp, new_id));
}”.
mas quando clico para adiciona um novo dado, em vez de aparecer o input para digitar o texto, aparece o texto do html
”
Número Telefone
remove
“.
Estou bem perdido.
Obrigado pela atenção.
att
o texto html não apareceu, o link do texto que aparece está em http://pastie.org/3138390
Entendi, o problema é que está sendo escapado a string que representa a chamada da função
add_fields
dentro do helperlink_to_add_fields
, que faz com que o html todo (o template) seja escapado e exibido como “texto”. Se você remover a chamada aoh
do seu helper, vai funcionar.cara, mto obrigado.
Opa, por nada.