- Parte 1: Nested Models - One-to-One
- Parte 2: Nested Models - One-to-Many
- Parte 3: Nested Forms
- Parte 4: Nested Forms dinâmicos usando Knockout.js
Nested Forms
Nos dois primeiros posts desta série, foi mostrado o poder do uso de Nested Models. O Rails fornece ainda helpers para a construção de views com este recurso. Usamos estes helpers para reescrever o arquivo app/views/people/_form.html.erb (criado via scaffolding, no primeiro post):<%= form_for(@person) do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :user_account do |acc| %>
<div class="field">
<%= acc.label :username %><br />
<%= acc.text_field :username %>
</div>
<div class="field">
<%= acc.label :password %><br />
<%= acc.password_field :password %>
</div>
<%= acc.fields_for :permissions do |p| %>
<div class="field">
<%= p.label p.object.restricted_area %>
<%= p.hidden_field :restricted_area %>
<%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %>
</div>
<% end %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>Entre na tela de cadastro (http://localhost:3000/people/new), para visualizar um formulário contendo os campos de Person, UserAccount e Permission:Oops. Apenas os campos de Person estão visíveis. Não é difícil conjecturar o porquê. Ao criar um novo objeto Person, ele não possui um user_account, como pode ser confirmado no console:
> Person.new.user_account
=> nilÉ necessário sobrescrever o método Person/user_account, para que retorne um novo objeto quando Person não tiver uma conta de usuário associada: def user_account
@user_account = super
@user_account = build_user_account if @user_account.nil?
@user_account
endIsto é suficiente para habilitar os campos de UserAccount:Para exibir os campos referentes aos atributos de Permission, criamos um método UserAccount/permissions_types, que pré-instancia as possíveis opções:
def permissions_types
hash = {}
permissions.each { |p| hash[p.restricted_area] = p }
[ "admin", "backups", "logs" ].each { |area|
hash[area] = Permission.new restricted_area: area unless hash.has_key? area
}
hash.values
endNote que as duas primeiras linhas "memorizam" associações previamente salvas, enquanto nas linhas seguintes, são construídos novos objetos apenas para as associações que ainda não estiverem presentes.É necessário modificar o formulário para construir os campos de permissões utilizando este método:
<%= acc.fields_for :permissions, acc.object.permissions_types do |p| %>
<div class="field">
<%= p.label p.object.restricted_area %>
<%= p.hidden_field :restricted_area %>
<%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %>
</div>
<% end %>Vejamos como ficou:Agora que os campos já estão presentes no formulário, basta salvar um registro:
Oops... estamos quase lá... Vamos tentar entender o que aconteceu. Com auxílio do Chrome Developer Tools, podemos analisar os dados que são submetidos com o formulário:
Os view helpers do Rails criam campos com nomes que são adequados à sua forma de processar nested models. Se analisarmos o log do controller, vemos que o request foi convertido para um hash, representando estes parâmetros de forma hierárquica (formatei o log para facilitar sua visualização):
Started POST "/people" for 127.0.0.1 at 2012-08-05 22:36:11 -0300
Connecting to database specified by database.yml
Processing by PeopleController#create as HTML
Parameters: {
"utf8"=>"✓",
"authenticity_token"=>"iTFVyyIwp0w2apXT02s+XN3z6PfOFEuUgZRaZWxPegU=",
"person"=>{
"name"=>"José da Silva",
"user_account_attributes"=>{
"username"=>"jsilva",
"password"=>"[FILTERED]",
"permissions_attributes"=>{
"0"=>{
"restricted_area"=>"admin",
"grants"=>"Read-Only"
},
"1"=>{
"restricted_area"=>"backups",
"grants"=>""
},
"2"=>{
"restricted_area"=>"logs",
"grants"=>"Read-Write"
}
}
}
},
"commit"=>"Create Person"
}Faz sentido: o rails tentou criar uma permission com o campo grants em branco, mas este campo é obrigatório (vide a parte 2 desta série). É necessário alguma forma de filtrar de permission_attributes os valores que estiverem com grants em branco. Há: Rails possui essa facilidade:accepts_nested_attributes_for :permissions, allow_destroy: true,
reject_if: proc { |attrs|
attrs["grants"].blank?
}Basta este pequeno ajuste em UserAccount para conseguimos salvar o registro, que na figura abaixo, foi reaberto para edição:É incrível: simplesmente funciona! Tudo bem até aqui, já conseguimos salvar. Mas logo descobrimos que o "brinquedo quebra" se tentar remover uma permissão: tente deixar Logs em branco e salvar. Parece que tudo deu certo, mas ao reabrir para edição, a permissão continua lá.
Isto ocorre pois, após termos adicionado a cláusula reject_if, as permissões em branco passaram a ser rejeitadas. O Rails não tem mais a oportunidade de processá-las. Além disso, vimos nos posts anteriores que, para remover um nested model, é necessário passar _destroy: true como um de seus parâmetros.
Pode-se usar um checkbox para adicionar um atributo _destroy:
<%= acc.fields_for :permissions, acc.object.permissions_types do |p| %>
<%= p.label p.object.restricted_area %>
<%= p.hidden_field :restricted_area %>
<%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %>
<% unless p.object.new_record? %>
<%= p.check_box :_destroy %>
<%= p.label :_destroy, 'Remove' %>
<% end %>
<% end %>O formulário fica com a seguinte aparência:E agora, com auxílio do checkbox, conseguimos também remover permissões. Um único formulário, manipulando uma complexa estrutura de dados associados - Person 1 : 1 UserAccount 1 : N Permission.
Note que não foi necessário adicionar nenhuma lógica no controller para interpretar argumentos do request dos objetos associados. Apesar de termos sido obrigados a adicionar um checkbox para remoção de permissões, não precisamos de nenhuma lógica customizada de data-binding para apresentar as associações na view. Tudo out-of-the-box, prontinho para usar, um bonus-track desta fantástica framework web que é o Rails!
No quarto e último post desta série, será mostrada "a cereja do bolo": com um pouquinho de JavaScript, jQuery e Knockout.js, tornaremos a adição de permissões mais dinâmica, além de adicionar dinamicamente o atributo _destroy no request, sem precisar de um checkbox.
Referências
- http://guides.rubyonrails.org/getting_started.html#building-a-multi-model-form
- http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
- http://erikonrails.snowedin.net/?p=267
- http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
- http://weblog.rubyonrails.org/2009/1/26/nested-model-forms/






