MultiModel forms, parte 3: Nested Forms
Autor: fabiolnm | Publicado: 2012-08-06T00:27:00-03:00

Este post é o terceiro de uma série de quatro artigos:
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
O código desta aplicação de exemplo está disponível neste repositório Github.

Nested FormsNos 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):
&lt;%= form_for(@person) do |f| %&gt;
  &lt;div class="field"&gt;
    &lt;%= f.label :name %&gt;&lt;br /&gt;
    &lt;%= f.text_field :name %&gt;
  &lt;/div&gt;

  &lt;%= f.fields_for :user_account do |acc| %&gt;
    &lt;div class="field"&gt;
      &lt;%= acc.label :username %&gt;&lt;br /&gt;
      &lt;%= acc.text_field :username %&gt;
    &lt;/div&gt;
    &lt;div class="field"&gt;
      &lt;%= acc.label :password %&gt;&lt;br /&gt;
      &lt;%= acc.password_field :password %&gt;
    &lt;/div&gt;

    &lt;%= acc.fields_for :permissions do |p| %&gt;
      &lt;div class="field"&gt;
        &lt;%= p.label p.object.restricted_area %&gt;
        &lt;%= p.hidden_field :restricted_area %&gt;
        &lt;%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %&gt;
      &lt;/div&gt;
    &lt;% end %&gt;
  &lt;% end %&gt;

  &lt;div class="actions"&gt;
    &lt;%= f.submit %&gt;
  &lt;/div&gt;
&lt;% end %&gt;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:
&gt; Person.new.user_account
 =&gt; 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: 
&lt;%= acc.fields_for :permissions, acc.object.permissions_types do |p| %&gt;
  &lt;div class="field"&gt;
    &lt;%= p.label p.object.restricted_area %&gt;
    &lt;%= p.hidden_field :restricted_area %&gt;
    &lt;%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %&gt;
  &lt;/div&gt;
&lt;% end %&gt;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"=&gt;"✓", 
  "authenticity_token"=&gt;"iTFVyyIwp0w2apXT02s+XN3z6PfOFEuUgZRaZWxPegU=",
  "person"=&gt;{
    "name"=&gt;"José da Silva",
    "user_account_attributes"=&gt;{
      "username"=&gt;"jsilva",
      "password"=&gt;"[FILTERED]",
      "permissions_attributes"=&gt;{
        "0"=&gt;{
          "restricted_area"=&gt;"admin",
          "grants"=&gt;"Read-Only"
        },
        "1"=&gt;{
          "restricted_area"=&gt;"backups",
          "grants"=&gt;""
        },
        "2"=&gt;{
          "restricted_area"=&gt;"logs",
          "grants"=&gt;"Read-Write"
        }
      }
    }
  },
  "commit"=&gt;"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&nbsp;para edição:


É incrível: simplesmente funciona! Tudo bem até aqui, já conseguimos salvar.&nbsp;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&nbsp;como um de seus parâmetros.

Pode-se usar um checkbox para adicionar um atributo _destroy:
&lt;%= acc.fields_for :permissions, acc.object.permissions_types do |p| %&gt;
  &lt;%= p.label p.object.restricted_area %&gt;
  &lt;%= p.hidden_field :restricted_area %&gt;
  &lt;%= p.select :grants, [ "Read-Only", "Read-Write", "Read-Write-Delete" ], include_blank: true %&gt;
  &lt;% unless p.object.new_record? %&gt;
    &lt;%= p.check_box :_destroy %&gt;
    &lt;%= p.label :_destroy, 'Remove' %&gt;
  &lt;% end %&gt;
&lt;% end %&gt;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,&nbsp;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ênciashttp://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/