Cómo Resolver la Carga de Datos en Cascada, Pais, Departamento, Ciudad


 

Si alguna vez has intentado poblar menús desplegables (selects) de manera secuencial, como en un formulario de dirección, probablemente te has encontrado con un problema común: la asincronía.

Intentas cargar los países, luego los departamentos y finalmente las ciudades. Pero, cuando llega el momento de establecer los valores por defecto (ej. para editar un perfil), el código falla porque las opciones aún no han sido cargadas en el DOM. selectElement.value = 'Colombia' no funciona si la opción 'Colombia' todavía no existe.

Este artículo te muestra una solución elegante y robusta que me funcionó como un reloj. (Observar que puede ser usada para culquier proceso que involucre algun tipo de jerarquía de varios niveles yo tengo algunas del tipo usada para categorias de productos de almacén, ventas  inventarios, así como estructuras de comercializadores , agencias, sucursales  y promotores y vendedores) 

El Problema: El Flujo Asíncrono y los Callbacks

El problema surge porque las funciones que cargan las listas (como Paises(), Departamentos(), y Ciudades()) son asíncronas. Utilizan fetch para obtener datos de una API, y esa operación no se completa de inmediato. El código de JavaScript sigue su camino sin esperar, y cuando intentas asignar un valor, las opciones del select están vacías.

El código tradicional con callbacks anidados termina siendo difícil de leer y mantener, lo que se conoce como "callback hell".

La Solución: async/await y la Promesa de Sincronización

Mi solución fue combinar el poder de async/await con una técnica para envolver las funciones de callback en promesas. Esto nos permite simular un comportamiento síncrono y asegurar que cada paso se complete antes de pasar al siguiente.

El Código Genérico de la Solución

A continuación, el patrón de código que utilicé. Puedes adaptar las variables y los nombres de las funciones a tu propio proyecto.


// Función para cargar y establecer las ubicaciones en cascada

const cargarUbicacionesEdicion = async (userData) => {

    try {

        // 1. PAÍS: Cargar países y esperar a que el DOM se actualice

        if (userData.clientePaisNombre) {

            await new Promise(resolve => {

                // Guarda la función de callback original

                const originalCallback = window.PaisesMostrar;


                // Sobreescribe la función para que resuelva nuestra promesa

                window.PaisesMostrar = function (data) {

                    originalCallback(data); // Llama a la función original para poblar el select

                    resolve(); // Resuelve la promesa

                };

    

                // Llama a la función asíncrona original

                Paises("paisNombre");

            });

            // Asigna el valor del país

            document.getElementById("paisNombre").value = userData.clientePaisNombre;

        }

        // 2. DEPARTAMENTO: Cargar departamentos y esperar

        if (userData.clienteDepartamentoNombre) {

            await new Promise(resolve => {

                const originalCallback = window.DepartamentosMostrar;

                window.DepartamentosMostrar = function (data) {

                    originalCallback(data);

                    resolve();

                };

                // Llama a la función de departamentos. Asegúrate de pasar el ID del país si es necesario.

                Departamentos("departamentoNombre", userData.clientePaisNombre); 

            });

            // Asigna el valor del departamento

            document.getElementById("departamentoNombre").value = userData.clienteDepartamentoNombre;

        }

        // 3. CIUDAD: Cargar ciudades y esperar

        if (userData.clienteCiudadNombre) {

            await new Promise(resolve => {

                const originalCallback = window.CiudadesMostrar;

                window.CiudadesMostrar = function (data) {

                    originalCallback(data);

                    resolve();

                };

                // Llama a la función de ciudades.

                Ciudades("ciudadNombre", userData.clienteDepartamentoNombre);

            });

            // Asigna el valor de la ciudad

            document.getElementById("ciudadNombre").value = userData.clienteCiudadNombre;

        }

    } catch (error) {

        console.error('Error al cargar las ubicaciones:', error);

    }

};

NOTA:

Las funciones Paises("paisNombre"), Departamentos("departamentoNombre"),Ciudades("ciudadNombre"), son funciones que llaman una Api rest full que cargan la variables , paisNombre, departamentoNombre y ciudad nombre con los respectivas option del select.  

al final queda un programa de referencia que llena los select.


¿Cómo Funciona Paso a Paso?

  1. Envolver en una Promesa: new Promise(resolve => { ... }) crea un envoltorio. El código dentro de async/await se pausa (await) hasta que se llama a la función resolve().

  2. Sobreescribir el Callback: Las librerías o funciones heredadas a menudo dependen de callbacks globales (ej. window.PaisesMostrar). Temporalmente, reemplazamos esa función de callback con una propia que hace dos cosas:

    • Llama al callback original para que siga poblando el select.

    • Llama a resolve(), lo cual "despierta" el await y permite que el código continúe.

  3. Encadenamiento Sucesivo: Gracias a await, cada sección del código espera pacientemente a que la anterior termine. Primero se cargan los países, luego el código se detiene. Una vez cargados, se asigna el valor, y solo entonces se pasa a la siguiente promesa para los departamentos, y así sucesivamente.

¿Por Qué Es Una Solución Robusta?

  • Evita el "Callback Hell": El código es plano, limpio y fácil de leer, lo que mejora la mantenibilidad.

  • Garantiza el Orden: Asegura que la asignación del valor de cada select se haga solo cuando las opciones correspondientes ya están en el DOM.

  • Funciona con Código Existente: No necesitas reescribir tus funciones de carga (Paises, Departamentos, Ciudades) si ya usan callbacks; simplemente las envuelves.

LLamar la funcion:
/
export const mostrarFormulario = async (isEdit = false, userData = null) => {
    DOMElements.userFormContainer.style.display = 'block';
    DOMElements.userForm.reset(); // Clear form fields                                                                                      
    DOMElements.clienteTerminos.checked = true; // Ensure terms are unchecked by default                                                    

    if (isEdit && userData) {
        DOMElements.formTitle.textContent = 'Editar Cliente';
// Cargar ubicaciones (País, Departamento, Ciudad) - mantener funcional                                                             
        await cargarUbicacionesEdicion(userData, true);

        DOMElements.formTitle.textContent = 'Editar Cliente';


**********************************************************************
Funciones de referencia para cargar los datos de los Paises,Departamentos y Ciudades
//Location.js

var paisSelect         = "";
var departamentoSelect = "";
var ciudadSelect       = "";

// Función que mantiene la llamada a la API
function Paises(seleccion) {
    paisSelect = seleccion;
    var c = {
        paisBuscar: "",
        paisSort: "NOMBRE"
    };
    apiHandler("PaisesConsultarID", API_KEY, usuarioDG, PaisesMostrar, c);
}

// Función que procesa los datos con JavaScript Vainilla
function PaisesMostrar(paisInfo) {
    var sel = document.getElementById(paisSelect).value;
    
    var selectElement = document.getElementById(paisSelect);
    selectElement.innerHTML = '';

    var defaultOption = document.createElement('option');
    defaultOption.value = "";
    defaultOption.textContent = "Seleccione un país";
    selectElement.appendChild(defaultOption);

    for (var i = 0; i < paisInfo.length; i++) {
        var p = paisInfo[i];
        var option = document.createElement('option');
        option.value = p.paisNombre;
        option.textContent = p.paisNombre;
        selectElement.appendChild(option);
    }
    
    document.getElementById(paisSelect).value = sel;
}

// Función que mantiene la llamada a la API
function Departamentos(seleccion) {
    departamentoSelect = seleccion;

    var paisNombreB = document.getElementById("paisNombre").value;

    var c = {
        departamentoBuscar: paisNombreB,
        departamentoSort: "PAISNOMBRE"
    };

    apiHandler("DepartamentosConsultarID", API_KEY, usuarioDG, DepartamentosMostrar, c);
}


function DepartamentosMostrar(departamentoInfo) {
    var sel = document.getElementById(departamentoSelect).value;

    var selectElement = document.getElementById(departamentoSelect);
    selectElement.innerHTML = '';

    var defaultOption   = document.createElement('option');
    defaultOption.value = "";
    defaultOption.textContent = "Seleccione un departamento";
    selectElement.appendChild(defaultOption);

    for (var i             = 0; i < departamentoInfo.length; i++) {
        var p              = departamentoInfo[i];
        var option         = document.createElement('option');
        option.value       = p.departamentoNombre;
        option.textContent = p.departamentoNombre;
        selectElement.appendChild(option);
    }

    document.getElementById(departamentoSelect).value = sel;
}

// Función que mantiene la llamada a la API
function Ciudades(seleccion) {
    ciudadSelect = seleccion;
    var departamentoNombreB = document.getElementById("departamentoNombre").value;

    var c = {
        ciudadBuscar: departamentoNombreB,
        ciudadSort: "DEPARTAMENTONOMBRE"
    };

    apiHandler("CiudadesConsultarID", API_KEY, usuarioDG, CiudadesMostrar, c);
}

// Función que procesa los datos con JavaScript Vainilla
function CiudadesMostrar(ciudadInfo) {
    var sel = document.getElementById(ciudadSelect).value;
    
    var selectElement = document.getElementById(ciudadSelect);
    selectElement.innerHTML = '';

    var defaultOption = document.createElement('option');
    defaultOption.value = "";
    defaultOption.textContent = "Seleccione una ciudad";
    selectElement.appendChild(defaultOption);
    
    for (var i = 0; i < ciudadInfo.length; i++) {
        var p = ciudadInfo[i];
        var option = document.createElement('option');
        option.value = p.ciudadNombre;
        option.textContent = p.ciudadNombre;
        selectElement.appendChild(option);
    }

    document.getElementById(ciudadSelect).value = sel;
}

***************************************************************
NOTA: La funcion apiHandler("CiudadesConsultarID", API_KEY, usuarioDG, CiudadesMostrar, c); es una llamada a mi rest full ,  esta api que toma como paremtros  la base de datos a consultar (usuarioDB), la funciona llamada para cargar la información ("CiudadesConsultarId"), la variable es un arrelgo jason  con algunas variables usadas como la información a buscar y el orden de presentación

Comentarios

Entradas populares