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

Ultima revisión 10/01/2023

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

En ocasiones me encuentro con profesionales que afirman que JavaScript tiene límites. No señores, no... los límites están en las personas, no JavaScript.

Cierto es que herramientas como jQuery, MooTools React o Angular ayudan en el proceso de desarrollo, pero gastan una inconmensurable cantidad de memoria y sobrecargan el DOM con cosas que, muchas veces, ni los desarrolladores ni las páginas necesitan. Esto, sin duda, provoca una pérdida sustancial del rendimiento global y la experiencia de usuario haciendo que baje hasta niveles incompresibles.

Y entonces, ¿qué podemos hacer? La respuesta es sencilla, crear nuestro propio framework o, como este es el caso, añadir a JavaScript la funcionalidad personalizada que se necesite.

Por ejemplo. Una de las razones por las que los desarrolladores usan frameworks y librerías es porque les permiten conseguir lo mismo con el mínimo esfuerzo. Esto es, sin ir más lejos, una de las cosas que jQuery consiguió en su día a través de una abreviatura del selector de elementos rápida de escribir y entender. Pero, ¿y por qué se está dejando de usar jQuery? La razón es sencilla, como he comentado, jQuery llena el DOM de métodos que muchas veces no se usan y, además, requiere más memoria y tiempo para hacer lo mismo que una función nativa.

Sin embargo, podemos mejorar la productividad, sin castigar demasiado el rendimiento. ¿Cómo? Mediate una función personalizada. Veámoslo con una definición optimizada de la función $ de jQuery.

/** 
    Componente que permite realizar consultas al DOM como querySelectorAll, pero mejorado.
    @version 1.0
    @author Pablo E. Fernández Casado (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param {void || string || object} selector - Selector CSS o elemento padre desde donde empezar a buscar.
    @param {object || string} parent - Selector CSS que se desea buscar, al igual que hace el método querySelectorAll.
    @returns {NodeList}
 **/
$ = function(selector, parent = document){ 
    parent = typeof parent == "string" ? document.querySelector(parent) : parent;
    let aux = parent.querySelectorAll(selector);
    
    return aux.length == 1 ? aux[0] : aux;
}

HTMLElement.prototype.length = 1;

Este simple código es como el método de jQuery, pero mejorado, ya que puede ser utilizado con todas las características de JavaScript y de la misma forma que si de jQuery se tratase, sin embargo, prácticamente no afecta al rendimiento ya que no sobrecarga el DOM.

¿Y cómo se usa?

if($("p").length == 0){
    // Instrucciones
}

let article = $("article").length != 0 ? $("article") : null;

Muchos pensarán que esto no es, exactamente, una mejora, Cierto, pero ahora, y aprovechando la declaración anterior, podemos crear una función para recuperar los padres de un elemento. Esta es una de esas que tanto usan los desarrolladores cuando no conocen el método closest, o cuando esta función se les queda algo corta. Para ello, nos valdremos de los prototipos de JavaScript, un mecanismo mediante el cual los objetos en JavaScript heredan características entre sí.


/** 
    Componente que permite recuperar todos los ancestros de un elemento en orden ascendente.
    @version 1.0
    @author Pablo E. Fernández Casado (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param {string} sel - Es el selector CSS que se desea coincida con los ancestros.
    @returns {NodeList}
 **/
HTMLElement.prototype.parents = NodeList.prototype.parents = function(sel = '*'){
	let trg = document.createElement("div");
	let aux = this instanceof NodeList ? this[0] : this;

	for (let el = aux.parentElement; el && el !== document; el = el.parentNode ) {
		if ( el.matches( sel ) ) trg.insertAdjacentElement("afterbegin", el.cloneNode());
	}

	return trg.children.length == 1 ? trg.children[0] : trg.childNodes;
}

En teste caso, nos hemos aprovechado de la interfaz HTMLElement y de la API NodeList y, con esta declaración, conseguiremos que todos los elementos del DOM puedan averiguar cuáles son sus ancestros con o sin coincidencia de selector.

Para usarla, sólo tendríamos que hacer algo como:


$("p")[0].parents()
// Esto podría devolver algo como: NodeList(4) [html, body, section, div.text]

$("h1", document.body).parents()
// Esto podría devolver algo como: NodeList(4) [html, body, section, header]

$("p")[0].parents("section")
// Esto podría devolver algo como: <section class="showcase"></section>

¿Y qué podemos hacer ahora?, Pues otra funcionalidad que no viene mal es la de recuperar el primero y último elemento de un NodeList dado. Esto podría ser algo como:


NodeList.prototype.first = function(){ return this[0] }
NodeList.prototype.last = function(){ return this[this.length -1] }

Y ya que estamos, también podría ser interesante saber si un elemento está o no visible. Para ello podríamos hacer algo como:


/** 
    Averiguar si el elemento solicitado está visible o no en pantalla.
    @version 1.0
    @author Pablo E. Fernández (islavisual@gmail.com)
    @Copyright 2017-2023 Islavisual.
    @param {void} void No requiere de ningún parámetro o argumento.
    @returns {boolean} Verdadero o falso en de si está o no visible.
 **/
HTMLElement.prototype.isVisible = function() {
    if (!(this instanceof Element)){ console.info('Element not found!', this); return false; }
    var style = getComputedStyle(this);
    if (style.display === 'none') return false;
    if (style.visibility !== 'visible') return false;
    if (style.opacity < 0.1) return false;
    if (this.offsetWidth + this.offsetHeight + this.getBoundingClientRect().height +
        this.getBoundingClientRect().width === 0) {
        return false;
    }

    var itemProps   = { x: this.getBoundingClientRect().left + this.offsetWidth / 2,
                        y: this.getBoundingClientRect().top + this.offsetHeight / 2 };

    var pointContainer = document.elementFromPoint(itemProps.x, itemProps.y);
    if(!pointContainer && 
        this.closest("ul") &&
        !this.closest("ul").classList.contains("intellimenu") &&
        this.closest("ul").previousElementSibling &&
        !this.closest("ul").previousElementSibling.classList.contains("collapsed")){
        pointContainer = this
    }

    if(pointContainer){
        do {
            if (pointContainer === this) return true;
        } while (pointContainer = pointContainer.parentElement);
    }

    return false;
}

Y para usarla bastaría con hacer una llamada como:


$("p")[5].isVisible();
// Devolverá false o true

El valor devuelto dependerá de si está o no visible, tanto si tiene la propiedad display a none, como si tiene la propiedad visibility a hidden, o si está fuera de la zona visible de la pantalla o tiene un width y/o height de 0px.

La próxima semana continuaré con la parte 2 de mejorando el DOM y cómo añadir funcionalidad a JavaScript sin frameworks.

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.

Ir al artículo Mejorando el DOM (Parte II). Cómo añadir funcionalidad a JavaScript sin frameworks

Sobre el autor

Imagen de Pablo Enrique Fernández Casado
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.