Formulários aninhados em Ruby on Rails

Tutoriais - 10/Mar/2021 - por André Kanamura

Introdução

Formulários aninhados, em resumo, são uma forma de deixar disponível um formulário de criação de um objeto dentro de outro formulário. Usando o método accepts_nested_attributes_for, é possível salvar os atributos de um objeto por meio da sua associação principal do ActiveRecord. Por exemplo, uma aplicação possui um model User para contas de usuários e cada user deve possuir um perfil (profile) com seus dados pessoais, como CPF, nome e telefone. Podemos permitir que o próprio formulário de registro de User receba os atributos para seu perfil. Vamos ver como fazer isso usando Ruby on Rails e a gem Devise para autenticação. Neste tutorial utilizaremos Ruby 3.0, Rails 6.1.1 e Devise 4.7.3. Este não será um tutorial detalhado sobre Devise, esperamos que você já possua algum conhecimento sobre a gem ou consulte sua documentação oficial.

Configuração inicial

Vamos começar criando o model User, aproveitando o comando gerador do Devise:

$ rails generate devise user

Isso já deve criar um model com os atributos de e-mail e senha, por padrão. Em seguida, vamos criar o model profile, com os atributos nome, cpf e telefone. Além disso, já vamos adicionar a referência para o model User

$ rails generate model profile name cpf phone_number user:references

Não podemos esquecer de rodar as migrações com rails db:migrate.

Como vamos modificar as views e controllers que ficam escondidos no funcionamento interno do Devise, precisamos expô-los. Antes de rodar os comandos abaixo, consulte a documentação oficial do Devise para conhecer as opções desses comandos e expor somente o que for necessário para o contexto da sua aplicação:

$ rails generate devise:views
$ rails generate devise:controllers users

Também será necessário configurar as rotas do Devise para utilizar os novos controllers customizados.

Implementação

Models

Agora que temos a configuração inicial, podemos começar a falar do código. Em primeiro lugar vamos ver como devem ficar os models User e Profile:

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile
  accepts_nested_attributes_for :profile
end
class Profile < ApplicationRecord
  belongs_to :user
end

Dessa maneira, usuários possuem um perfil e o perfil pertence a um usuário. Com a linha accepts_nested_attributes_for :profile, os formulários de registro de users aceitarão atributos para também criar um perfil. Então vamos ver como eles vão ficar.

Views

Quando rodamos o comando para expor as views do Devise, foram criados arquivos que ficam disponíveis dentro do diretório views/devise. Eles ficam organizados em diretórios separados por função: confirmations, mailer, passwords etc. Junto deles você vai encontrar o diretório registrations e lá dentro o arquivo new.html.erb, que nos interessa. O formulário de registro possui os campos para e-mail e senha por padrão. Nós precisamos customizá-lo para que passe a receber os atributos Nome, CPF e Telefone que fazem parte do perfil e ele deve ficar mais ou menos assim:

<h2>Cadastro de Usuário</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email, 'E-mail' %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password, 'Senha' %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation, 'Confirmar Senha' %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <h3>Perfil</h3>
  <%= f.fields_for :profile do |profile_f| %>
      <%= profile_f.label :name, 'Nome' %>
      <%= profile_f.text_field :name %>

      <%= profile_f.label :cpf, 'CPF' %>
      <%= profile_f.text_field :cpf %>

      <%= profile_f.label :phone_number, 'Telefone' %>
      <%= profile_f.text_field :phone_number %>
  <% end %>

  <div class="actions">
    <%= f.submit "Criar Conta" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

Note que eu incluí labels com nomes em português para facilitar a leitura, mas o ideal seria utilizar o i18n para fazer a tradução das views. A maior parte do HTML pode permanecer igual. O importante está logo abaixo do <h3>Perfil</h3>. Indicamos com fields_for a parte do formulário que vai receber os atributos de profile e montamos uma estrutura como um formulário normal. Quando o usuário clicar em “Criar Conta”, o formulário enviará os atributos do perfil como uma hash profile_attributes junto com os parâmetros para user. Por isso, no controller de registrations do Devise, vamos precisar permitir esses valores.

Controllers

Com o RegistrationsController exposto, podemos customizar o funcionamento de métodos para criação de um user do Devise. Para esse exemplo precisamos permitir o recebimento dos atributos de um perfil e “buildar” uma instância de profile na action new, o controller poderia ficar assim:

# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]

  def new
    super { |resource| resource.build_profile }
  end

  protected

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute,
                                      profile_attributes:[:id, :name, :cpf,
                                      :phone_number]])
  end
end

O controller exposto do Devise vem com código dentro dele comentado para facilitar o processo de customização. Nós apenas precisamos “descomentar” parte desse código e modificá-lo. Nesse caso, reaproveitamos o método configure_sign_up_params e o new. No método new, podemos usar o super para intermediar no processo interno do Devise. No nosso exemplo, usamos a variável resource para obter o user e aplicamos o método build_profile para buildar uma instância de perfil que será utilizada na criação do user. Nesse exemplo, os models tem associação de has_one com belongs_to, por isso usamos o método build_profile, mas caso a associação seja do tipo has_many será necessário fazer algo como resource.profiles.build.

Modificamos o método configure_sign_up_params para que ele também permita os atributos :id, :name, :cpf, :phone_number, que vem dentro de profile_attributes nos params.

Com isso, o formulário de criação da conta estará pronto para receber os dados de user com os atributos de profile e deve criar os dois objetos associados. A vantagem de usar um formulário aninhado neste caso é que você pode já exigir no momento de criação da conta que o visitante preencha seus dados de perfil. Naturalmente, também é possível que esses atributos fiquem dentro do próprio model User e nesse caso não seria necessário aninhamento, mas vamos considerar que para o contexto da nossa aplicação é desejável que os dados pessoais fiquem separados em outro model.

Alternativa

A própria documentação do Devise mostra uma forma alternativa de chegar ao mesmo resultado sem expor os controllers. A implementação seria semelhante, mas utilizando o ApplicationController e ficaria mais ou menos assim:

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute,
                                      profile_attributes:[:id, :name, :cpf,
                                      :phone_number]])
  end
end

Neste caso seria necessário passar o método build_profile para a view do formulário.

Conclusão

Com o método accepts_nested_attributes_for o Rails oferece uma maneira bem direta para facilitar a criação de formulários aninhados, permitindo salvar instâncias de models junto com seus objetos associados.

Referências

Nested forms Devise

Foto de perfil do autor
André Kanamura

Dev na Campus Code