materials

Eventos

Introducción

Nos permiten detectar acciones que realiza el usuario o cambios que suceden en la página y reaccionar en respuesta a ellas. Existen muchos eventos diferentes (podéis ver la lista en w3schools) aunque nosotros nos centraremos en los más comunes.

Javascript nos permite ejecutar código cuando se produce un evento (por ejemplo el evento click del ratón) asociando al mismo una función. Hay varias formas de hacerlo.

Cómo escuchar un evento

La primera manera “estándar” de asociar código a un evento era añadiendo un atributo con el nombre del evento a escuchar (con ‘on’ delante) en el elemento HTML. Por ejemplo, para ejecutar código al producirse el evento ‘click’ sobre un botón se escribía:

<input type="button" id="boton1" onclick="alert('Se ha pulsado');" />

Una mejora era llamar a una función que contenía el código:

<input type="button" id="boton1" onclick="clicked()" />
function clicked() {
  alert('Se ha pulsado');
}

Esto “ensuciaba” con código la página HTML por lo que se creó el modelo de registro de eventos tradicional que permitía asociar a un elemento HTML una propiedad con el nombre del evento a escuchar (con ‘on’ delante). En el caso anterior:

document.getElementById('boton1').onclick = funnction () {
  alert('Se ha pulsado');
}
...

NOTA: hay que tener cuidado porque si se ejecuta el código antes de que se haya creado el botón estaremos asociando la función al evento click de un elemento que aún no existe así que no hará nada. Para evitarlo siempre es conveniente poner el código que atiende a los eventos dentro de una función que se ejecute al producirse el evento load de la ventana. Este evento se produce cuando se han cargado todos los elementos HTML de la página y se ha creado el árbol DOM. Lo mismo habría que hacer con cualquier código que modifique el árbol DOM. El código correcto sería:

window.onload = function() {
  document.getElementById('boton1').onclick = function() {
    alert('Se ha pulsado');
  }
}

Event listeners

La forma recomendada de hacerlo es usando el modelo avanzado de registro de eventos del W3C. Se usa el método addEventListener que recibe como primer parámetro el nombre del evento a escuchar (sin ‘on’) y como segundo parámetro la función a ejecutar (OJO, sin paréntesis) cuando se produzca:

document.getElementById('boton1').addEventListener('click', pulsado);
...
function pulsado() {
  alert('Se ha pulsado');
})

Habitualmente se usan funciones anónimas ya que no necesitan ser llamadas desde fuera del escuchador:

document.getElementById('boton1').addEventListener('click', function() {
  alert('Se ha pulsado');
});

Si queremos pasarle algún parámetro a la función escuchadora (cosa bastante poco usual) debemos usar funciones anónimas como escuchadores de eventos:

NOTA: igual que antes debemos estar seguros de que se ha creado el árbol DOM antes de poner un escuchador por lo que se recomienda ponerlos siempre dentro de la función asociada al evento window.onload (o mejor window.addEventListener('load', ...) como en el ejemplo anterior).

Una ventaja de este método es que podemos poner varios escuchadores para el mismo evento y se ejecutarán todos ellos. Para eliminar un escuchador se usa el método removeEventListener.

document.getElementById('acepto').removeEventListener('click', aceptado);

NOTA: no se puede quitar un escuchador si hemos usado una función anónima, para quitarlo debemos usar como escuchador una función con nombre.

Tipos de eventos

Según qué o dónde se produce un evento estos se clasifican en:

Eventos de página

Se producen en el documento HTML, normalmente en el BODY:

Eventos de ratón

Los produce el usuario con el ratón:

NOTA: si hacemos doble click sobre un elemento la secuencia de eventos que se produciría es: mousedown -> mouseup -> click -> mousedown -> mouseup -> click -> dblclick

EJERCICIO: Pon un escuchador desde la consola al botón 1 de la página de ejemplo de DOM para que al hacer click se muestre el un alert con ‘Click sobre botón 1’. Ponle otro para que al pasar el ratón sobre él se muestre ‘Entrando en botón 1’.

Eventos de teclado

Los produce el usuario al usar el teclado:

NOTA: el orden de secuencia de los eventos es: keyDown -> keyPress -> keyUp

Eventos de toque

Se producen al usar una pantalla táctil:

Eventos de formulario

Se producen en los formularios:

Los objetos this y event

Al producirse un evento se generan automáticamente en su función manejadora 2 objetos:

Lo mejor para familiarizarse con los diferentes eventos es consultar los ejemplos de w3schools.

EJERCICIO: Pon desde la consola un escuchador al BODY de la página de ejemplo para que al mover el ratón en cualquier punto de la ventana del navegador, se muestre en algún sitio (añade un DIV o un P al HTML) la posición del puntero respecto del navegador y respecto de la página.

EJERCICIO: Pon desde la consola un escuchador al BODY de la página de ejemplo para que al pulsar cualquier tecla nos muestre en un alert el key y el keyCode de la tecla pulsada. Pruébalo con diferentes teclas

Bindeo del objeto this

En ocasiones no queremos que this sea el elemento sobre quien se produce el evento sino que queremos conservar el valor que tenía antes de entrar a la función escuchadora. Por ejemplo la función escuchadora es un método de una clase en this tenemos el objeto de la clase sobre el que estamos actuando pero al entrar en la función perdemos esa referencia.

El método .bind() nos permite pasarle a una función el valor que queremos darle a la variable this dentro de dicha función. Por defecto a una función escuchadora de eventos se le bindea le valor de event.currentTarget. Si queremos que tenga otro valor se lo indicamos con .bind():

document.getElementById('acepto').removeEventListener('click', aceptado.bind(variable));

En este ejemplo el valor de this dentro de la función aceptado será variable. En el ejemplo que habíamos comentado de un escuchador dentro de una clase, para mantener el valor de this y que haga referencia al objeto sobre el que estamos actuando haríamos:

document.getElementById('acepto').removeEventListener('click', aceptado.bind(this));

por lo que el valor de this dentro de la función aceptado será el mismo que tenía fuera, es decir, el objeto.

Podemos bindear, es decir, pasarle a la función escuchadora más variables declarándolas como parámetros de bind. El primer parámetro será el valor de this y los demás serán parámetros que recibirá la función antes de recibir el parámetro event que será el último. Por ejemplo:

document.getElementById('acepto').removeEventListener('click', aceptado.bind(var1, var2, var3));
...
function aceptado(param1, param2, event) {
  // Aquí dentro tendremos los valores
  // this = var1
  // param1 = var2
  // param2 = var3
  // event es el objeto con la información del evento producido
}

Propagación de eventos (bubbling)

Normalmente en una página web los elementos HTML se solapan unos con otros, por ejemplo, un <span> está en un <p> que está en un <div> que está en el <body>. Si ponemos un escuchador del evento click a todos ellos se ejecutarán todos ellos, pero ¿en qué orden?.

Pues el W3C establecíó un modelo en el que primero se disparan los eventos de fuera hacia dentro (primero el <body>) y al llegar al más interno (el <span>) se vuelven a disparar de nuevo pero de dentro hacia afuera. La primera fase se conoce como fase de captura y la segunda como fase de burbujeo. Cuando ponemos un escuchador con addEventListener el tercer parámetro indica en qué fase debe dispararse:

Por tanto, por defecto se disparará el escuchador más interno (el del <span>) y continuará el resto hasta el más externo (<body>) como si fuera una burbuja que sale afuera desde el interior.

Podéis ver un ejemplo en:

Sin embargo si al método .addEventListener le pasamos un tercer parámetro con el valor true el comportamiento será el contrario, lo que se conoce como captura y el primer escuchador que se ejecutará es el del <body> y el último el del <span> (podéis probarlo añadiendo ese parámetro a los escuchadores del ejemplo anterior).

En cualquier momento podemos evitar que se siga propagando el evento ejecutando el método .stopPropagation() en el código de cualquiera de los escuchadores.

Podéis ver las distintas fases de un evento en la página domevents.dev.

innerHTML y escuchadores de eventos

Si cambiamos la propiedad innerHTML de un elemento del árbol DOM todos sus escuchadores de eventos desaparecen ya que es como si se volviera a crear ese elemento (y los escuchadores deben ponerse después de crearse).

Por ejemplo, tenemos una tabla de datos y queremos que al hacer doble click en cada fila se muestre su id. La función que añade una nueva fila podría ser:

function renderNewRow(data) {
  let miTabla = document.getElementById('tabla-datos');
  let nuevaFila = `<tr id="${data.id}"><td>${data.dato1}</td><td>${data.dato2}...</td></tr>`;
  miTabla.innerHTML += nuevaFila;
  document.getElementById(data.id).addEventListener('dblclick', event => alert('Id: '+ event.target.id));

Sin embargo esto sólo funcionaría para la última fila añadida ya que la línea miTabla.innerHTML += nuevaFila equivale a miTabla.innerHTML = miTabla.innerHTML + nuevaFila. Por tanto estamos asignando a miTabla un código HTML que ya no contiene escuchadores, excepto el de nuevaFila que lo ponemos después de hacer la asignación.

La forma correcta de hacerlo sería:

function renderNewRow(data) {
  let miTabla = document.getElementById('tabla-datos');
  let nuevaFila = document.createElement('tr');
  nuevaFila.id = data.id;
  nuevaFila.innerHTML = `<td>${data.dato1}</td><td>${data.dato2}...</td>`;
  nuevaFila.addEventListener('dblclick', event => alert('Id: ' + event.target.id) );
  miTabla.appendChild(nuevaFila);

De esta forma además mejoramos el rendimiento ya que el navegador sólo tiene que renderizar el nodo correspondiente a la nuevaFila. Si lo hacemos como estaba al principio se deben volver a crear y a renderizar todas las filas de la tabla (todo lo que hay dentro de miTabla).

Delegación de eventos

Es un patrón de diseño que nos permite no tener que poner un escuchador a cada elemento sino uno global que haga el trabajo de todos.

Por ejemplo si queremos escuchar cuándo hacemos click en cada celda de la tabla en lugar de poner un escuchador en cada una (que podría tener cientos) pongo sólo 1 en la tabla y mediante la propiedad event.target puede saber sobre qué celda en concreto se ha hecho click. Esto además seguirá funcionando si dinámicamente añado nuevas celdas a la tabla ya que no son ellas las que tienen el escuchador sino la propia tabla.

NOTA: ten en cuenta que a veces el evento se produce en alguna etiqueta interna y event.target no es el elemento que buscamos. Por ejemplo si hay una imagen en la celda el event.target podría ser la <img> y no la <td>. Para asegurarnos de llegar al elemento deseado podemos usar el selector closest() que vimos en el DOM.

Podéis ver más ejemplos de delegación de eventos en El Tutorial de JavaScript Moderno.

Eventos personalizados

También podemos mediante código lanzar manualmente cualquier evento sobre un elemento con el método dispatchEvent() e incluso crear eventos personalizados, por ejemplo:

const event = new Event('build');

// Listen for the event.
elem.addEventListener('build', (e) => { /* ... */ });

// Dispatch the event.
elem.dispatchEvent(event);

Incluso podemos añadir datos al objeto event si creamos el evento con new CustomEvent(). Podéis obtener más información en la página de MDN.