El pensamiento detrás de la simplificación de los controladores de eventos | Programar Plus

Los eventos se utilizan para responder cuando un usuario hace clic en algún lugar, se enfoca en un enlace con su teclado y cambia el texto en un formulario. Cuando comencé a aprender JavaScript, escribí detectores de eventos complicados. Más recientemente, aprendí a reducir tanto la cantidad de código que escribo como la cantidad de oyentes que necesito.

Comencemos con un ejemplo simple: algunas cajas que se pueden arrastrar. Queremos mostrarle al usuario qué cuadro de color arrastró.

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="dragged">Drag a box</p>

Ver la pluma
Eventos Dragstart por Tiger Oakes (@NotWoods)
en CodePen.

La forma intuitiva de hacerlo

Escribí funciones de escucha de eventos separadas para cada elemento cuando comencé a aprender sobre los eventos de JavaScript. Es un patrón común porque es la forma más sencilla de empezar. Queremos un comportamiento específico para cada elemento, por lo que podemos usar un código específico para cada uno.

document.querySelector('#red').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent="Dragged red";
});

document.querySelector('#yellow').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent="Dragged yellow";
});

document.querySelector('#green').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent="Dragged green";
});

Reducción de código duplicado

Los detectores de eventos en ese ejemplo son todos muy similares: cada función muestra algo de texto. Este código duplicado se puede colapsar en una función auxiliar.

function preview(color) {
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
}

document
  .querySelector('#red')
  .addEventListener('dragstart', evt => preview('red'));
document
  .querySelector('#yellow')
  .addEventListener('dragstart', evt => preview('yellow'));
document
  .querySelector('#green')
  .addEventListener('dragstart', evt => preview('green'));

Esto es mucho más limpio, pero aún requiere múltiples funciones y detectores de eventos.

Aprovechando el objeto Evento

El Event El objeto es la clave para simplificar los oyentes. Cuando se llama a un detector de eventos, también envía un Event objeto como primer argumento. Este objeto tiene algunos datos para describir el evento que ocurrió, como la hora en que ocurrió. Para simplificar nuestro código, podemos usar el evt.currentTarget propiedad donde currentTarget hace referencia al elemento al que está conectado el detector de eventos. En nuestro ejemplo, será uno de los tres cuadros de colores.

const preview = evt => {
  const color = evt.currentTarget.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('#red').addEventListener('dragstart', preview);
document.querySelector('#yellow').addEventListener('dragstart', preview);
document.querySelector('#green').addEventListener('dragstart', preview);

Ahora solo hay una función en lugar de cuatro. Podemos reutilizar exactamente la misma función que un detector de eventos y evt.currentTarget.id tendrá un valor diferente dependiendo del elemento que dispare el evento.

usando burbujeo

Un último cambio es reducir el número de líneas en nuestro código. En lugar de adjuntar un detector de eventos a cada cuadro, podemos adjuntar un solo detector de eventos al <section> elemento que contiene todos los cuadros de colores.

Un evento comienza en el elemento donde se originó el evento (una de las cajas) cuando se dispara. Sin embargo, no se detendrá allí. El navegador va a cada padre de ese elemento, llamando a cualquier detector de eventos en ellos. Esto continuará hasta que el raíz del documento se alcanza (el <body> etiqueta en HTML). Este proceso se denomina “burbujeo” porque el evento se eleva a través del árbol de documentos como una burbuja.

Adjuntar un detector de eventos a la sección hará que el evento de enfoque aparezca como una burbuja desde el cuadro de color que se arrastró hasta el elemento principal. También podemos aprovechar la evt.target propiedad, que contiene el elemento que disparó el evento (uno de los cuadros) en lugar del elemento al que se adjunta el detector de eventos (el <section> elemento).

const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('section').addEventListener('dragstart', preview);

¡Ahora hemos reducido muchos oyentes de eventos a solo uno! Con un código más complicado, el efecto será mayor. Al utilizar el Event object y burbujeo, podemos controlar los eventos de JavaScript y simplificar el código para los controladores de eventos.

¿Qué pasa con los eventos de clic?

evt.target funciona muy bien con eventos como dragstart y change, donde solo hay una pequeña cantidad de elementos que pueden recibir el foco o cambiar la entrada.

Sin embargo, por lo general queremos escuchar click eventos para que podamos responder a un usuario que hace clic en un botón en una aplicación. click los eventos se activan para cualquier elemento del documento, desde grandes divs hasta pequeños tramos.

Tomemos nuestros cuadros de color arrastrables y hagámoslos clicables en su lugar.

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="clicked">Clicked a box</p>
const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#clicked').textContent = `Clicked ${color}`;
};

document.querySelector('section').addEventListener('click', preview);

Ver la pluma
Haga clic en eventos: no funciona del todo por Tiger Oakes (@NotWoods)
en CodePen.

Al probar este código, tenga en cuenta que a veces no se agrega nada a “Hacer clic” en lugar de cuando se hace clic en un cuadro. La razón por la que no funciona es que cada caja contiene un <span> elemento en el que se puede hacer clic en lugar del que se puede arrastrar <div> elemento. Dado que los intervalos no tienen un ID establecido, el evt.target.id propiedad es una cadena vacía.

Solo nos importan los cuadros de colores en nuestro código. Si hacemos clic en algún lugar dentro de un cuadro, necesitamos encontrar el elemento del cuadro principal. Nosotros podemos usar element.closest() para encontrar el padre más cercano al elemento en el que se hizo clic.

const preview = evt => {
  const element = evt.target.closest('div[draggable]');
  if (element != null) {
    const color = element.id;
    document.querySelector('#clicked').textContent = `Clicked ${color}`;
  }
};

Ver la pluma
Haga clic en eventos: uso de .closest por Tiger Oakes (@NotWoods)
en CodePen.

Ahora podemos usar un solo oyente para click ¡eventos! Si element.closest() devoluciones null, eso significa que el usuario hizo clic en algún lugar fuera de un cuadro de color y debemos ignorar el evento.

Más ejemplos

Estos son algunos ejemplos adicionales para demostrar cómo aprovechar un solo detector de eventos.

Liza

Un patrón común es tener una lista de elementos con los que se puede interactuar, donde los nuevos elementos se insertan dinámicamente con JavaScript. Si tenemos detectores de eventos adjuntos a cada elemento, entonces su código tiene que tratar con detectores de eventos cada vez que se genera un nuevo elemento.

<div id="buttons-container"></div>
<button id="add">Add new button</button>
let buttonCounter = 0;
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.textContent = buttonCounter;
  
  // Make a new event listener every time "Add new button" is clicked
  newButton.addEventListener('click', evt => {

    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${newButton.textContent}`;
  });

  buttonCounter++;

  const container = document.querySelector('#buttons-container');
  container.appendChild(newButton);
});

Ver la pluma
Listas: sin burbujas por Tiger Oakes (@NotWoods)
en CodePen.

Al aprovechar el burbujeo, podemos tener un solo detector de eventos en el contenedor. Si creamos muchos elementos en la aplicación, esto reduce la cantidad de oyentes de n para dos.

let buttonCounter = 0;
const container = document.querySelector('#buttons-container');
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.dataset.number = buttonCounter;
  buttonCounter++;

  container.appendChild(newButton);
});
container.addEventListener('click', evt => {
  const clickedButton = evt.target.closest('button');
  if (clickedButton != null) {
    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${clickedButton.dataset.number}`;
  }
});

Formularios

Quizás hay un formulario con muchas entradas y queremos recopilar todas las respuestas de los usuarios en un solo objeto.

<form>
  <label>Name: <input name="name" type="text"/></label>
  <label>Email: <input name="email" type="email"/></label>
  <label>Password: <input name="password" type="password"/></label>
</form>
<p id="preview"></p>
let responses = {
  name: '',
  email: '',
  password: ''
};

document
  .querySelector('input[name="name"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="name"]');
    responses.name = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="email"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="email"]');
    responses.email = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="password"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="password"]');
    responses.password = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });

Ver la pluma
Formas: sin burbujas por Tiger Oakes (@NotWoods)
en CodePen.

Cambiemos a un solo oyente en el padre <form> elemento en su lugar.

let responses = {
  name: '',
  email: '',
  password: ''
};

document.querySelector('form').addEventListener('change', evt => {
  responses[evt.target.name] = evt.target.value;
  document.querySelector('#preview').textContent = JSON.stringify(responses);
});

Conclusión

Ahora sabemos cómo aprovechar el burbujeo de eventos y el objeto de evento para simplificar complejos revoltijos de controladores de eventos en solo unos pocos… ¡y, a veces, en solo uno! Esperamos que este artículo lo haya ayudado a pensar en sus controladores de eventos bajo una nueva luz. Sé que esto fue una revelación para mí después de pasar mis primeros años de desarrollo escribiendo código duplicado para lograr lo mismo.