Javascript y Eventos. Todo lo que necesitas saber

Resumen de cómo funcionan los eventos en Javascript y su propagación por el DOM

Gerardo Fernández
Nov 2, 2019 · 7 min read

En este artículo quiero presentar una guía detallada de cómo funcionan los eventos en Javascript ya que creo que, pese a ser una de las características de Javascript con la que más acostumbrados a trabajar estamos, su comportamiento y la forma en que tienen lugar y son procesados es muchas veces algo por lo que pasamos por alto.

Así que, ¡vamos a ello!


¿Qué son los eventos?

Empecemos por lo fundamental. Los eventos os objetos que implementan la interfaz Event definida dentro del estándar del DOM.

Este tipo de objetos son enviados tanto por el user agent (a menudo el navegador) o por la propia aplicación y son recibidos y procesados por los Event Listeners. Su principal función es permitir responder a las interacciones de los usuarios o cambios en la red.

Pese a haber muchos tipos de eventos (los cuales podéis consultar en la web de MDN) en este artículo me centraré principalmente en los referidos al DOM, ya que son con los que probablemente estemos más familiarizados y los que más usemos en el día a día.


¿Cómo se propagan los eventos?

Para entender la forma en que se propagan los eventos atenderemos al siguiente código:

<div id="container">
<div id="dataList">
<div id="clickedData"></div>
</div>
<button id="dataSender">Add new data</button>
</div>

que presentará la siguiente estructura para el DOM:

Cuando trabajamos con eventos, es posible añadir Event Listeners a cada uno de estos elementos mediante Javascript gracias a que todos ellos implementan la interfaz EventTarget . Para ello, basta con seleccionar el elemento (por ejemplo mediante querySelector ) y a continuación emplear sobre dicho elemento el método addEventListener(type, callback, options) para añadir el listener:

  • type representa el nombre del evento que queremos escuchar,
  • callback la función que será invocada cuando se reciba el evento,
  • options es un objeto para modificar el comportamiento por defecto del listener.

Por ejemplo, podemos añadir el evento click al botón con id dataSender para imprimir por consola el mensaje que queramos:

const button = document.querySelector('#dataSender');
button.addEventListener('click', function(event) {
console.log('button clicked');
});

De este modo, cada vez que pulsemos el botón obtendremos en la consola el texto button clicked . ¿Fácil verdad?

La forma en que el evento se propaga cuando realizamos un click consta de 3 fases:

  • Capture, donde el árbol del DOM es recorrido hacia abajo hasta alcanzar un EventTarget , en nuestro caso el botón dataSender
  • On Target, fase en la cual se alcanza el Event target
  • Bubble, en el cual el evento “sube hacia arriba” como si de una burbuja se tratara.

Si por ejemplo, en vez de un solo listener adjuntado al botón dataSender añadiésemos también a los elementos body y container :

const body = document.querySelector('body');
const container = document.querySelector('#container');
const button = document.querySelector('#dataSender');
body.addEventListener('click', function(event) {
console.log('body clicked');
});
container.addEventListener('click', function(event) {
console.log('container clicked');
});
button.addEventListener('click', function(event) {
console.log('button clicked');
});

El proceso que seguiría el evento sería el siguiente:

De modo que obtendríamos en consola lo siguiente:

"button clicked""container clicked""body clicked"

Es decir, primero el listener del botón dataSender , posteriormente el de container y finalmente el del body , de ahí el nombre de propagación en burbuja.

Por defecto, cuando añadimos un eventListener a un elemento, éste es llamado durante la fase de bubble. Si queremos realizar alguna acción durante las dos fases anteriores deberemos declararlo del siguiente modo:

button.addEventListener('click', function(event) { 
console.log('button clicked (one)');
}, true);

Por supuesto es posible añadir múltiples eventos (aunque sean del mismo tipo) a un mismo elemento:

const button = document.querySelector('#dataSender');
button.addEventListener('click', function(event) {
console.log('button clicked (one)');
});
button.addEventListener('click', function(event) {
console.log('button clicked (two)');
});

En este caso los listeners son invocados en el orden en que fueron declarados.

También podemos eliminar un listener si ya no lo necesitamos:

const button = document.querySelector('#dataSender');
function onButtonClicked(event) {
console.log('button clicked (one)');
}
button.addEventListener('click', onButtonClicked);
button.removeEventListener('click', onButtonClicked);

¿Y cómo detenemos la propagación?

En el caso de que queramos detener la propagación del evento en algún punto determinado del DOM podremos emplear los métodos event.stopPropagation() o event.stopImmediatePropagation() . Vamos a ver sus diferencias.

stopPropagation()

El método stopPropagation() lo podemos invocar sobre un objeto evento. Para ver su funcionamiento recurriremos al siguiente ejemplo:

const body = document.querySelector('body');
const container = document.querySelector('#container');
const button = document.querySelector('#dataSender');
body.addEventListener('click', function(event) {
console.log('body clicked');
});
container.addEventListener('click', function(event) {
console.log('container clicked');
});
button.addEventListener('click', function(event) {
event.stopPropagation();
console.log('button clicked');
});
button.addEventListener('click', function(event) {
console.log('button clicked (two)');
});

En el primer listener del botón dataSender estamos deteniendo la propagación mediante la instrucción event.stopPropagation lo cual provoca el evento no “suba en burbuja” hacia los elementos superiores:

Sin embargo, el segundo listener que tenemos asociado al botón sí se ejecuta.

stopImmediatePropagation()

Sin embargo, si queremos prevenir que se ejecuten otros listener asociados al botón podremos emplear el método stopImmediatePropagation de modo que:

const body = document.querySelector('body');
const container = document.querySelector('#container');
const button = document.querySelector('#dataSender');
body.addEventListener('click', function(event) {
console.log('body clicked');
});
container.addEventListener('click', function(event) {
console.log('container clicked');
});
button.addEventListener('click', function(event) {
event.stopImmediatePropagation();
console.log('button clicked');
});
button.addEventListener('click', function(event) {
console.log('button clicked (two)');
});

En este caso, el segundo listener ya no recibe el evento:

Oye… ¿Y el famoso preventDefault()?

Cuando trabajamos con eventos en Javascript es muy común ver el siguiente código:

function(event) {
event.preventDefault();
event.stopPropagation();
}

El método preventDefault lo que hace es decir al navegador que no ejecute el comportamiento por defecto del evento recibido.

Esto es muy común cuando empleamos por ejemplo etiquetas <a> como botones en vez de como enlaces (ejem) de modo que si tenemos el siguiente HTML:

<a id="myLink" href="https://somedomain.invalid">Link</a>

y añadimos el siguiente código Javascript:

const link = document.querySelector('#myLink');
link.addEventListener('click', function(event) {
event.preventDefault();
// do something cool
});

Lo que estaremos haciendo es prevenir que el navegador vaya hacia la url del enlace cuando el usuario haga click sobre él.

Otro caso de uso habitual es para controlar el envío de formularios al pulsar sobre el botón de enviar (por ejemplo, para enviarlos por medio de una llamada AJAX).


Lanzando eventos manualmente

Si bien estamos acostumbrados a que sea el navegador quien se encargue de lanzar los eventos según las interacciones de los usuarios, también es posible crear y lanzar eventos manualmente.

Esto es tan sencillo como crear un nuevo objeto de la clase CustomEvent y lanzarlo mediante la función dispatchEvent . El constructor de la clase CustomEvent presenta la siguiente signature:

event = new CustomEvent(type [, eventInitDict])

El segundo argumento nos permite rellenar la propiedad detail para establecer nuestros propios datos:

const clickedData = document.querySelector('#clickedData');
const anEvent = new CustomEvent('dataSent', {
detail: { foo: 'bar' },
});
clickedData.dispatchEvent(anEvent);

Cuando lanzamos un evento de esta manera el recorrido es similar al que sucede cuando se lanza desde el navegador salvo por el hecho de que no hay propagación en burbuja, pues por defecto todos los eventos creados mediante la instrucción new CustomEvent tienen puesta la propiedad bubbles a false :

const body = document.querySelector('body');
const container = document.querySelector('#container');
const button = document.querySelector('#dataSender');
const clickedData = document.querySelector('#clickedData');
body.addEventListener('clickedData', function(event) {
console.log('body listener');
});
container.addEventListener('clickedData', function(event) {
console.log('container listener');
});
button.addEventListener('clickedData', function(event) {
console.log('button listener');
});
const anEvent = new CustomEvent('clickedData', {
detail: { foo: 'bar' },
});
clickedData.dispatchEvent(anEvent);

El recorrido del evento es el siguiente:

En el caso de que queramos que el evento se propague deberemos crearlo del siguiente modo:

const anEvent = new CustomEvent('clickedData', {
bubbles: true,
detail: { foo: 'bar' },
});
Photo by Zdeněk Macháček on Unsplash

Conclusiones

Como habéis podido ver detrás de la instrucción addEventListener hay bastante lógica detrás que conviene saber por si en algún momento necesitamos crear nuestros propios eventos o profundizar en su uso para determinados caso.

Además, la posibilidad que tenemos de crear nuestros propios eventos nos permite conectar los distintos componentes de nuestra aplicación y pasar información entre ellos de una forma distinta a la propuesta por otros patrones.


¿Quieres recibir más artículos como este?

Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇

Gerardo Fernández

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade