En ocasiones, tenemos modelos asociados que necesitamos manipular en un único formulario en lugar de tener un formulario por cada uno de ellos y en este tutorial les mostrare como crear estos formularios anidados (nested forms).

Un formulario anidado nos permite generar una mejor experiencia de usuario al trabajar con modelos relacionados, ya que de esta manera no se tendrá que estar cambiando a las vistas de cada modelo para hacer cambios.

Para mostrar como trabajar con formularios anidados crearemos un proyecto para listar Bancos y sus sucursales. Un banco se podrá editar en un formulario que incluirá todas sus sucursales.

Hora de codear

[symple_heading style=»» title=»Paso 1: Crear Proyecto» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

rails new bank-list
cd bank-list

[symple_heading style=»» title=»Paso 2: Crear los scaffolds» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

Vamos a necesitar los siguientes dos scaffolds para nuestro ejemplo:

rails g scaffold Bank name:string
rails g scaffold BankSubsidiary address:string bank:references
rake db:migrate

La opción :references crea un campo que hace referencia al modelo, en este caso bank.

 Revisemos el schema.

ActiveRecord::Schema.define(version: 20150507004618) do

  create_table "bank_subsidiaries", force: :cascade do |t|
    t.string   "address"
    t.integer  "bank_id" # campo creado con la opción :references  
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_index "bank_subsidiaries", ["bank_id"], name: "index_bank_subsidiaries_on_bank_id"

  create_table "banks", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

[symple_heading style=»» title=»Paso 3: Asociando los modelos y creando las validaciones» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

Ahora vamos a crear la asociación entre los modelos, que en este caso es una relación de ‘uno a muchos’, es decir, un banco puede tener muchas sucursales pero una sucursal solo puede pertenecer a un banco. También validaremos los campos que son obligatorios para nuestros modelos.

Tip: se puede añadir un método llamado to_s y pedirle que imprima la columna que queremos cuando llamamos al objeto y así no tener que especificarlo cada vez que lo usamos. Ej: ‘instancia’ v/s ‘instancia.columna’

class Bank < ActiveRecord::Base
  has_many :bank_subsidiaries, dependent: :destroy
  validates :name, presence: true

  def to_s
    name
  end
end
class BankSubsidiary < ActiveRecord::Base
  belongs_to :bank
  validates :address, presence: true

  def to_s
    address
  end
end

 

También usaremos el archivo seeds para crear datos para probar la aplicación.

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
#   cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
#   Mayor.create(name: 'Emanuel', city: cities.first)

santander = Bank.create(name: 'Banco Santander')
chile = Bank.create(name: 'Banco de Chile')
['Vitacura 4325 - Vitacura', 'Av. Presidente Riesco 5561 Of. 203 P.2 - Las Condes', 'Agustinas N° 1070 P. 6 Of. 52 Stgo. Centro'].each do |bs|
  santander.bank_subsidiaries.create(address: bs)
end
['Av. Presidente Riesco 5711 L/1,LAS CONDES', 'Av. Nueva Los Leones 017,PROVIDENCIA'].each do |bs|
  chile.bank_subsidiaries.create(addredd: bs)
end

 

Ahora ejecutamos nuestro seed, e iniciamos el servidor:

rake db:seed
rails s

 

[symple_heading style=»» title=»Paso 4: Mostrando las sucursales en el detalle del banco» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

Queremos que al entrar al detalle de un banco nos muestre todas las sucursales asociadas a este, por lo que tenemos que modificar en archivo /app/views/banks/show.html.erb y agregar lo que esta entre la linea 8 y 18:

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @bank %> # podemos borrar .name gracias al método to_s que definimos mas arriba
</p>

<h2>Sucursales</h2>

<% if @bank.bank_subsidiaries.any? %>
	<ul>
		<% @bank.bank_subsidiaries.each do |subsidiary| %>
		<li><%= link_to subsidiary, subsidiary %></li> # no es necesario usar .address gracias al método to_s que definimos mas arriba
		<% end %>
	</ul>
<% else %>
	<p>no hay sucursales</p>
<% end %>

<%= link_to 'Edit', edit_bank_path(@bank) %> |
<%= link_to 'Back', banks_path %>

 

[symple_heading style=»» title=»Paso 5: Modificando el formulario del modelo bank» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

En el siguiente paso modificaremos el formulario del modelo bank para que podamos agregar sucursales cuando entramos a editar el banco, para esto usaremos el Helper fields_for.

...

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <h2>Sucursales</h2>

  <%= f.fields_for :bank_subsidiaries do | subsidiary | %>
    <div class="bank_subsidiaries_fields">
      <div class="fields">
        <%= subsidiary.label :address, "Direccion" %><br>
        <%= subsidiary.text_field :address %>
      </div>
    </div>
  <% end %>

...

Ahora si entramos a editar el banco tendremos un campo para añadir una sucursal, pero no esta mostrando las sucursales que ya existen, para eso vamos a modificar nuestro modelo bank y añadiremos lo siguiente:

class Bank < ActiveRecord::Base
  has_many :bank_subsidiaries, dependent: :destroy

  accepts_nested_attributes_for :bank_subsidiaries

  validates :name, presence: true

  def to_s
    name
  end
end

Ahora si recargamos la pagina veremos que se mostraran las sucursales asociadas al banco seleccionado.

[symple_heading style=»» title=»Paso 6: Editando una sucursal» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

Si ahora tratamos de editar alguna de las sucursales nos daremos cuenta que el cambio no se aplica, y si revisamos la consola nos encontraremos con el problema:  

Unpermitted parameter: bank_subsidiaries_attributes

Lo que tenemos que hacer ahora es ir al controlador de bank y agregamos bank_subsidiaries_attributes  a los strong parametersEsta es una medida de seguridad de Rails 4.

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def bank_params
      params.require(:bank).permit(:name, bank_subsidiaries_attributes: [:id, :address])
    end
end

Si probamos nuevamente editar una sucursal ahora si se aplicaran los cambios!

 

[symple_heading style=»» title=»Paso 7: Agregar una nueva sucursal» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

En estos momentos al editar un banco se listan todas las sucursales asociadas a éste pero no tenemos como añadir una nueva sucursal. Esto lo arreglaremos fácilmente haciendo una pequeña modificación en /app/controllers/banks_controller.rb en el método edit:

... 
 # GET /banks/new
  def new
    @bank = Bank.new
  end

  # GET /banks/1/edit
  def edit
    @bank.bank_subsidiaries.build
  end

  # POST /banks
  # POST /banks.json
  def create
    @bank = Bank.new(bank_params)

    respond_to do |format|
...

Si ahora vamos a editar un banco, ademas de tener listadas las sucursales, tendremos un campo vacío para añadir una nueva. Pero si hacemos una prueba tendremos un error, ya que en nuestro modelo bank_subsidiary dijimos que no podemos tener el campo address vacío (validates :address, presence: true), esta validación no la podemos eliminar por lo que tendremos que hacer lo siguiente:

class Bank < ActiveRecord::Base
  has_many :bank_subsidiaries, dependent: :destroy

  accepts_nested_attributes_for :bank_subsidiaries,
    reject_if: proc { |attr| attr['address'].blank? }

  validates :name, presence: true

  def to_s
    name
  end
end

Lo que hicimos fue, a nuestro accept_nested_attributes_for, agregar un reject_if que comprueba si el atributo anidado esta en blanco, y si lo esta no toma en cuenta el accept_nested_attributes_for y en via el formulario sin ellos.

Ahora seria bueno que al añadir un nuevo banco tengamos la posibilidad de añadir una sucursal en el mismo formulario. Para esto hacemos un pequeño cambio en /app/controllers/banks_controller.rb en el método new:

...

 # GET /banks/new
  def new
    @bank = Bank.new
    @bank.bank_subsidiaries.build
  end

...

 

[symple_heading style=»» title=»Paso 8: Borrar una sucursal en el formulario del banco» type=»h2″ font_size=»» text_align=»left» margin_top=»30″ margin_bottom=»30″ color=»undefined» icon_left=»» icon_right=»»]

Ahora somos capaces de editar y crear sucursales en el formulario del banco. Pero no tenemos ninguna manera de poder eliminar una en este formulario. Para poder hacer esto necesitaremos agregar allow_destroy: true a nuestro accepts_nested_attributes_for:

class Bank < ActiveRecord::Base
  has_many :bank_subsidiaries, dependent: :destroy

  accepts_nested_attributes_for :bank_subsidiaries,
    reject_if: proc { |attr| attr['address'].blank? },
    allow_destroy: true

  validates :name, presence: true

  def to_s
    name
  end
end

También tenemos que modificar nuestro formulario para añadir una opción de borrar, esto lo haremos agregando un campo check_box y le pasamos como atributo  :_destroy (este se tiene que llamar así).

...

<%= f.fields_for :bank_subsidiaries do | subsidiary | %>
    <div class="bank_subsidiaries_fields">
      <div class="fields">
        <%= subsidiary.label :address, "Direccion" %><br>
        <%= subsidiary.text_field :address %>
        <%= subsidiary.check_box :_destroy %>
        <%= subsidiary.label :_destroy, "Borrar" %>
      </div>
    </div>
<% end %>

...

Y obviamente tenemos que agregar este atributo en los strong parameter del controlador:

...

 # Never trust parameters from the scary internet, only allow the white list through.
    def bank_params
      params.require(:bank).permit(:name, bank_subsidiaries_attributes: [:id, :address, :_destroy])
    end

...

Ahora si podemos agregar, editar y eliminar sucursales desde el mismo formulario del banco!!!

Y ya hemos terminado con nuestro tutorial, a continuación les dejo unos links para profundizar en el tema.

  • Link al repositorio de este tutorial en github: Nested Forms
  • Mas sobre Nested Forms en este link
  • Si quieres aprender mas sobre Nested Attributes mira este link