- 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
end
Isto é 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
end
Note 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/