Formulários aninhados com objetos ActiveModel :: Model

ActiveModel :: Model é uma excelente maneira de fazer os objetos se comportarem como ActiveRecord. Um uso potencial para isso é se quisermos fazer uso de dados de formulário que não necessariamente persistem em um objeto, mas queremos manter nossos controladores limpos. Um ótimo exemplo pode ser encontrado neste post do blog do thinkbot de cerca de um ano atrás.

Uma coisa que eu acho que muitos encontraram faltando, no entanto, é has_manye accepts_nested_attributes_forfuncionalidade. Eu me encontrei neste barco recentemente, quando quis usar esses objetos de formulário para criar vários registros. Como posso ter um formulário que pode criar muitos registros arbitrariamente e ter erros de validação para cada registro que estou tentando criar, sem associações?

Como muitos, comecei a procurar joias para resolver meu problema, mas depois de um dia ou mais procurando pela solução perfeita, encontrei a chave que faltava. Se um objeto com associação um-para-muitos for instanciado com um hash params, e esse hash tiver uma chave para a associação, o Rails chamará o <association_name>_attributes=método naquele objeto. Portanto, precisamos definir isso em nosso objeto de formulário.

class ContactListForm
include
ActiveModel::Model

attr_accessor
:contacts

def contacts_attributes=(attributes)
@contacts ||= []
attributes
.each do |i, contact_params|
@contacts.push(Contact.new(contact_params))
end
end
end

Agora precisamos de um formulário para este modelo. Mantendo a simplicidade:

<div>
<%= form_for @contact_list, url: contacts_path, method: :post do |f| %>
<%= f.fields_for :contacts do |c| %>
<%= c.text_field :first_name %>
<%= c.text_field :last_name
<%= c.text_field :phone %>
<% end %>

<p>
<%= f.submit "Submit" %>
</p>
<% end %>
</div>

É claro que nosso objeto ainda não tem nenhum contato, então nosso controlador precisará se certificar de que o formulário tenha pelo menos um fields_forbloco para renderizar, dando-lhe um na inicialização

class ContactsController < ApplicationController
def new
@contact_list = ContactListForm.new(contacts: [Contact.new])
end
end

Meu controlador também precisa de uma createação para quando o formulário for enviado.

class ContactsController < ApplicationController
def create
@contact_list = ContactListForm.new(params[:contact_list_form])

if @contact_list.save
flash
[:notice] = "Created contacts"
redirect_to root_path

else
flash
[:notice] = "There were errors"
render
:action => 'new'
end
end
end

Se todos os formulários fossem válidos, todos os contatos foram enviados ao banco de dados. Mas e os erros de validação para cada contato do formulário? Primeiro, peguei um bom conselho de uma postagem StackOverflow para apenas redefinir o error_messagesmétodo que estava obsoleto no Rails 3

class CustomFormBuilder < ActionView::Helpers::FormBuilder
def error_messages
return unless object.respond_to?(:errors) && object.errors.any?

html
= ""
html
<< @template.content_tag(:span, "Please correct the following errors.")
html
<< object.errors.full_messages.map { |message| @template.content_tag(:li, message) }.join("n")

@template.content_tag(:ul, html.html_safe)
end
end

ActionView::Base.default_form_builder = CustomFormBuilder

Agora, apenas uma pequena atualização no meu formulário:

<%= f.fields_for :contacts do |c| %>
<%= c.error_messages %>
<%= c.text_field :first_name %>
<%= c.text_field :last_name
<%= c.text_field :phone %>
<% end %>

E estou pronto para ir.

Para minha próxima postagem, adicionaremos um link para o formulário que adicionará entradas para outro contato, para que possamos enviar um número arbitrário de contatos. Por favor fique atento!