domingo, 2 de mayo de 2010

Listas dependientes con jQuery: El plug-in

En la nota "Listas dependiente con jQuery" presenté el código necesario para cargar dinámicamente las opciones de una lista en base al valor seleccionado en otra lista. En esta ocasión voy a presentar el resultado de la evolución del código presentado y cómo se transformó en un plug-in de jQuery.

jQuery plug-in

Para los neófitos en la programación web, jQuery es una biblioteca de funciones que facilitan la creación de aplicaciones web, a través de la manipulación del modelo de objetos de documento (DOM) y la comunicación con un servidor utilizando AJAX y JSON. Además de proveer una amplia gama de funcionalidad, jQuery permite la integración de funcionalidad personalizada a través de un mecanismo de extensión: los agregados o plug-ins.

Después de codificar la solución al problema de las listas dependientes, pensé que era un patrón lo suficientemente común como para que alguien hubiera creado un solución antes que yo. Entonces busqué en la biblioteca de extensiones de jQuery y efectivamente encontré un par de agregados que permiten cambiar las opciones disponibles en una lista dinámicamente a partir del valor seleccionado en otra lista. Sin embargo, estos agregados asumen que los datos ya están cargados, o dicho de otra manera, sólo filtran las opciones, no las cargan dinámicamente. Entonces, el siguiente paso lógico es crear un agregado de jQuery a partir de mi código.

La metamorfosis

Comencemos primero por ver el código base que se encarga de cargar las opciones de una lista dependiendo del valor seleccionado en otra:

<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>

Del código anterior, se pueden identificar las siguientes funciones:

  • Inicializar - Obtener referencia al campo padre y agregar código que responda al evento change.
  • Limpiar Hijo - Elimina las opciones del campo hijo y agrega la opción predeterminada.
  • Cargar Hijo - Agrega las opciones del campo hijo de acuerdo al valor del campo padre.
  • Obtener Datos - Si las opciones no están en el almacén local, obtenerlas del almacén persistente (servidor) y guardarlas en el almacén local.

Adicionalmente, ya que la idea de crear un agregado es que pueda aplicarse de manera genérica, el código debe permitir la configuración de la funcionalidad. Entonces es deseable que se pueda configurar el patrón de la nomenclatura para los archivos que contienen los datos.

Finalmente todo esto lo tenemos que empaquetar de acuerdo al patrón sugerido en la documentación de jQuery. Este patrón consiste basicamente de una función que recibe como parámetro el objeto global jQuery al cual le agregamos una función con el nombre del agregado a través de la propiedad fn.

La arquitectura de jQuery consiste en extender un objeto del DOM. En este caso el agregado proveería una función adicional a través de la cual se extiende el campo padre indicándole el identificador del campo hijo y el patrón de la nomenclatura de los archivos de datos.

El código resultante

(function($) {
    var ParentChild;
    ParentChild = function(element, childId, urlPattern) {
        this.domSelect = element;
        this.domSelect.data('parentChild', this);
        this.child = $('#' + childId);
        this.pattern = urlPattern;
        this.data = {};
        var me = this;
        this.getData = function(parentId) {
            $.getJSON(this.getUrl(parentId), function(data) {
                me.data[parentId] = data;
                me.populateChild(parentId);
            });
        };
    };
    ParentChild.prototype = {
        emptyChild: function() {
            this.child.empty();
            this.child.append('');
        },
        getUrl: function(parentId) {
            var url;
            if ((!this.pattern) || (this.pattern == ""))
                url = parentId;
            else
                url = this.pattern.replace(new RegExp("\\{p\\}", "gi"), parentId);
            return url;
        },
        disableChild: function() {
            this.child[0].disabled = true;
        },
        populateChild: function(parentId) {
            if(!this.data[parentId]) {
                this.getData(parentId);
            } else {
                for (childId in this.data[parentId])
                    this.child.append('');
                this.child[0].disabled = false;
            }
        }
    };
    $.fn.parentChild = function(childId, urlPattern) {
        var child = $('#' + childId);
        this.each(function() {
            var pc;
            pc = $(this).data("parentChild");
            if ((!pc) || (typeof pc === "undefined")) {
                pc = new ParentChild($(this), childId, urlPattern);
            }
            $(this).change(function() {
                var parentId = $(this).val();
                pc.emptyChild();
                pc.disableChild();
                if (parentId > 0)
                    pc.populateChild(parentId)
            });
        });
    };
})(jQuery);

El código de las líneas 2 a la 42 definen una clase privada que modela la funcionalidad básica y contiene los datos. En el constructor (líneas 3 a la 16), se guarda la referencia al elemento padre extendido en la propiedad domSelect. Luego, se almacena la instancia de la clase utilizando la llave parentChild. Se obtiene y se guarda la referencia al elemento hijo en la propiedad child. Se guarda el patrón para construir los URL en la propiedad pattern. Se inicializa la propiedad data a un objeto vacío (recuerden que en JavaScript no hay arreglos asociativos, sino que los objetos permiten acceder a los valores de sus propiedades utilizando la sintaxis de arreglos). Se almacena la autoreferencia this en la variable me. En las líneas 10 a la 15 se define el método getData que recibe un parámetro con la clave de identificación del padre y utiliza el método de jQuery getJSON para obtener los datos de manera asíncrona. El URL se obtiene de la función getUrl definida más adelante, y en la función de retrollamada (líneas 12 y 13) básicamente se almacenan los datos recibidos en el almacén local y se llama la función populateChild con la clave de identificación del padre. Nótese que se utiliza la variable me para referenciar la instancia de la clase ParentChild ya que en este contexto this es la referencia a la función de retrollamada (la función anónima definida en las líneas 12 y 13).

En las líneas 17 a la 42, se definen las funciones:

  • emptyChild - Líneas 18 a la 21. Vacía las opciones de la lista dependiente y agrega la opción predeterminada.
  • getUrl - Líneas 22 a la 29. Genera el URL del archivo con los datos en formato JSON a partir de la clave de identificación del padre. Se utiliza una expresión regular muy simple para remplazar {p} en el patrón con la clave de identificación del padre.
  • disableChild - Líneas 30 a la 32. Deshabilita el campo hijo.
  • populateChild - Líneas 33 a la 41. Verifica si existen los datos para el padre indicado. Si no existen, llama a la función getData. Si ya existen, agrega las opciones correspondientes y habilita el campo hijo.

Lo interesante de estos métodos es que están definidos en el prototipo de la clase, y por lo tanto sólo existe una copia de ellos. A diferencia del método getData, el cual debido al concepto de cerradura (Closure) se replicaría en cada instancia de la clase ParentChild.

Finalmente en las líneas 43 a la 59, se define la función de extensión de jQuery, es decir, el agregado. Cabe notar que jQuery opera sobre uno o más elementos del DOM, por lo que el patrón sugerido itera cada uno de los elementos asociados al objeto jQuery (línea 45). Básicamente primero determina si ya existe una instancia de la clase ParentChild asociada con el elemento en cuestión, si no existe, entonces la crea pasando la referencia al elemento extendido con jQuery, la clave del elemento hijo y el patrón para construir el URL. Finalmente, agrega código para responder al evento change (Líneas 51 a la 57) que obtiene el valor del campo padre, vacía el campo hijo, deshabilita el campo hijo y si el valor del padre es mayor que cero, llena la lista del campo hijo.

Conclusión

Crear un agregado para jQuery es relativamente sencillo. Hay que tomar en cuenta que se está extendiendo el objeto jQuery que a su vez extiene uno o más elementos del DOM. Hay que tener en cuenta el contexto de ejecución cuando se utiliza la referencia this, ya que puede hacer referencia a algo totalmente diferente de lo que creemos. El propósito de un agregado es encapsular funcionalidad genérica, por lo que debemos proveer flexibilidad a través de parámetros de configuración. Eventualmente, liberaré el código con una licensia de código abierto.

1 comentario:

  1. Excelentes articulos amigo, gracias por compartir, la verdad esto de las listas dependientes son muy tipicas en casi todas las aplicaciones....saludos :)

    ResponderEliminar