Icono del sitio KumbiaPHP Framework PHP en español

Select anidado o select dependientes

Con este tutorial vamos a aprender como implementar select anidado o select dependientes usando KumbiaPHP y jquery. Es una lista simple enlazada con tres niveles: Regiones, comunas y ciudades.

Implementando el select anidado

Primero que todo cargamos la librería jquery añadiendo en nuestro template activo la siguiente línea:


<?= Tag::js('jquery/jquery.min'); ?>

La base de datos


CREATE TABLE `ciudad` (
  `id` int(4) NOT NULL AUTO_INCREMENT,
  `comuna_id` int(4) NOT NULL,
  `nombre` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_ciudad__comuna` (`comuna_id`),
  CONSTRAINT `FK_ciudad__comuna` FOREIGN KEY (`comuna_id`) REFERENCES `comuna` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO `ciudad` VALUES (1,1,'Primera Ciudad'),(2,2,'Primera Ciudad'),(3,2,'Segunda Ciudad'),(4,3,'Primera Ciudad'),(5,3,'Segunda Ciudad'),(6,3,'Tercera Ciudad');

CREATE TABLE `comuna` (
  `id` int(4) NOT NULL AUTO_INCREMENT,
  `region_id` int(4) NOT NULL,
  `nombre` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_comuna__region` (`region_id`),
  CONSTRAINT `FK_comuna__region` FOREIGN KEY (`region_id`) REFERENCES `region` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO `comuna` VALUES (1,1,'Primera Comuna'),(2,1,'Segunda Comuna'),(3,2,'Primera Comuna'),(4,3,'Primera Comuna');

CREATE TABLE `region` (
  `id` int(4) NOT NULL AUTO_INCREMENT,
  `nombre` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO `region` VALUES (1,'Primera Region'),(2,'Segunda Region'),(3,'Tercera Region');

Los modelos

Vamos a tener tres modelos que se encargaran de la consulta de los datos, heredando como no de la clase ActiveRecord:

Archivo: app/models/region.php


<?php
class Region extends ActiveRecord
{
    /**
     * Lista todas las regiones
     * @return array
     */
    public function all()
    {
        return $this->find('order: nombre');
    }
}

Archivo: app/models/comuna.php


<?php
class Comuna extends ActiveRecord
{

    /**
     * Lista todas las comunas de una región
     * 
     * @param int $region_id
     * @return array
     */
    public function allByRegion(int $region_id)//validación int de PHP7
    {
        return $this->find("region_id = $region_id", 'order: nombre');
    }
}

Archivo: app/models/ciudad.php


<?php
class Ciudad extends ActiveRecord
{

    /**
     * Lista todas las ciudades de una comuna
     * 
     * @param int $region_id
     * @return array
     */
    public function allByComuna(int $comuna_id)//validación int de PHP7
    {
        return $this->find("comuna_id = $comuna_id", 'order: nombre');
    }
}

El controlador

Usaremos de ejemplo el controlador UserController aunque no implementaremos la funcionalidad de creación de un usuario para enforcarnos en el select anidado. Tendrá dos funciones auxiliares que nos cargarán los respectivos selects sin template, getComunas() y getCiudades().

Archivo: app/controllers/user_controller.php


<?php
/**
 * Controlador para las acciones y vistas con el usuario
 */
class UserController extends AppController
{

    public function index()
    {
        
    }

    public function create()
    {
        //se verifica si se ha enviado via POST los datos
        if (Input::hasPost('user')) {
            //
        }
    }

    public function getComunas()
    {
        //No es necesario el template
        View::template(null);
        //Carga la variable $region_id en la vista
        $this->region_id = Input::post('region_id');
    }

    public function getCiudades()
    {
        //No es necesario el template
        View::template(null);
        //Carga la variable $comuna_id en la vista
        $this->comuna_id = Input::post('comuna_id');
    }
}

Las vistas

La primera vista es la de crear un usuario, la cual contendrá los tres selects, aunque en principio solo mostrará el primero y unas capas vacias, ya que los siguientes dependen de qué se seleccione y serán cargados mediante ajax:

Archivo: app/views/user/create.phtml

<h1>Crear usuario</h1>
<?= Html::linkAction('', 'Listar usuarios', 'class="button"') ?>
<?php View::content() ?>
<?= Form::open() ?>
  <div class="row">
    <div class="six columns">
      <label for="user_region_id">Región</label>
      <?= Form::dbSelect('user.region_id', 'nombre', array('region', 'all'), '- Seleccione -'); ?>
    </div>
  </div>
  <div class="row">
    <div class="six columns">
      <label for="user_comuna_id">Comuna</label>
      <div id='div_comunas'></div>
    </div>
  </div>
  <div class="row">
    <div class="six columns">
      <label for="user_ciudad_id">Ciudad</label>
      <div id='div_ciudades'></div>
    </div>
  </div>
<input class="button-primary" value="Enviar" type="submit">
<?= Form::close() ?>

<script type='text/javascript'>
  $(document).on('change', "#user_region_id", function () {
    var region_id = $('#user_region_id').val();
     $.ajax({
       type: "POST",
       url: "<?php echo PUBLIC_PATH . 'user/getComunas/'; ?>",
       data: "region_id=" + region_id,
       success: function (html) {
         $("#div_comunas").html(html);
       }
    });
  });
</script>

La vista anterior con javascript añade un listener para que cuando el valor del select que contiene las regiones cambie, haga un llamado mediante ajax para obtener el listado de comunas por región y este resultado lo carga en la capa div_comunas.

La siguiente vista cargará el select usando el helper Form::dbSelect() usando el método allByRegion() del modelo Comuna y pasándole como parámetro $region_id.
Archivo: app/views/user/getComunas.phtml


<?= Form::dbSelect("user.comuna_id", 'nombre', array('comuna', 'allByRegion', $region_id), '- Seleccione -'); ?>

<script type='text/javascript'>
  $(document).on('change', "#user_comuna_id", function () {
    var comuna_id = $('#user_comuna_id').val();
      $.ajax({
        type: "POST",
        url: "<?php echo PUBLIC_PATH . 'user/getCiudades/'; ?>",
        data: "comuna_id=" + comuna_id,
          success: function (html) {
            $("#div_ciudades").html(html);
          }
       });
 });
</script>

La vista anterior con javascript añade un listener para que cuando el valor del select que contiene las comunas cambie, haga un llamado mediante ajax para obtener el listado de ciudades por comuna y este resultado lo carga en la capa div_ciudades.

La siguiente vista es similar a getComunas.phtml, lo que hace es cargar todas las ciudades que pertenezcan a una comunidad usando el helper Form::dbSelect() pasandole como parámetros el nombre del modelo ciudad, el método allByComuna() y el parámetro de consulta $comuna_id.

Archivo: app/views/user/getCiudades.phtml

<?= Form::dbSelect("user.ciudad_id", 'nombre', array('ciudad', 'allByComuna', $comuna_id), '- Seleccione -'); ?> 

Ejecutando el ejemplo

Cuando cargamos la url /user/create veremos algo como lo siguiente, en primera instacia solo va mostrar las regiones:

Muestra el primer select anidado

Lo siguientes dos select se mostraran comforme se vaya seleccionando una región y posteriormente una comuna.

Lista enlazada simple de tres niveles

Descargar código completo

Como siempre, el código completo está disponible para todos en el siguiente repositorio en Github listo para usar con Docker: https://github.com/henrystivens/dependent-select

Salir de la versión móvil