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.