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.

3 comentarios:

Anónimo dijo...

Hola:
en 1 lugar te felicito por tus aportes a cerca de symfony y propel.
en 2 lugar te solicito ayuda: no puedo hacer correr propel en windows.
tengo los tres paquetes:propel-1.2.1.zip, phing-2.3.0.zip,creole-1.1.0.zip.
los descomprimi en C:/wamp/www/ y confogure los .bat como sigue a continuacion:
-----------------------------------------------------
phing.bat
============

if "%OS%"=="Windows_NT" @setlocal

rem %~dp0 is expanded pathname of the current script under NT
set DEFAULT_PHING_HOME=%~dp0..

goto init
goto cleanup

:init

if "%PHING_HOME%" == "" set PHING_HOME=%DEFAULT_PHING_HOME%
set DEFAULT_PHING_HOME=

if "%PHP_COMMAND%" == "" goto no_phpcommand
if "%PHP_CLASSPATH%" == "" goto set_classpath

goto run
goto cleanup

:run
%PHP_COMMAND% -d html_errors=off -qC %PHING_HOME%\bin\phing.php %1 %2 %3 %4 %5 %6 %7 %8 %9
goto cleanup

:no_phpcommand
REM echo ------------------------------------------------------------------------
REM echo WARNING: Set environment var PHP_COMMAND to the location of your php.exe
REM echo executable (e.g. C:\PHP\php.exe). (Assuming php.exe on Path)
REM echo ------------------------------------------------------------------------
set PHP_COMMAND=C:\wamp\php\php.exe
goto init

:err_home
echo ERROR: Environment var PHING_HOME not set. Please point this
echo variable to your local phing installation!
goto cleanup

:set_classpath
set PHP_CLASSPATH=%PHING_HOME%\classes;C:\wamp\www\creole\classes
goto init

:cleanup
if "%OS%"=="Windows_NT" @endlocal
REM pause

--------------------------------------------------------------------------
propel-gen.bat
================
@echo off

rem *********************************************************************
rem ** The Propel generator convenience script for Windows based systems
rem ** $Id$
rem *********************************************************************

rem This script will do the following:
rem - check for PHING_COMMAND env, if found, use it.
rem - if not found detect php, if found use it, otherwise err and terminate
rem - check for PROPEL_GEN_HOME evn, if found use it
rem - if not found error and leave

if "%OS%"=="Windows_NT" @setlocal

rem %~dp0 is expanded pathname of the current script under NT
set DEFAULT_PROPEL_GEN_HOME=%~dp0..

goto init
goto cleanup

:init

if "%PROPEL_GEN_HOME%" == "" set PROPEL_GEN_HOME=%DEFAULT_PROPEL_GEN_HOME%
set DEFAULT_PROPEL_GEN_HOME=C:\wamp\www\propel\generator

if "%PHING_COMMAND%" == "" goto no_phingcommand

goto run
goto cleanup

:run
%PHING_COMMAND% -f %PROPEL_GEN_HOME%\build.xml -Dusing.propel-gen=true -Dproject.dir=%*
goto cleanup

:no_phingcommand
REM echo ------------------------------------------------------------------------
REM echo WARNING: Set environment var PHING_COMMAND to the location of your phing
REM echo executable (e.g. C:\PHP\phing.bat).
REM echo Proceeding with assumption that phing.bat is on Path
REM echo ------------------------------------------------------------------------
set PHING_COMMAND=C:\wamp\www\phing\bin\phing.bat
goto init

:cleanup
if "%OS%"=="Windows_NT" @endlocal
REM pause

------------------------------------------------------------------------------------

el error que me sale es que no puedo o no se como crear proyectos, cuando hago: propel-gen -bookstore me crea el build para el proyecto pero solo me crea el config. A las clases y el sql no los genera..

Ayuda please

Anónimo dijo...

Hola me gustaria que explicarais como se hace esto pero con doctrine, un saludo.
Mi correo: ticcaje@gmail.com

Anónimo dijo...

A comprehensive suitableness program tailored to an person resolve very likely nave on harmonious or more delineated skills, and on age-[3] or health-related needs such as bone health.[4] Many sources[citation needed] also cite mental, common and nervous fettle as an substantial vicinity of whole fitness. This is often presented in textbooks as a triangle made up of three points, which show true, nervous, and loony fitness. Incarnate good shape can also hamper or handle various inveterate health conditions brought on next to insalubrious lifestyle or aging.[5] Working out can also refrain from people snore better. To stay sturdy it is mighty to preoccupy in physical activity.
Training

Unequivocal or task-oriented [url=http://www.pella.pl]fitness[/url] is a living soul's gifts to fulfil in a definite activity with a tolerable know-how: after sample, sports or military service. Spelled out training prepares athletes to respond fully in their sports.

Examples are:

400 m sprint: in a sprint the athlete should be trained to master-work anaerobically from one end to the other of the race.
Marathon: in this specimen the athlete must be trained to work aerobically and their perseverance have to be built-up to a maximum.
Many blazing fighters and police officers undergo unvarying good physical condition testing to end if they are qualified of the physically hard tasks required of the job.
Members of the Collective States Army and Army Nationalist Guard should be proficient to pass the Army Physical Good shape Test (APFT).