viernes, 23 de abril de 2010

Listas dependientes con jQuery

Recientemente me solicitaron que modificara un formulario de un sitio web para que al seleccionar un Estado, se cargara la lista de Municipios correspondientes de manera dinámica. La restricción era que no podía depender de procesamiento en el servidor debido a la plataforma en la que está construido el sitio. La solución que se me ocurrió fue utilizar JavaScript para cargar dinámicamente los municipios. A continuación les presento el código y una breve explicación de su funcionamiento.


El problema: Campos dependientes

Es muy común que al crear formularios tengamos campos dependientes como el caso de los Estados y Municipios o Categoría y Productos. El reto es cargar las opciones del campo dependiente de manera dinámica en base al valor seleccionado en el campo padre. También es deseable que, para mejorar la experiencia del usuario, la información se cargue progresivamente. Es decir, cargar los municipios hasta que se seleccione el estado y cargar sólamente los municipios del estado seleccionado.

Herramientas

jQuery

En algún momento experimenté con AJAX, antes de que se hicieran populares bibliotecas como jQuery, Prototype, MooTools, etc. Luego, utilicé Prototype en algunos proyectos, pero nunca había trabajado con jQuery, así que me aproveche la oportunidad y utilicé jQuery para aislar los problemas de compatibilidad entre navegadores a la hora de manipular la estructura de una página web.

JSON

JavaScript Object Notation es una notación simple para describir objetos desarrollada por Douglas Crockfort basada en el lenguaje JavaScript. Es un formato apliamente utilizado debido a que es más breve que XML y permite transferir datos relativamente complejos de manera eficiente.

La solución

A grandes rasgos la solución es simple:

  1. Determinar el Estado seleccionado.
  2. Vaciar la lista de Municipios.
  3. Cargar los Municipios del Estado seleccionado.

Para lograr esto podemos responder al evento change del elemento <select> utilizando jQuery de la siguiente manera:

Primera versión

<script type="text/javascript">
//Definir objeto con las listas de municipios por estado.
var padreHijos = {
    "0": [],
    "1": {
        "1": "Aguascalientes",
        "2": "Asientos"
        //Agregar municipios por estado.
        }
    };
//Definir identificadores de campos
var idPadre = 'estado';
var idHijo = 'municipio';
//Definir código que se ejecuta cuando se termine de cargar la estructura del documento.
$(document).ready(function() {
    //Obtener referencia al campo padre.
    var padreSelect = $('#' + idPadre);
    //Definir código que se ejecuta cuando cambia valor de campo padre.
    padreSelect.change(function() {
        //Obtener referencia al campo hijo.
        var hijoSelect = $('#' + idHijo);
        //Obtener clave de identificación del elemento padre.
        var padreId = padreSelect.val();
        //Deshabilitar el campo hijo.
        hijoSelect[0].disabled = true;
        //Eliminar las opciones del campo hijo.
        hijioSelect.empty();
        //Agregar opción predeterminada.
        hijoSelect.append('<option value="0" selected="selected">-- Seleccionar --</option>');
        //Obtener lista de hijos para el padre seleccionado.
        var hijos = padreHijos[padreId];
        //Agregar opciones para cada hijo.
        for (id in hijos) {
            hijoSelect.append('<option value="' + id + '">' + hijos[id] + '</option>');
        }
        //Habilitar campo hijo.
        hijoSelect[0].disabled = (padreId == 0);
    });
});
</script>

El código anterior depende de que exista un objeto padreHijos que funciona como un arreglo asociativo de las claves de identificación de los estados y su lista de municipios. Luego, se definen los identificadores de los elementos <select> para los campos estado y municipio

El primer problema con este código es que la base de datos puede requerir mucho espacio y por lo tanto, retrasar la descarga del código. Por lo tanto, la primer mejora sería cargar la información gradualmente. Para esto, podemos usar la función getJSON que permite hacer una petición asíncrona de un archivo con datos en notación JSON. Entonces, en lugar de declarar todos los municipios en el código que se carga inicialmente, creamos un archivo con la lista de municipios para cada estado. Por ejemplo, para el caso de los municipios correspondientes a Aguascalientes, creamos el siguiente archivo:

m1.json

{"1":"Aguascalientes",
"2":"Asientos",
"3":"Calvillo",
"4":"Cosío",
"5":"Jesús María",
"6":"Pabellón de Arteaga",
"7":"Rincón de Romos",
"8":"San José de Gracia",
"9":"Tepezalá",
"10":"El Llano",
"11":"San Francisco de los Romo"}

Nótese que los archivos con la lista de municipios van a utilizar la nomenclatura: m{id}.json en la que se remplaza {id} por la clave de identificación del estado correspondiente.

Luego hay que modificar el código para cargar los municipios dinámicamente a partir el estado seleccionado:

Segunda versión

<script type="text/javascript">
//Definir identificadores de campos
var idPadre = 'estado';
var idHijo = 'municipio';
//Definir objeto contenedor de lista de hijos por padre.
var padreHijos = {"0":[]};
//Definir código que se ejecuta al terminar de cargar la estructura del documento.
$(document).ready(function() {
    //Obtener referencia al campo padre.
    var padreSelect = $('#' + idPadre);
    //Definir código que se ejecuta al cambiar el valor del campo padre.
    padreSelect.change(function() {
        //Obtener referencia al campo hijo.
        var hijoSelect = $('#' + idHijo);
        //Obtener clave de identificación del estado seleccionado.
        var padreId = padreSelect.val();
        //Deshabilitar el campo hijo
        hijoSelect[0].disabled = true;
        //Vaciar la lista de hijos
        hijoSelect.empty();
        //Agregar la opción predeterminada.
        hijoSelect.append('<option value="0" selected="selected">-- Seleccionar --</option>');
        //Definir función para cargar opciones en elemento $lt;select>
        var cargarHijos = function(padreId) {
            //Obtener lista de hijos
            var hijos = padreHijos[padreId];
            //Agregar opciones para cada municipio.
            for (id in hijos) {
                hijoSelect.append('<option value="' + id + '">' + hijos[id] + '</option>');
            }
            //Habilitar la lista de hijos.
            hijoSelect[0].disabled = (padreId == 0);
        };
        //Si la clave de identificación del padre es mayor que cero
        //cargar lista de hijos.
        if (padreId > 0) {
            //Si no existe la lista de hijos, obtener la información
            //del archivo codificado en JSON.
            if (!padreHijos[padreId]) {
                var url = 'm' + padreId + '.json';
                $.getJSON(url, function(data) {
                    //Guardar lista de hijos en copia local.
                    padreHijos[padreId] = data;
                    cargarHijos(padreId);
                });
            } else {
                //Si ya existe la lista de hijos en la copia local, simplemente cargarla
                cargarHijos(padreId);
            }
        }
    });
});
</script>

Conclusión

Al utilizar jQuery se evitan las complicaciones de compatibilidad entre navegadores. También se facilita la creación de código que cargue gradualmente la información necesaria en lugar de cargar todo desde el principio.

Posteriormente publicaré un artículo con la versión final y con la incorporación de esta funcionalidad en un plug-in de jQuery.

6 comentarios:

  1. Hola que tal.. tendras el archivo descargable?

    ResponderEliminar
  2. En cuanto tenga tiempo libre, publico el código para que se pueda descargar. Paciencia.

    ResponderEliminar
  3. hola brother bueno quisiera q me ayudes a chekar este codigo que no eh podido solucionar, cheka este link http://www.forosdelweb.com/f127/problema-con-select-jquery-906769/ espero que me puedas ayudar.

    ResponderEliminar
  4. NO ha de tener mucho tiempo libre, xq ya pasaron mas de 5 años de la fecha que comento que subiria el codigo :P


    venisker

    ResponderEliminar
  5. NO ha de tener mucho tiempo libre, xq ya pasaron mas de 5 años de la fecha que comento que subiria el codigo :

    ResponderEliminar
  6. jejeje han pasado 8 años de la fecha que comento que subiría el código

    ResponderEliminar