Logo de islavisual
Logo de islavisual IslaVisual
imagen de sección

Ultima revisión 17/01/2023

Mejorando el DOM (Parte II). Cómo añadir funcionalidad a JavaScript sin frameworks

Si bien, la semana pasada iniciamos una pequeña práctica de cómo añadir funcionalidad a JavaScript de una forma eficiente, rápida y sencilla, hoy continuaremos con más utilidades que, en un momento dado, nos podrían venir estupendamente.

Hay muchas posibilidades que aportar a nuestro framework propio, así que, empezaremos por algo más o menos sencillo, pero francamente útil. Por ejemplo, podríamos hacer unos métodos para mostrar y ocultar elementos.

El método para mostrar podría ser:

/**
    @description Muestra un elemento
    @version 1.0
    @author Pablo E. Fernández Casado (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param {Number} speed Es el valor con la que se muestra en milisegundos.
    @example document.querySelector("button + ul").show()
    @example document.querySelector("button + ul").show(300)
    @returns {HTMLElement} El propio elemento
**/
HTMLElement.prototype.show = function(speed){
    if(this){
        if(typeof speed == undefined || speed == 0){
            this.style.display = ""

        } else {
            this.style.transition = "";
            this.style.display = "block";
            const height = this.offsetHeight + "px";
            this.style.transition = "max-height " + speed + "ms ease";
            this.style.maxHeight = "0px";
            this.style.overflow = "hidden"; 
            
            setTimeout(function(){ this.style.maxHeight = height; }.bind(this), 0);
            setTimeout(function(){ 
                this.style.maxHeight = "";
                this.style.transition = "";
                this.style.overflow = ""; 
                this.style.display = "";
            }.bind(this), speed);
        }
    }

    return this
}

Y el método para ocultar podría ser:

/**
    @description Oculta un elemento
    @version 1.0
    @author Pablo E. Fernández Casado (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param {Number} speed Es el valor con la que se oculta en milisegundos.
    @example document.querySelector("button + ul").hide()
    @example document.querySelector("button + ul").hide(300)
    @returns {HTMLElement} El propio elemento 
**/
HTMLElement.prototype.hide = function(speed){
	if(this){
		if(typeof speed == undefined || speed == 0){
			this.style.display = "none"

		} else {
            this.style.transition = "";
            this.style.display = "block";
			this.style.maxHeight = this.offsetHeight + "px";
			this.style.transition = "max-height " + speed + "ms ease";
            this.style.overflow = "hidden";
            
            setTimeout(function(){ this.style.maxHeight = "0px"; }.bind(this), 0);
			setTimeout(function(){
                this.style.maxHeight = "";
                this.style.transition = "";
                this.style.overflow = "";
                this.style.display = "none";
            }.bind(this), speed);
		}
    }
}

Seguro que a muchos les resultará familiar esta funcionalidad, pues es similar a la de jQuery, pero sin la sobrecarga del DOM, los problemas de seguridad y la bajada de rendimiento. Además, ambos métodos, admiten un parámetro opcional que nos permite establecer un valor numérico con la velocidad con la que se muestra. Si no está establecido o es cero, sólo se elimina o establece la propiedad display en el elemento seleccionado como una propiedad en el estilo en línea. Sin embargo, si el valor es distinto de cero, se provocará una animación que manipula la propiedad de la altura del propio elemento, entre otras más.

Genial. ¿Y ahora qué? Pues creo que ya va siendo hora de meternos en faena y realizar alguna funcionalidad algo más compleja. Si bien, ya tenemos una función abreviada para la selección de elementos y algunas funciones como recuperar los padres de un elemento, el primer o último elemento de un NodeList, si está o no visible y la posibilidad de mostrarlos u ocultarlos a través de JavaScript, ahora, podríamos hacer una funcionalidad que nos indique si la página (o dicumento) está cargada o no.

Es evidente que no podemos hacer esta comprobación con la intefaz de HTMLElement, pero sí con con la interfaz de Document, ya que es el punto de entrada al contenido de cualquier página web y, por tanto, a todo lo que contiene el árbol DOM.

Aquí, lo importante es controlar el núnmero de condicionantes a controlar y lanzar un evento personalizado cuando todas las premisas se hayan cumplido. Dicho esto, el código podría quedar como:

/**
    @description Componente que ejecuta las funciones contenidas cuando la página no tenga ningún subproceso ni petición pendiente.
    @version 1.0
    @author Pablo E. Fernández (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param fnWhenReady - Una función con lo que se desea ejecutar cuamdo se cumplan las condiciones
    @external check - Es un objeto que se puede configurar para añadir comprobaciones externas durante el proceso de carga
    @example 
        document.isReady.check = { 'pageReady': 1 }
        document.isReady(function(){
            // Intrucciones
        });
    @returns {void}
  **/
Document.prototype.isReady = function(fnWhenReady){
    let myTimeout;
    let fireCallbacks = function(e){
        // Si no hay más premisas activas, limpiamos un poco el DOM y lanzamos la función o funciones que nos soliciten
        if(document.isReady.IS_READY === true && document.isReady.LIST.length == 0){
            // Ejecutamos las instrucciones solicitadas
            document.addEventListener('isReady', function(){ fnWhenReady(); }, false);
            document.dispatchEvent(document.isReady.event);

            // Limpiamos el DOM
            document.removeEventListener('DOMContentLoaded', fireCallbacks.bind(this, 'DOM'), false);
            window.removeEventListener('load', fireCallbacks.bind(this, 'LOAD'), false);
            if(typeof jQuery == "function"){ jQuery(document).unbind('ajaxComplete'); }

            fnWhenReady = function(){ return false }

            // Regresamos al contexto normal
            clearTimeout(myTimeout);
            return;
        } else {
            myTimeout = setTimeout(function(){
                fireCallbacks(e);
            }, 50)
        }

        // Si todavía hay algo pendiente, decrementamos el control de chequeos pendiente
        // y ponemos en marcha la propiedad de IS_READY para que empiece a comprobar el estado
        document.isReady.COUNTER--;
        document.isReady.LIST.splice(document.isReady.LIST.indexOf(e), 1)

        document.isReady.IS_READY = true;
    };

    // Establecemos todas las premisas a comprobar, incluyendo jQuery
    let init = function(){
        // Para testear las peticiones Ajax de JavaScript
        document.isReady.addControl("READY");
        let intervalDocIsReady = setInterval(function(){
            if(document.readyState === 'complete'){
                fireCallbacks("READY");
                clearInterval(intervalDocIsReady)
            }
        }, 50);
        
        // Para testear el DOM
        document.isReady.addControl("DOM");
        document.addEventListener('DOMContentLoaded', fireCallbacks.bind(this, 'DOM'), false);
        
        // Para testear el evento onload
        document.isReady.addControl("LOAD");
        window.addEventListener('load', fireCallbacks.bind(this, 'LOAD'), false);
        
        // Para testear la carga completa de jQuery
        if(typeof jQuery == "function"){
            document.isReady.addControl("AJAX");
            jQuery(document).ajaxComplete(function(e) {
                fireCallbacks("AJAX");
            });
            
            document.isReady.addControl("ACTIVE");
            let intervaljQueryActive = setInterval(function(){
                if(jQuery.active === 0){
                    fireCallbacks("ACTIVE");
                    clearInterval(intervaljQueryActive)
                }
            }, 50);
        }

        // Para testear funciones o variables externas
        if(typeof document.isReady.check == "object"){
            let intervalsCheck = [];
            for(let key in document.isReady.check){
                document.isReady.addControl("CHECK"+key);

                for(let b in window) { if(b == key) vKey = b; }

                intervalsCheck[key] = setInterval(function(key, vKey){
                    if(typeof window[key] != "function" && vKey == key && window[vKey] == document.isReady.check[key]){
                        fireCallbacks("CHECK"+key);
                        clearInterval(intervalsCheck[key])
                    } 
                    if(typeof window[key] == "function" && window[vKey]() == document.isReady.check[key]){
                        fireCallbacks("CHECK"+key);
                        clearInterval(intervalsCheck[key])
                    }

                }.bind(this, key, vKey), 50);
            }
        }
    };

    init();
}

// Funciones y propiedades necesarias para el correcto funcionamiento
Document.prototype.isReady.addControl = function(p){
    document.isReady.COUNTER++;
    document.isReady.LIST.push(p);
}

Document.prototype.isReady.event = new Event("isReady");

Document.prototype.isReady.IS_READY = false;
Document.prototype.isReady.COUNTER = 0;
Document.prototype.isReady.LIST = [];
Document.prototype.isReady.check = null;

Como he dicho, esta funcionalidad ya es algo más compleja porque, además de ser algo más larga de definir, empieza a requerir múltiples propiedades y métodos para conseguir nuestro objetivo, pero, si nos ponemos a pensar, no es más que un evento onload mejorado que se ejecuta cuando todas las premisas se han cumplido, incluyendo funciones y variables externas.

En lo referente a los parámetros que utiliza, la función interna fnWhenReady sólo se ejecutará cuando las todas las premisas se hayan cumplido y puede ser anónima (como es el caso del ejemplo que se muestra en la propiedad @example de la cabecera del método) o una ya definida con nombre.

El objeto externo check se ha puesto para poder controlar que lo que haya dentro de nuestra llamada a isReady se ejecute cuándo, además, se cumplan otros requisitos como puediera ser que una función denominada IsAllLoaded devolviese true o cuándo se verifique que una variable denominada pageReady fuese 1.

La forma de usar esta funcionalidad podría ser:

/***********************************************************************************************
    Llamada normal sin argumentos a isReady y comprobación de función o variable externa
************************************************************************************************/
document.isReady.check = { 'IsAllLoaded': true }
document.isReady(function(){
    // Intrucciones
});

/****************************************
    Llamada con argumentos a isReady
*****************************************/
document.isReady(fnEnd.bind(null, 'param1', 'param2'));
function fnEnd(e, e2){
    // Impresión de los valores de los parámetros recibidos
    console.log(e, e2)

    // Intrucciones
}

No obstante, recordemos que la primera línea es opcional porque, sólo con la declaración isReady, se controlará si hay llamadas en JavaScript y jQuery pendientes y si los eventos onload y DOMContentLoaded se han disparado.

Y ya que estamos, podríamos hacer una funcionalidad que nos permitiese cargar archivos JS, CSS y JSON a través de la API Fetch para mejorar el FCP de Google. Aquí podríamos usar una función genérica con nombre normal y corriente, perom, en este caso, usaremos el objeto window.

window.importFile = function(url, type){
    function render(text, type){
        switch (type){
            case "text/javascript":
                document.head.insertAdjacentHTML("beforeend", "<style>" + text + "</style>");
                break;
            case "text/css":
                document.head.insertAdjacentHTML("beforeend", "<script type='" + type + "'>" + text + "</script>");
                break;
            default:
                break;
        }

        return text;
    }

    return fetch(url)
    .then(function (response) {
        switch (type){
            case "text/javascript":
                return response.text();
                break;
            case "text/css":
                return response.text();
                break;
            default:
                return response.json();
                break;
        }
        })
        .then(function(data){
            return render(data, type)
        });
}

Y la forma de utilizarlo podría ser algo como:

importFile("./js/isitools.js", "text/javascript");

let json = await importFile("./json/example.json", "application/json");

NOTA:Si deseáis más información sobre JavaScript o mejorar vuestras aplicaciones y webs con este lenguaje podéis adquirir el libro de Domine JavaScript 4ª Edición.

Espero que os haya sido útil... Hasta más vernos.

Sobre el autor

Pablo Enrique Fernández Casado

CEO de IslaVisual, Manager, Full Stack Analyst Developer y formador por cuenta ajena con más de 25 años de experiencia en el campo de la programación y más de 10 en el campo del diseño, UX, usabilidad web y accesibilidad web. También es escritor y compositor de música, además de presentar múltiples soft kills como la escucha activa, el trabajo en equipo, la creatividad, la resiliencia o la capacidad de aprendizaje, entre otras.

Especializado en proveer soluciones integrales de bajo coste y actividades de consultoría de Usabilidad, Accesibilidad y Experiencia de Usuario (UX), además de ofrecer asesoramiento en SEO, optimización de sistemas y páginas web, entre otras habilidades.