martes, 28 de agosto de 2007

Crear listas dependientes con Ajax en Symfony

Trabajar con Ajax puede ser un verdadero dolor de cabeza. gracias a Dios Symfony trae integrado prototype y un buen numero de helpers que facilitan el trabajo a la hora de hacer páginas que requieran actualización dinámica utilizando esta tecnología. Un tópico recurrente es el de las listas dependientes (ejemplo: Estado-Municipio-Parroquia). Existen varios mecanismos para hacer este tipo de Ajax. El que presentó a continuación tiene la particularidad que puede ser usado para aplicaciones que se codifiquen "a mano" o generadas vía admin_generator, lo cual hace que el ćodigo sea muy reusable.
  • El modelo.
En el modelo, especificamente en la clase Peer, es recomendable crear un método que recupere los objetos asociados a la clave foranea, por ejemplo, recuperar los municipios que pertenecen a un estado en particular.


<?php
class MunicipioPeer extends BaseMunicipioPeer
{
static public function doSelectByEstado($estado)
{
$c= new Criteria();
$c->add(MunicipioPeer::ID_ESTADO ,$estado);
return MunicipioPeer::doSelect($c);
}
}


  • El componente.
Ahora nos vamos al modulo respectivo del modelo, en este caso municipio y creamos un componente. En nuestro ejemplo, en la parte de la lógica de negocio del componente se generará el arreglo de objetos de tipo municipio asociados a un estado en particular, y en el parcial se creará el objeto select a partir de dicho arreglo. Para que el componente sea lo más reusable posible, el mismo debe recibir 3 parametros: el id de la clave foranea (el id del estado ), que se utiliza para hacer la búsqueda en el modelo, el nombre que llevará el objeto select al generar en el parcial, y el valor por defecto que tendrá dicho objeto. A continuación se muestra la parte de la lógica de negocio del componente:


<?php
class municipioComponents extends sfComponents
{
public function executeSelectByEstado()
{
$this->municipios = MunicipioPeer::doSelectByEstado($this->id_estado);
}
}


El parcial _selectByEstado.php de el componente sería el siguiente:



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


En el parcial observamos las variables comentadas previamente ($nombre para el nombre del objeto select, $municipios, el arreglo de municipios que se generó previamente en la parte de la lógica de negocio del componente y $id_municipio, que es el valor seleccionado por defecto).
  • Usando el componente en un modulo generado con admin generator.
En el ejemplo supondremos que tenemos una tabla llamada medio, la cual tiene un id_estado y un id_municipio, las cuales son claves foráneas contra las respectivas tablas de estado y municipio.
generamos el modulo medio con el admin generator y en el generator.yml indicamos que queremos capturar en la pantalla de edición del medio el estado y municipio del medio.

edit:
title: REGISTRO DE MEDIOS
display: [nom_medio, id_estado, id_municipio .....]


Al ejecutar la acción edit del modulo medio veremos que se listan todos los municipios, sin filtro por estados. Lo primero que vamos a hacer es colocar que por defecto, cuando se ejecute la acción los municipios salgan filtrados. Para eso vamos a crear un parcial _id_municipio.php en el módulo de medios y en ese parcial llamamos al componente que creamos previamente.



<span id="municipio">
<?php include_component('municipio','selectByEstado',array(
'id_estado'=>$medio->getIdEstado(),
'nombre'=>'medio[id_municipio]',
'id_municipio'=>$medio->getIdMunicipio(),
));
?>
</span>


Tal vez surja la pregunta de por qué el select se colocó dentro de un span, pero más adelante aclararé dicha duda. Ahora vamos a proceder a establecer que la lista de municipio se actualice via Ajax cada vez que se seleccione un estado distinto. Para ello vamos a hacer un parcial llamado _id_estado.php para el select del estado y lo colocaremos un observador, que dispare una acción ajax cada vez que cambie el mismo:




<?php $value = object_select_tag($medio, 'getIdEstado', array (
'related_class' => 'Estado',
'control_name' => 'medio[id_estado]',
'include_custom' => 'Seleccione',
)); echo $value ? $value : ' ' ?>

<?php echo observe_field('medio_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value+'&nombre=medio[id_municipio]'",
)) ?>

Note que en el helper observe_field, se utiliza el id del objeto select, y no el nombre. El id lo puedes ubicar observando el código fuente de la página que genera la acción, o lo puedes deducir del nombre. Adicionalmente el helper observe_field hace una llamada asíncrona a una acción en particular, en nuestro ejemplo esa acción esta en el módulo municipio y se llama listByEstado. A esta acción se le pasarán 2 parámetros, que son el id del estado y el nombre del objeto a generar.
  • Creando la acción del Ajax.
En el módulo municipio creamos la acción listByEstado. Se puede hacer de 2 maneras: dejar la acción en blanco y en la vista listByEstadoSuccess.php llamar al componente selectByEstado creado previamente, o en el acción generar el arreglo de municipios y en la vista solo llamar al parcial. La primera opción, es muy simple y se deja al lector. El segundo ejemplo sería el siguiente:
En actions.class.php.


<?php
class municipioActions extends autoMunicipioActions
{
public function executeListByEstado()
{
$this->municipios = MunicipioPeer::doSelectByEstado($this->getRequestParameter('id_estado'));
}



en la vista listByEstadoSuccess.php:


<?php use_helper('Object') ?>
<?php include_partial('selectByEstado',array(
'municipios'=>$municipios,
'nombre'=>$sf_request->getParameter('nombre'),
));
?>


Solo queda indicar en el generetor.yml de medio que utilize los 2 parciales que creamos.


edit:
title: REGISTRO DE MEDIOS
display: [nom_medio, _id_estado, _id_municipio .....]


A partir de ahora, cuando se ejecute el la acción edit del modulo medio, si estamos editando un registro nuevo, veremos la lista de municipios blanco la primera vez, y si es un registro existente, veremos la lista de municipios relacionadas con el id del estado del registro. En cualquiera de los casos, al seleccionar un nuevo estado, automáticamente se actualizará vía ajax el span municipio con una nueva lista de municipios, basados en el estado seleccionado.
  • Algunas consideraciones.

  1. Por que actualizar un span y no actualizar el objeto select directamente?. Es posible hacerlo, solo bastaría con indicar al observer que el update lo haga directamente al objeto DOM medio_id_municipio, y en la vista de la acción Ajax usar solamente el helper options_for_select para que solo se genere el código html de las opciones. Esto funcionaría perfectamente en Firefox, pero no en internet explorer, ya que este navegador tiene un bug que hace que falle cualquier intento de actualizar un objeto select vía javascript con un innerHTML.
  2. Se puede hacer que la acción listbyEstado del módulo municipio solo se ejecute si se llama via Ajax colocandole una condicion if (!$this->getRequest()->isXmlHttpRequest()) en el ćodigo de la acción.
  3. Se le pueden colocar efectos a la llamada realizada por el observer_field (desvanecimiento, condiciones en caso de que la acción genere un error, entre otras).
  • Y que pasa si tengo un subnivel adicional?.
Y que pasa si tengo un campo, por ejemplo, la parroquia, que depende del municipio seleccionado. Bueno, el código que hemos creado, con algunas pequeñas modificaciones, nos servirá perfectamente, en el siguiente tópico detallaremos esas modificaciones

11 comentarios:

Anónimo dijo...

Mmm muy interesante. Ahora que ya estoy terminando mi proyecto seguramente lo voy a usar para mejorar algunos forms que tengo.

Gracias por el apunte ;-D

jn dijo...

La verdad no entendí mucho. Estoy utilizando symfony en un proyecto que empezó hace 3 días, osea que soy muy nuevo,la verdad no sé en qué archivos pegar el código que das de ejemplo y cuando se crean nuevos archivos no sé en qué carpeta van .... :(

Boris Duin dijo...

Juancho:

Te recomiendo leer el post que es la continuación de este que incluye un link a una copia del proyecto completo para descargar :)

Unknown dijo...

Estoy interesado en este tipo de tecnologia, ya que podemos crear proyecto en un tiempo mas corto, pregunto ? hay una guia o una pagina donde se enceuntre material de symfony, yo estoy trabjando en linux. Gracias y exitos para todos

Unknown dijo...

Hey
He intentado por TRES dias probar el código y chamo la verdad es que no me corre, me dice que no encuentra el template _id_estado. Podrias decir donde (la dirección exacta) van cada uno de los archivos porfa? Gracias de antemano

Anónimo dijo...

Que tal Daniel, puedes encontrar toda la documentacion oficial y ejemplos usando symfony en www.symfony-project.org

Saludos.

Anónimo dijo...

Que tal.
Muy agradecido con el aporte, pero seria bueno que atendieras todas las sugerencias que te hacemos, como por ejemplo, colocar especificamente en que directorios van cada uno de los trozos de codigo que nos muestras, sería de gran ayuda porque por lo que veo a ninguno de los que estamos necesitando este código nos funciona. Gracias de verdad...

tobelindo dijo...

¡Hola!

Me parece que hay un error en el observe_field. Donce dice:

observe_field('medio_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value+'&nombre=medio[id_municipio]'",
))

Debería decir:
observe_field('medio_id_estado',array(
'update' => 'municipio',
'url' => 'municipio/listByEstado',
'with' => "'id_estado=' + value+'&nombre=medio[id_estado]'",
))

Jose Vasquez dijo...

por favor necesito su ayuda....!!!! necesito hacer 4 listas anidadas... de nivel 4 por fa!!!! alguien me puede ayudar ??? hasta de nivel 3 da bn!!!! PORFAVORRR!!!!1 gracias de antemano

jean dijo...

Puedes Hacer este mismo ejemplo de los combos anidados en symfony2 ???

Anónimo dijo...


ante todo saludos ....en estos momentos estoy presentando problemas con un
combobox independiente , ....en este lo q quiero hacer es q cuando yo quiero
seleccionar un medio para darle salida fuera de la unidad y ne me salgan los
q estes registrado en la tabla de perdida o sea Perdidos.......



->add('medio', 'entity', array('class' => 'DepartamentoBundle:Medio',
'label' => 'Medio',
'empty_value' => '--Seleccione el Medio--',
'query_builder' => function($repository) {
$qb = $repository->createQueryBuilder('c');

$qb->where(" c.id NOT IN (' Select Perdida.id_medio
from Perdida ') ");


return $qb;
}))

en si el codigo no me da ningun error pero cuando voy al combobox a
seleccionar un medio me salen todos los medios incluyendo los q estas
perdidos.....he mirado ejemplos en internet y hacen algo igual a
esto...saludos de jorge
este es mi correo
jorge.cruz@inf.fie.uo.edu.cu