viernes, 28 de septiembre de 2007

Crear listas dependientes con Ajax en Symfony. Segunda Parte.

En el penúltimo post abordé el tema de como manejar una lista dependiente usando Ajax. El ejemplo planteado es muy efectivo cuando tenemos una lista que depende de otra. Pero, que pasa cuando existe otra lista que a su vez dependa de esta última lista en cuestión, por ejemplo, tener la lista de municipios, que dependa de una lista de estados, que a su vez dependa de una lista de paises. A continuación veremos como con unas pequeñas variaciones del dicho ejemplo podemos conseguir esto, e incluso, poder hacer listas de n niveles de dependencia.

La Dirección de una Persona.

Para el ejemplo vamos a suponer que tenemos un modelo con 4 tablas; país, estado, municipio y persona, donde el registro de la persona contiene un referencia a las otras 3 tablas.
Para cada una de ellas vamos a crear un módulo administrativo.

symfony propel-init-admin test pais Pais
symfony propel-init-admin test estado Estado
symfony propel-init-admin test municipio Municipio
symfony propel-init-admin test persona Persona

  • Los componentes
Aprovechando la potencia que ofrecen los componentes, vamos a crear 3 de ellos, uno para país, otro para el estado y otro para el municipio. Los componentes para el Estado y Municipio reciben hasta 3 parámetros; el nombre que tendrá el objeto select a nivel de formulario, el id seleccionado por defecto, y el id del objeto padre para filtrar la búsqueda ( por ejemplo, el componente del municipio recibe el id del estado para filtrar solo los municipios que pertenecen a ese estado). En el caso del país, recibe solo 2 parámetros, el nombre del objeto select y el id del país seleccionado por defecto (El componente del municipio es el mismo que se mostró en el post anterior).
A continuación se muestra la parte de la lógica de negocio del componente de país:

<?php
class paisComponents extends sfComponents
{
public function executeSelectAll()
{
$this->paises = PaisPeer::doSelect(New Criteria());
}
}

Y acá el parcial _selectAll.php:

<?php echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($paises),
isset($id_pais)?$id_pais:''),array());?>

Ahora el componente en el módulo estado:

<?php
class estadoComponents extends sfComponents
{
public function executeSelectByPais()
{
$this->estados = EstadoPeer::doSelectByPais($this->id_pais);
}
}

El parcial _selectByPais.php del componente.

<?php
echo select_tag($nombre,options_for_select(array(''=>'Seleccione')+
_get_options_from_objects($estados),
isset($id_estado)?$id_estado:''),array());?>

  • Las Acciones que se llamarán vía Ajax.

Para el ejemplo que nos compete necesitamos tener 2 acciones a ejecutar vía Ajax: Una que se ejecutará al seleccionar un país, y que debe actualizar la lista de estados, y otra que se ejecutará al seleccionar un estado y que debe actualizar la lista de municipios.
Hay un punto muy importante a tener en consideración. Como vimos en el post previo relacionado con este caso, a cada lista de la que dependa otra lista le tenemos que crear un observador. En este caso tenemos que crear un observador al campo que selecciona el país y otro al que selecciona el estado. En este punto en donde la cosas se complican un poco. Haciendo un poco de memoria debemos recordar que no podemos actualizar el objeto select de manera directa, esto por un bug presente en Internet Explorer, y que nos obligo a colocar el objeto select dentro de un span y "recrearlo" completamente vía Ajax. Volviendo al ejemplo, creamos un acción llamada listByPais en el módulo de Estado, y una acción llamada listByEstado en el módulo municipio, y siguiendo el ejemplo del post previo creamos la vistas respectivas, donde se llaman a los componentes ya descritos; procedemos a crear los parciales _id_pais.php , _id_estado.php y _id_municipio.php en el módulo de personas, donde se llaman a los componentes de cada campo y colocando además un observador al campo país y otro al campo estado. Editamos el generator.yml especificando los parciales y ejecutamos desde el navegador el modulo persona, llamando la acción create. Seleccionamos el país, y la lista de estados se actualiza perfectamente, pero cuando seleccionamos un estado, la lista de municipios no se actualiza. Si tienes una herramienta como firebug instalada observarás que la llamada a la acción municipio/listByEstado no se ejecutó. Por qué?, porque al seleccionar el país, el objeto estado fue literalmente "borrado y creado nuevamente", por lo que el observador "pierde la conexión" con dicho objeto. Que hacemos?, como solventamos esta falla?. Personalmente conozco 4 formas de resolver este problema en Symfony, pero para nuestro ejemplo voy a explicar la que considero más al "estilo symfony". Para este caso la acción estado/listByPais no nos sirve, esta solamente es útil para casos donde no exista una lista que dependa de la que el genera. Procedemos entonces a crear en el modelo persona una nueva acción llamada listEstadosByPais:

<?php
class personaActions extends autopersonaActions
{
public function executeListEstadosByPais()
{
}
}


Ahora en la vista listEstadosByPaisSuccess.php vamos a llamar al componente selectByPais del módulo estado, y adicionalmente, vamos a crear nuevamente el observador a dicho campo.


<?php use_helper('Object','Javascript') ?>

<?php include_component('estado','selectByPais',array(
'id_pais' => $sf_request->getParameter('id_pais'),
'nombre' => 'persona[id_estado]',
));
?>
<?php echo observe_field('persona_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value + '&nombre=persona[id_municipio]'",
))
?>

ahora vamos al parcial _id_pais.php y lo vamos a cambiar de la siguiente manera:

<?php include_component('pais','selectAll',array(
'id_pais'=>$persona->getIdPais(),
'nombre'=>'persona[id_pais]',
));?>
<?php echo observe_field('persona_id_pais',array(
'update' => 'estado',
'url' => 'persona/listEstadosByPais',
'with' => "'id_pais=' + value",
'script' => true,
)) ?>

Como vemos, ahora el observador llamará a la acción que acabamos de crear. Aparte le configuramos la acción script en true. Esto es para que cuando se haga el llamado a la acción persona/listEstadosByPais, el código javascript que crea el observador sea ejecutado.
Así, cuando ejecutemos nuevamente la acción create del módulo persona, al seleccionar un país, se regenerá la lista de estados, y se recrea el observador del mismo.
Anexo un link para descargar el proyecto completo, que incluye además un ejemplo de como utilizar listas dependientes para filtrar (en el módulo municipio, al listar, puede filtrarse por pais y estado).

En un próximo post estaré hablando de como hacer consultas complejas a nivel del modelo explotando las bondades del objeto criteria.

domingo, 16 de septiembre de 2007

De Vacaciones

Si bien en mi primer post ofrecí por lo menos una entrada semanal en el blog enfocado primordialmente a Symfony, desde el primero de este mes estoy de vacaciones, el el Estado Merida, acá en Venezuela, recorriendo los distintos pueblos de la región en compañia de mi esposa e hija, y donde la conexión a internet es casi nula. Tengo como punto de honor colocar un post que sería la continuación del post sobre las listas dependientes actualizadas vía Ajax, para el cual pienso colocar los códigos fuentes del ejemplo. Regresó a la rutina el día 25 de este mes, por lo tanto espero escribir el post antes de que el mes finalice.