#17 HABTM Checkboxes
En nuestra aplicación de ejemplo para este episodio, tenemos dos modelos (Product
y Category
), cada uno con una relación has_and_belongs_to_many
(tiene_y_pertenece_a_muchos) que se corresponde con el otro modelo. Nos gustaría poder editar las categorías en las que se encuentra un producto utilizando una lista de checkboxes en el formulario de edición para ese producto.
Agregar checkboxes
Actualmente, el formulario para editar nuestro producto, tiene campos para el nombre y el precio del producto, pero no hay forma de editar las categorias en las que se encuentra un producto. Existen dos formas para agregar un checkbox a un formulario. Utilizando check_box
o check_box_tag
. Vamos a utilizar check_box_tag
, ya que nos brinda el control que necesitamos sobre el nombre del checkbox. El código que agregaremos al formulario de edición (products/edit.html.erb
) se muestra debajo.
<% for category in Category.find(:all) %> <div> <%= check_box_tag "product[category_ids][]", category.id, @product.categories.include?(category) %> <%= category.name %> </div> <% end %>
check_box_tag
toma tres parámetros: el nombre del checkbox, el valor del checkbox y un valor booleano que determina si el checkbox debe estar marcado. El nombre que hemos utilizado se ve un poco extraño, pero muy pronto entenderá porqué se llama de esta forma. Para determinar si cada checkbox de categoría debe ser marcado o no, debemos verificar si el producto se encuentra dentro de esa categoría utilizando @product.categories.include?(category)
.
Veamos si funciona
El formulario de edición con los checkboxes agregados.
Si actualizamos el formulario y lo probamos, veremos que funciona y las categorías del producto son actualizadas cuando marcamos alguno de los checkboxes y clickeamos 'submit'. Cómo sabe Rails como actualizar el producto correctamente?. El log de desarrollo tiene la respuesta.
Processing ProductsController#update (for 127.0.0.1 at 2009-01-15 20:57:56) [PUT] Parameters: {"commit"=>"Edit", "authenticity_token"=>"31b711f2c24ae7cea5abf3f758eef46b472eebf3", "product"=>{"price"=>"99.0", "name"=>"Television Stand", "category_ids"=>["2", "4"]}, "id"=>"1"}
Cuando el formulario es enviado, pasa los parámetros de categoría en el hash de producto como un array. Hace esto, debido el nombre que le asignamos a los checkboxes (product[category_ids][]
). La primera parte del nombre le indica a Rails que pase las ids de categoría (category_id
) como parte del hash de producto, mientras que los corchetes vacíos le indican que pase los valores como un array. Pero, ¿dónde se encuentra el método category_ids
, al que llamamos cuando los parámetros del producto son actualizados? La respuesta es que es generado por el método has_and_belongs_to_many
en el modelo Product
. Podemos probar esto abriendo script/console
y actualizar un producto manualmente.
>> p = Product.first => #<Product id: 1, name: "Television Stand", price: 99.0, created_at: "2009-01-11 21:32:12", updated_at: "2009-01-11 21:32:12"> >> p.category_ids => [2, 3] >> p.category_ids = [1,4] => [1, 4] >>
Al hacer esto, Rails genera el siguiente SQL para actualizar las categorías del producto:
DELETE FROM "categories_products" WHERE product_id = 1 AND category_id IN (2,3) INSERT INTO "categories_products" ("product_id", "id", "category_id") VALUES (1, 1, 1) INSERT INTO "categories_products" ("product_id", "id", "category_id") VALUES (1, 4, 4)
Una pequeña advertencia
Existe aún un pequeño problema con nuestro método update. Si desmarcamos todos los checkboxes para retirar al producto de todas las categorías, entonces la actualización fallará en retirar de cualquier categoría en la que se encontrase el producto. Esto se debe a que el valor de un checkbox en un formulario HTML no será enviado si es deseleccionado y por lo tanto ninguna id de categoría van a aparecer en el hash de parámetros del producto, lo que lleva a que las ids de categoría no serán actualizadas.
Para solucionar este problema debemos modificar nuestro controlador de productos para configurar el parámetro category_id
para que sea un array vacío cuando no sea pasado a la acción update. Podemos hacer esto utilizando el operador ||=
de Ruby y agregar lo siguiente a la acción update.
params[:product][:category_ids] ||= []
Esto nos asegura que si ningún checkbox es marcado, indicando que no se encuentra en ninguna categoría, entonces el producto se actualizará correctamente.