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 manera tradicional 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');
}

Como se trata de poner un atributo al elemento podemos usar DOM para evitar “ensuciar” con código la página HTML:

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

IMPORTANTE: si asociamos un evento a un elemento que aún no existe (porque aún no lo ha renderizado el navegador) no se produce ningún error pero cuando posteriormente se renderice ese elemento no tendrá asociado el evento. Para evitarlo siempre es conveniente poner los escuchadores de los eventos dentro de una función que se ejecute cuando sepamos que ya se ha renderizado toda la página, es decir tras producirse:

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');
  }
}

o mejor

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

Event listeners

Pero esta forma tradicional de poner escuchadores a los eventos lo es la más adecuada. La forma recomendada de hacerlo es usando el modelo avanzado de registro de eventos del W3C, mediante 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 cuando se produzca (OJO, sin paréntesis):

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', () => {
  alert('Se ha pulsado');
});

Si queremos pasarle algún parámetro a la función manejadora (cosa bastante poco usual) debemos usar obligatoriamente 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 una función asociada a window.addEventListener("load", ...) o mejor a document.addEventListener("DOMContentLoaded", ...).

Una ventaja de esta forma de poner escuchadores 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('boton1').removeEventListener('click', pulsado);

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:

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 manejadora. Por ejemplo, si la función manejadora es un método de una clase en this tenemos la instancia de la clase sobre la que estamos actuando pero al entrar en la función manejadora del evento se sobreescribe esta variable.

class ... {
  ...
  escucha() {
    document.getElementById('boton1').addEventListener('click', this.pulsado);
  }

  pulsado(event) {
    // Aquí this debería ser la instancia de la clase, pero si es llamado por la función que escucha el click el evento this será el elemento sobre el que se ha hecho click
  }
}

La forma de solucionarlo es usar el método .bind(), que nos permite pasarle a una función el valor que queremos darle a la variable this dentro de dicha función:

document.getElementById('boton1').removeEventListener('click', this.pulsado.bind(this));

En este ejemplo el valor de this dentro de la función pulsado será this, es decir, la instancia de la clase, en lugar de event.currentTarget (en vez de this le podríamos pasar cualquier otro valor).

Podemos bindear, es decir, pasarle a la función manejadora 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));
...
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
}

Esto es lo que hacíamos en la práctica de DOM cuando le pasábamos a las funciones manejadoras del submit y el click del formulario en la vista métodos del controlador con el objeto this bindeado:

this.view.setBookSubmitHandler(this.handleSubmitBook.bind(this));
this.view.setBookRemoveHandler(this.handleRemoveBook.bind(this));

Sin ese bindeo esos métodos perderían la referencia a la instancia del controlador y no podrían acceder a sus propiedades y métodos.

Otra forma de solucionarlo sin usar bind() es usar funciones flecha, que no tienen su propio this sino que heredan el de la función que las contiene:

class ... {
  ...
  escucha() {
    document.getElementById('boton1').addEventListener('click', (event) => this.pulsado(event));
  }

  pulsado(event) {
    // Aquí this será la instancia de la clase
  }
}

Por tanto en la práctica de DOM podemos sustituir los bind por funciones fecha:

this.view.setBookSubmitHandler((payload) => this.handleSubmitBook(payload));
this.view.setBookRemoveHandler((bookId) => this.handleRemoveBook(bookId));

Propagación de eventos

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 (bubbling). 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

Como los escuchadores de eventos se asocian a un elemento, si lo borramos desaparecerá el escuchador aunque luego lo volvamos a pintar no tendrá escuchador a menos que se lo pongamos de nuevo.

Por ejemplo, si cambiamos el contenido de la propiedad innerHTML de un elemento todos los escuchadores de eventos de sus elementos hijos desaparecen ya que es como eliminar su contenido y volverlo a renderizar.

Eso pasaría en este ejemplo en que tenemos una tabla de datos donde al hacer dobleclick en cada fila se muestra 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 así sólo la última fila añadida tendría escuchador ya que la línea miTabla.innerHTML += nuevaFila borra todo el contenido de myTabla y lo vuelve a renderizar pero ya no tendría escuchadores, excepto el de nuevaFila que lo ponemos después de renderizarlo.

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 y no todas las filas de la tabla como pasaba con el primer código.

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 puedo 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 al elemento por lo que event.target no sería el elemento que buscamos sino su descendiente. 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 (tdClicked = event.target.closest('td')).

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.