sfDependentSelectPlugin - 0.1.3

Widgets for dependents selects and action for automatize AJAX calls.

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


« Back to the Plugins Home

Signin


Forgot your password?
Create an account

Tools

Stats

advanced search
Information Readme Releases Changelog Contribute
Show source

sfDependentSelectPlugin

Un plugin para enlazar selects de una forma simple y elegante

$this->widgetSchema['pais_id'] = new sfWidgetFormDoctrineChoice(array(
    'model'   => 'Pais'
));
$this->widgetSchema['provincia_id'] = new sfWidgetFormDoctrineDependentSelect(array(
    'model'   => 'Provincia', 
    'depends' => 'Pais'
));

Nota: Documentación original en español disponible en doc/README_es.

Características

  • Modo estático y modo AJAX con módulo automático (respetando add_empty, {peer|table}_method, order_by, etc),

  • Permite enlazar listas de array con listas de modelos o entre modelos (niveles ilimitados),

  • Soporte para Doctrine y Propel,

  • Fácil de personalizar y extender.

Instalación

  • Instalar el plugin

    $ php symfony plugin:install sfDependentSelectPlugin
    
  • Publicar javascript

    $ php symfony plugin:publish-assets
    
  • Limpiar la cache

    $ php symfony cache:clear
    
  • Habilitar el plugin en la configuración del proyecto (config/ProjectConfiguration.class.php)

    $this->enablePlugins(..., 'sfDependentSelectPlugin');
    
  • Habilitar el módulo sfDependentSelectAuto si deseas utilizar ajax automatizado (apps/{tuapp}/config/settings.yml)

    all:
      .settings:
        ...        
        enabled_modules:        [sfDependentSelectAuto]
    

Dependencias

  • jQuery: solamente si utilizas AJAX. Debes agregarla tú en caso necesites esta funcionalidad.

Contenido

Widgets

  • sfWidgetFormArrayDependentSelect: despliega un select utilizando como fuente de datos un array,
  • sfWidgetFormDoctrineDependentSelect: utiliza Doctrine como fuente de datos,
  • sfWidgetFormPropelDependentSelect: utiliza Propel como fuente de datos.

Actions

  • sfActionsDependentSelect: implementa una acción "_ajax" que devuelve los valores necesarios para cada petición.

Modules

  • sfDependentSelectAuto: es el módulo utilizado por defecto por los widgets, simplemente extiende a sfActionsDependentSelect.

Ejemplos de uso

Los ejemplos son para Doctrine, pero puedes hacerlo de igual forma para propel utilizando la clase sfWidgetFormPropelDependentSelect.

Pais, provincia y ciudad

Típico caso en que se tiene paises, provincias y ciudades relacionadas. Al momento de crear una dependencia a la ciudad desde otra entidad, se crea un gran problema a la hora de mostrar el formulario, ya que aparecen todas las ciudades de todas las provincias de todos los paises mezclados.

A continuación, la solución. Se hará con una persona que vive en una ciudad.

  • config/doctrine/schema.yml:

    Pais:
      columns:
        nombre: string(45)
    
    Provincia:
      columns:
        pais_id: integer
        nombre: string(45)
      relations:
        Pais:
          foreignAlias: Provincias
    
    Ciudad:
      columns:
        provincia_id: integer
        nombre: string(45)
      relations:
        Provincia:
          foreignAlias: Ciudades
    
    Persona:
      columns:
        ciudad_id: integer
        nombre: string(45)
      relations:
        Ciudad:
          foreignAlias: Personas
    
  • lib/form/doctrine/PersonaForm.class.php:

    class PersonaForm extends BasePersonaForm
    {
        public function configure()
        {
            // widgets
    
            $this->widgetSchema['pais_id'] = new sfWidgetFormDoctrineChoice(array(
                'model'     => 'Pais',
                'add_empty' => 'Seleccione país',
            ));
    
            $this->widgetSchema['provincia_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'     => 'Provincia', 
                'depends'   => 'Pais',
                'add_empty' => 'Seleccione provincia',
            ));
    
            $this->widgetSchema['ciudad_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'     => 'Ciudad', 
                'depends'   => 'Provincia',
                'add_empty' => 'Seleccione ciudad',
            ));    
    
            // siempre el orden de los selects tienen que ser según la dependencia.
            // es decir, primero pais > provincia > ciudad
            $this->widgetSchema->moveField('ciudad_id', 'after', 'provincia_id');
    
            // validadores
    
            $this->validatorSchema['pais_id'] = new sfValidatorDoctrineChoice(array(
                'model' => 'Pais',
            ));
    
            $this->validatorSchema['provincia_id'] = new sfValidatorDoctrineChoice(array(
                'model' => 'Provincia',
            ));          
    
            // el validador de 'ciudad_id' está en BasePersonaForm    
        }
    }
    

Listo, con eso ya debería funcionar. Se mostrará el select "independiente" para paises, y de acuerdo al valor de éste serán las provincias mostradas. De igual forma para con las ciudades según la provincia.

Cabe destacar que el ejemplo anterior no hace llamadas remotas (AJAX), simplemente carga los selects con los diferentes grupos de datos y los va mostrando de acuerdo a los valores seleccionados en el select del cual depende. Por eso notarás la rapidez en el funcionamiento, ideal para pequeños/medianos conjuntos de datos.

Cuando los niveles son profundos y con grandes cantidades de opciones, seguramente querrás utilizar AJAX. A continuación veremos cómo modificar el ejemplo anterior para añadir esta característica facilmente.

Ahora con AJAX

Importante: Recuerda cargar jQuery sino no se harán las llamadas remotas.

  • lib/form/doctrine/PersonaForm.class.php:

    class PersonaForm extends BasePersonaForm
    {
        public function configure()
        {
            // widgets
    
            $this->widgetSchema['pais_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'     => 'Pais',
                'add_empty' => 'Seleccione país',
                'ajax'      => true,
            ));
    
            // si hubieses querido cargarlo sin ajax (ya que son pocos datos):
            #$this->widgetSchema['pais_id'] = new sfWidgetFormDoctrineChoice(array(
            #    'model'     => 'Pais', 
            #    'add_empty' => 'Seleccione pais',
            #));                 
    
            $this->widgetSchema['provincia_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'        => 'Provincia', 
                'depends'      => 'Pais',
                'add_empty'    => 'Seleccione provincia',
                'ajax'         => true,
                // aprovechamos para mostrar otras opciones
                #'ref_method'   => 'getPaisId',
                #'url'          =>  sfContext::getInstance()->getController()->genUrl('sfDependentSelectAuto/_ajax'),
                #'cache'        => true,
                // están disponibles las mismas que sfWidgetForm{Doctrine|Propel}Choice
                'order_by'      => array('nombre', 'asc'),
                #'method'       => '__toString',
                #'key_method'   => 'getId',
                #'table_method' => 'getPaisesEuropeos',
            ));
    
            $this->widgetSchema['ciudad_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'     => 'Ciudad', 
                'depends'   => 'Provincia',
                'add_empty' => 'Seleccione ciudad',
                'ajax'      => true,
                'order_by'  => array('nombre', 'asc'),
            ));    
    
            ...
        }
    }
    

Eso es todo. Las llamadas remotas son atendidas por un módulo especial llamado sfDependentSelectAuto que devuelve los valores respetando todas las opciones establecidas como si de forma local se tratase.

Más adelante se explican detalladamente las opciones disponibles y cómo funciona el módulo automático para que lo puedas extender y devolver tus propios datos.

Antes de eso, otro ejemplo.

Productos y servicios

Se utilizará como ejemplo un caso de ventas de productos y servicios que extienden a un ítem. Al momento de cargar el detalle de la factura, se selecciona el tipo (producto o servicio) y luego se despliegan los mismos en otro select.

Este ejemplo muestra cómo mezclar arrays con modelos.

  • config/doctrine/schema.yml:

    Item:
      columns:
        nombre: string(45)
        precio: float
        tipo: string(45)
    
    Producto:
      inheritance:
        extends: Item
        type: column_aggregation
        keyField: tipo
        keyValue: producto        
    
    Servicio:
      inheritance:
        extends: Item
        type: column_aggregation
        keyField: tipo
        keyValue: servicio
    
    Factura:
      columns:
        numero: integer
        fecha: date
    
    FacturaConcepto:
      columns:
        factura_id: integer
        item_id: integer
        cantidad: float
      relations:
        Factura:
          foreignAlias: Conceptos
        Item:
          foreignAlias: Conceptos
    
  • lib/model/doctrine/Item.class.php

    class Item extends BaseItem
    {
        private static $tipos = array(
            'producto' => 'Productos',
            'servicio' => 'Servicios',
        );
    
        public static function getTipos()
        {
            return self::$tipos;
        }
    }
    
  • lib/form/doctrine/FacturaConceptoForm.class.php

    class FacturaConceptoForm extends BaseFacturaConceptoForm
    {
        public function configure()
        {
            // widgets
    
            $this->widgetSchema['tipo'] = new sfWidgetFormChoice(array(
                'choices' => Item::getTipos(),
            ));
    
            // si quieres cargar los tipos con ajax, podrías haber utilizado
            // la clase sfWidgetFormArrayDependentSelect que se explica luego
    
            $this->widgetSchema['item_id'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'   => 'Item',
                'depends' => 'tipo',
                'ajax'    => true,
            ), array(
                'size'    => 5,
            ));
    
            // validadores
    
            $this->validatorSchema['tipo'] = new sfValidatorChoice(array(
                'choices' => Item::getTipos(),
            ));
        }
    }
    

Aparecerá un select en donde seleccionas el tipo, luego de seleccionarlo se carga la lista de items de ese tipo.

Puedes combinar arrays y modelos libremente.

Utilizando sólo arrays

Veamos el ejemplo de paises y provincias pero esta vez utilizando un array como fuente de datos. Para ello se utilizará el widget sfWidgetFormArrayDependentSelect.

A los datos los pondremos en una clase contenedora.

  • lib/MisDatos.class.php:

    abstract class MisDatos
    {
        private static $paises = array(
            'ar' => 'Argentina',
            'br' => 'Brasil',
        );
    
        private static $provincias = array(
            'ar' => array(
                'ar_bue' => 'Buenos Aires',
                'ar_cba' => 'Córdoba',
            ),
            'br' => array(
                'br_sp' => 'São Paulo',
                'br_mg' => 'Minas Gerais',
            ),
        );
    
        public static function getPaises()
        {
            return self::$paises;
        }
    
        public static function getProvincias()
        {
            return self::$provincias;
        }    
    }
    
  • lib/form/doctrine/PersonaForm.class.php:

    class PersonaForm extends BasePersonaForm
    {
        public function configure()
        {    
            // widgets
    
            $this->widgetSchema['pais'] = new sfWidgetFormArrayDependentSelect(array(
                'callable' => array('MisDatos', 'getPaises'),
            ));
    
            $this->widgetSchema['provincia'] = new sfWidgetFormArrayDependentSelect(array(
                'callable' => array('MisDatos', 'getProvincias'),
                'depends' => 'pais',
            ));            
    
            // validadores
    
            $this->validatorSchema['pais'] = new sfValidatorChoice(array(
                'choices' => MisDatos::getPaises(),
            ));
    
            $this->validatorSchema['provincia'] = new sfValidatorChoice(array(
                'choices' => MisDatos::getProvincias(),
            ));
        }
    }
    

También funciona hablitando ajax.

Referencia de opciones

Por widget, en negritas están las requeridas. El símbolo ">" significa que ese widget extiende al otro.

sfWidgetFormDependentSelect

  • depends = null: ¿De qué otro select se depende? Puede ser el nombre del campo o bien del modelo. El widget se encargará de determinar los detalles de acuerdo a este valor,

  • add_empty = true: Especifica si agrega una opción en blanco. Puede ser true, false o el texto que quieras que aperzca,

  • ajax = false: Especifica si se utilizan llamadas remotas para rellenar los valores del select,

  • cache = true: Especifica si se utilizará la caché de valores de un select una vez que ya fué cargado remotamente (para evitar peticiones extras),

  • url = sfDependentSelectAuto/_ajax: URL dónde se harás las llamadas remotas para cargar los selects. El valor no debe ser del tipo "modulo/accion" ni nombre de ruta (puede utilizar el método genUrl de sfWebController para genrarla),

  • params = array(): Parámetros adicionales que desea enviar a la llamada remota.

  • source_class = null: La clase que se utilizará como orígen de datos. (Ver más adelante en personalizando).

  • source_params = array(): Parámetros enviados al orígen de datos.

sfWidgetFormArrayDependentSelect > sfWidgetFormDependentSelect

  • callable = null: Función o método que se llamará para solicitar el array. En caso de ser un método estático, indicar: array('Clase', 'metodo'). No utilices instancias de clases como $this, ya que no funcionará en las llamadas remotas.

  • heredadas: depends, add_empty, ajax, cache, url, params

sfWidgetFormObjectDependentSelect > sfWidgetFormDependentSelect

  • model = null: Modelo que se listará en este select

  • method = __toString: Método para el texto cada opción

  • key_method = getPrimaryKey: Método para el valor de cada opción

  • ref_method = null: Método de la relación con 'depends'

  • order_by = null: Órden del lista, del tipo array('columna', {'asc'|'desc'})

  • heredadas: depends, add_empty, ajax, cache, url, params

sfWidgetFormDoctrineDependentSelect > sfWidgetFormObjectDependentSelect

  • table_method = null: Método que se llamará en la clase Table del objeto para devolver los valores a mostrar.

  • heredadas: depends, add_empty, ajax, cache, url, params, model, method, key_method, ref_method, order_by

sfWidgetFormPropelDependentSelect > sfWidgetFormObjectDependentSelect

  • peer_method = null: Método que se llamará en la clase PEER del objeto para devolver los valores a mostrar.

  • heredadas: depends, add_empty, ajax, cache, url, params, model, method, key_method, ref_method, order_by

Personalizando

En el caso que desees tener el control de los datos en las llamadas tanto locales como remotas puedes hacerlo de varias formas.

Utilizando la opción '{table|peer}_method'

Si trabajas con un ORM, lo más fácil será especificar la opción '{table|peer}_method' en el widget y luego implementarla en la clase correspondiente.

Veamos un ejemplo para devolver sólo aquellas provincias que sean productoras.

  • lib/form/doctrine/PersonaForm.class.php:

    class PersonaForm extends BasePersonaForm
    {
        public function configure()
        {    
            ...
    
            $this->widgetSchema['provincia'] = new sfWidgetFormDoctrineDependentSelect(array(
                'model'        => 'Provincia',
                'depends'      => 'Pais',
                'table_method' => 'getProductorasQuery',
            ));            
    
            ...
        }
    }
    
  • lib/model/doctrine/ProvinciaTable.class:

    class ProvinciaTable extends Doctrine_Table
    {    
        public function getProductorasQuery($refValue)
        {
            return $this->createQuery('p')->where('p.es_productora = true and p.pais_id = ?', $refValue);
        }
    }
    

Es importante que sepas que cuando la acción llama al método {table|peer}, se le pasa como parámetro el valor de referencia, para que puedas filtrar tus datos de acuerdo al valor del select del cual depende.

Creando tu propio orígen de datos o widget

En el caso que quieras utilizar otra fuente de datos (por si no usas un ORM o simplemente un canal RSS), puedes crear facilmente uno personalizado. De esta forma podrás aprovochar la automatización de las llamadas tanto locales como remotas y enlazarlas con otros selects dependientes que utilicen otros orígenes.

Veamos cómo hacer para crear tu propio orgien de datos para provincias productoras.

  • lib/ProvinciaProductoraSource.class.php:

    class ProvinciaProductoraSource extends sfDependentSelectObjectSource
    {
        // como vamos a trabajar con objetos extendemos a sfDependentSelectObjectSource
        // en tu caso puedes extender directamente a sfDependentSelectSource
    
        public function getObjects($fk = null)
        {
            return ProvinciaTable::getInstance()->createQuery('p')
                ->where('p.es_productora = true and p.pais_id = ?', $fk)
                ->execute();
        }
    
        public function getObject($pk)
        {
            // este método se utiliza para la reconstrucción de la cadena de 
            // selects como se explica más adelante. de momento sólo preocuparnos
            // en devolver el objeto que corresponde con el parametro $pk
    
            return ProvinciaTable::getInstance()->find($pk);
        }
    }
    
  • lib/form/doctrine/PersonaForm.class.php:

    class PersonaForm extends BasePersonaForm
    {
        public function configure()
        {    
            ...
    
            // nota que cuando utilizas tu propia fuente de datos tienes que
            // utilizar el widget sfWidgetFormDependentSelect
    
            $this->widgetSchema['provincia'] = new sfWidgetFormDependentSelect(array(
                'depends'      => 'Pais',
                'source_class' => 'ProvinciaProductoraSource',
                // tambien puedes enviarle parametros al source
                #'source_params'=> array('excepto' => 'Santa Fé'),
            ));
    
            ...
        }
    }
    

En este ejemplo hemos extendido a sfDependentSelectObjectSource que facilita el trabajo con objetos, pero si por ejemplo quieres crear una fuente RSS puedes basarte en cómo lo hace sfWidgetFormArrayDependentSelect.

También puedes crear tu propio widget para que utilice siempre este orígen o bien añadirle otra funcionalidad, revisa las clases sfDependentSelectSource y sfWidgetFormDependentSelect para conocer como crear las tuyas.

Extendiendo tu módulo

Otra forma es extender tu módulo a sfActionsDependentSelect y definir métodos de la forma getValuesFor{name} y getRefValueFor{name}. No olvides especificar tu acción en la opción 'url' del widget.

Veamos en el ejemplo de paises, si quisiéramos devolver las provincias de esta forma.

  • apps/tuapp/modules/tumodulo/actions/actions.class.php:

    class tuModuloActions extends sfActionsDependentSelect
    {
        protected function getValuesForPersonaProvincia($request, $source)
        {
            // en este caso trabajamos con objetos, pero puedes hacerlo de cualquier
            // forma siempre y cuando devuelvas un array con los id como clave
            // y los textos como valores del array.
    
            $valores = array();
            $provincias = ProvinciaTable::getInstance()->createQuery('p')
                ->where('p.pais_id = ? and p.es_productora = true')
                ->execute($request->getParameter('_ds_ref'));
    
            // el parámetro _ds_ref contiene el valor del select del cual depende
            // en este caso sería el valor de pais.id
    
            foreach ($provincias as $provincia) {
                $valores[$provincia->getId()] = $provincia->getNombre();
            }
    
            return $valores;
        }
    
        protected function getRefValueForPersonaProvincia($request, $source)
        {
            // este método se utiliza para "reconstruir" la cadena de selects
            // dependientes desde el orden inverso. es decir, si sólo se tiene
            // el valor de provincia, tenemos que saber de qué pais es para
            // seleccionarlo en el select de paises.
            // en este caso el valor de _ds_ref es el valor actual del select
            // de provincias (provincia.id) y debemos devolver a qué país 
            // corresponde (provincia.pais_id)
    
            $provincia = ProvinciaTable::getInstance()->find($request->getParameter('_ds_ref'));
            return $provincia->getPaisId();
        }
    }    
    

Nota que cada método también recibe como parámetro la fuente de datos por si te es necesario.

Puedes revisar la clase sfActionsDependentSelect para conocer más sobre cómo extender la funcionalidad en tu módulo.

Si no puedes extender tu módulo a la clase antes mencionada y definitivamente ninguna de las formas ofrecidas satisfacen tus necesidades, deberás implementar una acción en tu módulo que contenga toda la funcionalidad como la acción '_ajax' lo hace en la clase sfActionsDependentSelect. Con Firebug puedes ver los parámetros enviados vía POST.

Sobre el código javascript

Por defecto se incluye en el plugin dos versiones del script, normal y minimizado. Siempre se cargará el minimizado a menos que cambies tu configuración:

  • apps/tuapp/config/app.yml:

    dev:
      app:
        ...
        sfDependentSelectPlugin:
          js: normal
    

También puedes establecer 'false' para que no se cargue el script. El valor para mostrarlo minimizado es 'minimized'.

¿Sugerencias?

Desgraciadamente no hablo inglés y he elaborado la mayor parte del código tratando de usar terminología en ese idioma con ayuda de traductores automáticos. Pido disculpas por incoherencias encontradas y agradecería me informen para poder corregirlas.

Me pueden escribir a mi correo personal por cualquier sugerencia o duda. Con mucho gusto estaré dispuesto a mejorar los aspectos que me indiquen.

Agradecimientos

A [TRADUCTORES] por ofrecerse tan amablemente a corregir y traducir la documentación y a todos los miembros de la lista de symfony-es por formar esta gran comunidad.

TODO

  • Traducir documentación a inglés
  • Probar funcionamiento en symfony 1.1 y 1.2