jueves, 25 de septiembre de 2008

Selects dependientes en CakePHP: la manera bruta, toma 1

Vaya por delante que soy malísimo con Javascript. Sé que hay mejores maneras de hacer esto de los selects dependientes (de hecho, ya se me está ocurriendo alguna).

Allá va:

Básicamente se trata de tener una acción en un Controller que obtenga los datos necesarios para el select "dependiente" y cuya vista sea (redoble de tambores)... el nuevo campo select con los valores adecuados y llamarla mediante Ajax.

Pondré unos ejemplos de un proyecto en que estoy ahora. (Para ponerte en contexto se trata de un sistema de publicación. Cuando estás editando un post puedes asignarlo a un Blog y, a su vez, puedes asignarlo a una Serie dentro de ese Blog (en ambos casos con selects). Cada Blog tiene distintas Series, por lo que al escoger un Blog en su select, el select de Series tiene que cambiar. Espero haberme explicado.)

Bien, en mi caso la acción que genera el select será

/series/select


La vista padre es el archivo /views/post/admin_edit.ctp en el que tengo...


echo $form->input('blog_id', array(
'label' => __('Blog to publish this post', true),
'empty' => __('select a blog', true),
)
);
if (isset($series)) {
echo $form->input('series_id', array(
'label' => __('Is this post part of a series?', true),
'empty' => __('No', true),
'div' => array(
'id' => 'PostSeriesDiv'
)
)
);
}
echo $ajax->observeField('PostBlogId', array(
'frequency' => '1',
'update' => 'PostSeriesDiv',
'url' => array(
Configure::read('Routing.admin') => false,
'controller' => 'series',
'action' => 'select'
)
)
);
En el fragmento anterior se pueden ver los dos selects implicados y el uso de $ajax->observeField para generar un script cuya función es controlar los cambios del primer select. Por supuesto, al principio de la vista me aseguro de cargar la biblioteca prototype.js

echo $javascript->link('prototype', false);

En la vista "padre" (el formulario donde están los selects) necesitas un poco de Javascript para llamar a esa acción. Yo lo he hecho con observeField. Este método del Ajax Helper pide como parámetros una frequency en segundos para observar el campo, una url de la acción destino, un update, para saber dónde colocar el nuevo contenido y un último parámetro with para indicar los parámetros que se han de pasar en la petición.

(aquí es dónde creo que se podría hacer algo mejor con un evento onChange, pero de momento no me pidas más, lo dejaré para la toma 2)

¿Cómo se pasa el valor del Select "padre" a la acción para poder obtener las opciones? Muy sencillo: observeField por defecto serializa el campo observado en el parámetro with y tú puedes recoger esos parámetros en la acción destino en el array $this->data['Model']['field']. Si, exactamente así. Esta es mi acción receptora, tal como la tengo en /controllers/series_controller.php

function select() {
$this->set('series', $this->Series->getByBlog($this->data['Post']['blog_id']));
}


Debido a la forma en que se actualizan los elementos, hay que tomar unas precauciones con la construcción de los selects.

En primer lugar al select en la vista maestra hay que ponerlo en una DIV con un id único pues el elemento select en sí no se puede actualizar con una petición Ajax. Suponiendo que uses $form->input() para generar el campo, basta con poner el parámetro "div" en las opciones y ponerle un "id".

En la vista devuelta por Ajax, tienes que poner el mismo parámetro "div" en null, para que no se nos duplique. Tal que así:

/views/series/select.ctp
<?php
echo $form->input('Post.series_id', array(
'options' => $series,
'div' => null, // Esto elimina el div contenedor
'label' => __('Is this post part of a series?', true),
'empty' => __('No', true)
)
);
?>

9 comentarios:

floydbrush dijo...

¡Hola! Antes de nada, debo decirte que tu blog me ha sido de ayuda muchas veces, está bien acceder a información sobre cakephp en español.

Dicho esto, comentas que el elemento select en si, no se puede actualizar con una petición AJAX. No es del todo cierto, poder se puede haciendo lo mismo con una vista como:

foreach($series as $k => $v) {
echo '< option value="'.$k.'">'.$v.'< /option>';
}


Sin embargo no se puede usar el helper, por lo que yo acostumbro a optar siempre por tu opción y actualizar una div con el select.

¡saludos!

Frankie dijo...

Soy un paquete en Javascript, por lo que los Helpers de Cake me vienen de maravilla.

Gracias por el comentario.

Miguel dijo...

muy buen articulo, pero quizas como no esta el codigo completo no entiendo algunas cosas:
-como llenas el primer select?, No veo la linea options en el array... asumo que en otro lado lo llenas y omitistes ponerlo
???

Miguel dijo...

yo segui tu ejemplo y lo que me aparece en el primer select es un textbox....

Frankie dijo...

@Miguel, en este ejemplo el PostsController tendría un método admin_edit() en el que se llena la variable $blogs con los datos correspondientes y se pasa a la vista con $this->set(compact('blogs')).

El campo obtiene las opciones gracias a la "automagia" de CakePHP que al tener un campo blog_id y una variable de la vista llamada $blogs sabe que debe mostrar un campo desplegable y tomar las opciones de la variable $blogs.

Anónimo dijo...

Antes de nada, agradecer el artículo.
Está genial.
Estoy haciendo un ejemplo en el que intento hacer lo mismo pero con dos observeField. El primer select funciona y actualiza el segundo. En el segundo intento actualizar un div pero ni siquiera me ejecuta la función de update_select. No sé la razón, si tienes alguna idea te lo agradezco

Frankie dijo...

@Anónimo: No acabo de ver qué es lo que quieres hacer exactamente.

Te aconsejo que crees los DIV en la vista con el Ajax Helper y controla que los scripts no vayan a estar dentro de una de ellas.

juan dijo...

Antes de nada disculpame por haber puesto un post como anónimo y además haberme explicado mal.

Tengo dos selects . En el primer select según el valor que selecciono se cargan unos datos u otros en el segundo select. Para ello como bien has explicado en tu ejemplo pongo un observeField... Ahora quiero que al seleccionar el segundo según ese valor, haga una consulta y rellene un div. Pongo otro observefield; pero en este select cuando elijo un valor no me entra en la función update_select2...
Y no sé qué puede pasar porque es igual que el primero, o eso creo ...
Un saludo y gracias de nuevo por compartir tus conocimientos

Frankie dijo...

@Juan, no controlo lo suficiente del tema (estoy muy verde en JS y Ajax) pero creo que parte del problema es que el segundo observeField tiene que operar sobre un elemento generado por javascript y no funciona igual. Me suena haber visto algo en el grupo Google de CakePHP en inglés y algún articulo sobre cómo manejar esa situación, aunque tendría que buscarlo porque no guardé esos enlaces.

Gracias por pasarte por aquí.