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 parameters. Esta 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