Manejando eventos en JavaScript

By dedalo534

Introducción

Ahora que nos sentimos confortables con el uso de CSS para colocar componentes, y manipular sus estilos, y hemos dado los primeros pasos en JavaScript, comprendiendo variables, funciones, métodos, etc, es hora de empezar a usar ese conocimiento, proporcionando a los visitantes de nuestras páginas interactividad y comportamientos dinámicos (algo así como hacer drag&drop, animaciones, etc). Manejar eventos en JavaScript nos permitirá tener un cierto rol de Doctor Frankenstein, y dar vida a nuestras creaciones.

Pero ya basta acerca de las maravillas de JavaScript — este artículo será práctico, nos dirá cuáles son los eventos, y como hacer uso de ellos en nuestras páginas. La tabla de contenidos es la siguiente:

Hay que tener en cuenta que se puede descargar el código de ejemplo de este artículo, y probar por nosostros mismos.

¿Qué son los eventos?

Los eventos ocurren cuando algún tipo de interacción tiene lugar en una página web. Puede ser el usuario final, haciendo click sobre algo, moviendo el ratón sobre un determinado elemento, o presionando determinadas teclas del teclado. Un evento también puede ser algo que ocurre en el navegador, como que la página haya terminado de cargarse, o que usuario se mueve por el scroll, o redimensiona la página.

A través del uso de JavaScript, podemos detectar cuando determinados eventos ocurren, y realizar acciones en respuesta a esos eventos.

Cómo funcionan los eventos

Cuando los eventos tiene lugar, asociado a un elemento HTML de una página web, se chequea si existen gestores de eventos asociados a ese evento. Si la respuesta es si, se les llama en un orden determinado, mientras se les envía referencias y otra información del evento que ha ocurrido. Los gestores de eventos entonces actúan sobre ese evento.

Hay dos tipos de órdenes de invocación: captura de evento y propagación de evento.

La captura de eventos comienza con el elemento de más afuera en el árbol DOM, y funciona entrando hacia dentro del elemento HTML. Por ejemplo, un click sobre una página web primero chequearía el elemento HTML en busca de un gestor para el evento onclick, luego sobre elemento body, y así, hasta que alcanzase el gestor del evento.

La propagación de los eventos trabaja justo de la forma contraria: comienza chequeando si el elemento destino del evento tiene asociado algún gestor de ese evento, y continúa hacia arriba chequeando a los respectivos padres de los elementos, hasta que alcanza el elemento HTML.

La evolución de los eventos

En los primeros dias de usar JavaScript, usamos gestores de eventos directamente en el elemento HTML, como estos:

<a href="http://www.opera.com/" onclick="alert('Hello')">Say hello</a>

El problema de este enfoque es que esto resultó en gestores de eventos repartidos por todo el código, sin tener un control centralizado, y perdiendo la capacidad de los navegadores de almacenar en caché los ficheros externos de JavaScript.

El siguiente paso en la evolución de eventos fue manejar eventos desde dentro de un bloque JavaScript, por ejemplo:

<script type="text/javascript">
  document.getElementById("my-link").onclick = waveToAudience;
    function waveToAudience() {
      alert("Waving like I've never waved before!");
    }
</script>

<a id="my-link" href="http://www.opera.com/">My link</a>

Hay que hacer notar la limpieza del HTML del último ejemplo. Esto se conoce generalmente como JavaScript discreto. El beneficio de esto, además del cacheo de JavaScript y del control del código, es la separación del código: se tienen todos los contenidos en un sitio, y el código que permite la interacción en otro sitio. Esto permite un enfoque más accesible, cuando un enlace funciona perfectamente con JavaScript desactivado; esto es algo que piden por favor los motores de búsquedas.

Eventos de Nivel 2 del árbol DOM

Sobre noviembre del año 2000, la especificación Document Object Model (DOM) Level 2 Events fue lanzada desde W3C, ofreciendo una vía mas detallada y granular de controlar eventos en una página web. La nueva vía aplica eventos a elementos HTML más o menos de la siguiente forma:

document.getElementById("my-link").addEventListener("click", myFunction, false);

El primer parámetro del método addEventListener es el nombre del evento, y se debería notar que ya no se utiliza el prefijo “on”. El segundo parámetro hace referencia a la función que nosotros queremos que se llame cuando el evento ocurra. El tercer parámetro controla el tipo de invocación del evento, por ejemplo, si el evento es en captura, o en propagación.

La contrapartida de addEventListener es removeEventListener, que elimina un evento asociado a un elemento HTML.

El modelo de eventos de Internet Explorer, la excepción

Por desgracia, Internet Explorer está muy lejos de implementar eventos de nivel 2 del árbol DOM, y como contrapartida tiene su propio método attachEvent. Que queda más o menos como sigue:

document.getElementById("my-link").attachEvent("onclick", myFunction);

Hay que hacer notar que attachEvent todavía utiliza el prefijo “on” antes del nombre del evento actual, y que no incluye soporte para decidir si el evento es en captura o en propagación.

El contrario de attachEvent es detachEvent, para eliminar un evento asociado a un elemento HTML.

La aplicación de eventos multi-navegador

Con todas las inconsistencias entre navegadores web, en la implementación de la gestión de eventos, ha habido numerosos intentos de desarrolladores de páginas web de ofrecer una buena solución, para gestionar eventos de forma correcta en cualquier navegador. Estas soluciones tienen diferentes pros y contras, y se conocen popularmente como funciones addEvent.

La mayor parte de librerias JavaScript las tienen integradas, y además, hay un buen número de soluciones independientes disponibles online. Una sugerencia es usar addEvent por Dean Edwards; también se puede considerar echar un vistazo a event handling options with the jQuery JavaScript library.

Eventos y accesibilidad

Antes de que vayamos más al grano sobre controlar y gestionar eventos, hay que enfatizar el concepto de accesibilidad. Para la mayoría de la gente es un término muy amplio; para nosotros debería significar que lo que queramos hacer a través de la gestión de eventos, realmente debe funcionar aunque JavaScript esté deshabilitado o bloqueado en los navegadores.

Alguna gente deshabilita JavaScript en los navegadores, pero con más frecuencia proxys, firewalls, y otros programas antivirus hacen que JavaScript no se comporte como se espera. No hay que dejar que esto nos desanime, sino que hay que seguir una guía para gestionar eventos que tengan un comportamiento por defecto en caso de que JavaScript no esté disponible.

Por norma general, nunca aplicar eventos a elementos HTML que no traen un comportamiento predefinido para un evento en concreto. Deberíamos aplicar solamente eventos como onclick a elementos como a, que ya tiene un comportamiento por defecto para los eventos de click (por ejemplo, navegar hacia el destino especificado en el enlace, o enviar un formulario).

Controlando eventos

Empecemos con un ejemplo simple de un evento, y cómo podemos reaccionar a él. En aras de la simplicidad, usaremos la solución addEvent mostrada anteriormente, para evitar tener que entrar en las complejidades de los eventos multi-navegador en cada ejemplo.

El primer ejemplo es el evento onload, que pertenece al objeto window. Normalmente, cualquier evento que afecta a la ventana del navegador (como onload, onresize y onscroll) está disponible a través del objeto window.

El evento onload tiene luegar cuando toda la página web ha terminado de cargar. Esto incluye el código HTML en sí, así como dependencias externas como imagenes, ficheros CSS y ficheros JavaScript. Cuando todos ellos han terminado de cargar, se llama a window.onload, y así podemos desencadenar funcionalidad sobre la página web para que ocurra. El siguiente ejemplo es muy simple, y genera un mensaje de alerta para que aparezca cuando la página ha terminado de cargar:

addEvent(window, "load", sayHi);
function sayHi() {
  alert("Hello there, stranger!");
}

No está tan mal, no? Si queremos, podemos usar la funciones anónimas, eliminando la necesidad de un nombre para nuestra función. Como sigue:

addEvent(window, "load", function () {
  alert("Hello there, stranger!");
});

Aplicando eventos a ciertos elementos

Para conseguir más, debemos empezar por añadir eventos a otros elementos en la página. Por el bien del argumento, supongamos que queremos un evento cada vez que pinchamos sobre un enlace. Combinando esto con lo aprendido anteriormente, esta sería la manera de hacerlo:

addEvent(window, "load", function () {
  var links = document.getElementsByTagName("a");
    for (var i=0; i<links.length; i++) {
      addEvent(links[i], "click", function () {
        alert("NOPE! I won't take you there!");
        // Esta línea es soporte añadido a través de la función addEvent. Mirar más abajo.
      evt.preventDefault();
    });
  }
});

Vale, ¿que acabamos de hacer?. Primero, utilizamos el evento onload para saber cuando la página ha terminado de cargar. Entonces, buscamos todos los enlaces en la página, utilizando el método getElementsByTagName del objeto document. Recorremos la lista de los enlaces, aplicando un evento a cada uno de ellos, que hará una acción cuando ese enlace sea pinchado.

¿Pero que hay acerca de la parte graciosa de que “el enlace no te llevará allí”?. Después de que se haya mostrado el alert, la última línea se lee como que retorna false. Eso significa en este contexto, que devolviendo false previene de ejecutar la acción por defecto. De todas formas, entraremos en otras formas de dictar el comportamiento de los eventos en la última sección de este artículo.

Referencias a objetos Eventos

Para añadir más detalle a nuestra gestión de eventos, podemos tomar diferentes acciones dependiendo de ciertas propiedades del evento que ha ocurrido. Por ejemplo, si estamos tratango un evento onkeypress, quizás querramos que el evento solamente ocurra si el usuario ha presionado la tecla enter, pero no cuando presione otras teclas.

Al igual que con el modelo de eventos, Internet Explorer ha decidido utilizar un objeto global de eventos llamado event para gestionar objetos, mientras que la recomendación del W3C implementaba por el resto de navegadores es pasar un objeto evento correspondiente al evento que ha ocurrido. El mayor problema de implementar la gestión de eventos multi-navegador es obtener una referencia al evento en sí, y una referencia al elemento al que está apuntando el evento. Este código soluciona esto para nosotros:

addEvent(document.getElementById("check-it-out"), "click", eventCheck);
function eventCheck (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : window.event;
  var eventTarget = (typeof eventReference.target !== "undefined")? eventReference.target :
  eventReference.srcElement;
}

La primera linea en la función eventCheck chequea si existe un objeto evento pasado a la función. Si es así, automáticamente se convierte en el primer parámetro de la función, en este ejemplo es el nombre evt. Si no existe, significa que el navegador actual es Internet Explorer, entonces se busca una propiedad global del objeto window llamado event.

La seguna línea busca el destino del evento, en la propia referencia del evento. Si esta no existe, se busca en la propiedad srcElement implementada por Internet Explorer.

Nota: esta gestión y comportamiento están detalladas en la referencia anterior a la función addEvent, donde los objetos evento han sido normalizados para trabajar igual en todos los navegadores. El código de más arriba está escrito para mostrar las diferencias entre navegadores.

Chequeando una propiedad específica de un evento

Pongamos esto en acción. El siguiente ejemplo ejecuta diferente código dependiendo de que tecla se presione:

addEvent(document.getElementById("user-name"), "keyup", whatKey);
function whatKey (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  var keyCode = eventReference.keyCode;
  if (keyCode === 13) {
    // La tecla Enter ha sido presionada.
    // Código para validar el formulario y enviarlo.
  }
  else if (keyCode === 9) {
    // Se ha presionado la tecla Tabulador.
    // Código para, quizás, borrar el campo del formulario.
  }
}

El código de dentro de la función whatKey chequea una propiedad del evento que ha tenido lugar, llamada keyCode, para mirar que tecla se ha presionado sobre el teclado. El número 13 significa que ha sido la tecla Enter, y el número 9 significa que ha sido el Tabulador.

Eventos por defecto, y eventos de propagación

Hay un número de casos en lo que estaremos interesados en detener el comportamiento por defecto de un evento. Por ejemplo, quizás queramos impedir que el usuario envíe un formulario si determinados campos no está rellenos. Lo mismo va para los eventos de propagación, y en esta parte se explicará como podemos tener el control de esas situaciones.

Previniendo el comportamiento por defecto de eventos.

Con el modelo de eventos, y las diferencias entre los objetos eventos, hay dos formas de soportar IE, y el resto de navegadores. Construyendo sobre el código anterior para obtener una referencia al objeto evento, el siguiente listado incluye código para detener el comportamiento por defecto de un enlace, cuando el usuario pincha sobre él:

addEvent(document.getElementById("stop-default"), "click", stopDefaultBehavior);
function stopDefaultBehavior (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  if (eventReference.preventDefault) {
    eventReference.preventDefault();
  }
  else {
    eventReference.returnValue = false;
  }
}

Este tipo de código utiliza algo llamado detección del objeto llamado, para confirmar que un método está disponible actualmente antes de que sea llamado, lo que previene de posibles errores. El método preventDefault está disponible en todos los navegadores, excepto Internet Explorer, y previene de ejecutar la acción por defecto de un evento.

Si ese método no se soporta, entraría por establecer el returnValue a false del objeto evento global, deteniendo el comportamiento por defecto en Internet Explorer.

Deteniendo la propagación de eventos

Teniendo en cuenta la siguiente jerarquía en HTML:

<div>
  <ul>
    <li>
      <a href="http://www.opera.com/">Opera</a>
    </li>
    <li>
      <a href="http://www.opera.com/products/dragonfly/">Opera Dragonfly</a>
    </li>
  </ul>
</div>

Supongamos que tenemos que aplicar el evento onclick a todos los elementos a, li y ul. El evento onclick primero llamaría al gestor de eventos del enlace, luego al de la lista de items, y luego al gestor de eventos de la lista desordenada.

Si el usuario pincha sobre el enlace, normalmente, no querremos llamar a ningun gestor de eventos del elemento padre li, sino que querremos permitir al usuario navegar hasta la página correspondiente. Por tanto, si el usuario pincha sobre el elemento li al lado del enlace, quizás queramos invocar al gestor de eventos para el elemento li, al igual que para el elemento ul.

Hay que destacar que con el Modelo de Eventos de Nivel 2 del árbol DOM, y con la useCapture activada, por ejemplo utilizando captura de eventos, esto puede empezar en la lista sin ordenar, despues en la lista ordenada, y luego en el enlace. Como la captura de eventos no es una opción en Internet Explorer, esta funcionalidad se utiliza muy raramente en la vida real.

A continuación se expone el código para detener la propagación de un evento:

addEvent(document.getElementById("stop-default"), "click", cancelEventBubbling);
function cancelEventBubbling (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  if (eventReference.stopPropagation) {
    eventReference.stopPropagation();
  }
  else {
    eventReference.cancelBubble = true;
  }
}

Ejemplo completo de gestión de eventos

Está disponible una página de ejemplo donde se añade un gestor de eventos, y se previene la acción por defecto del evento, dependiendo de determinados criterios. El gestor de eventos chequea si el formulario está relleno para ser enviado, ó no, dependiendo de si el usuario ha rellenado todos los campos. El código JavaScript es el siguiente:

addEvent(window, "load", function () {
  var contactForm = document.getElementById("contact-form");
  if (contactForm) {
    addEvent(contactForm, "submit", function (evt) {
      var firstName = document.getElementById("first-name");
      var lastName = document.getElementById("last-name");
      if (firstName && lastName) {
        if (firstName.value.length === 0 || lastName.value.length === 0) {
          alert("You have to fill in all fields, please.");
          evt.preventDefault();
        }
      }
    });
  }
});

Resúmen

En este artículo solamente hemos arañado la superficie de la gestión de eventos, esperando que se haya ganado en comprensión de cómo funcionan los eventos. Quizás se debería haber sido más duro con las inconsistencias de los navegadores web, pero es importante conocer esas inconsistencias desde el principio.

now these issues from the start.

Una vez se hayan aceptado esas incosistencias, y aprendido como solventarlas con las soluciones de más arriba, ¡no hay límite para las posibilidades que JavaScript y la gestión de eventos nos proporcionan!.

Ejercicios

  • ¿Qué es un evento?
  • ¿Cuál es la diferencia entre captura de eventos, y propagación de eventos?
  • ¿Es posible tener control de la ejecución de un evento, por ejemplo, deteniendo el comportamiento por defecto? ¿Cómo?
  • ¿Cuál es el problema principal de attachEvent y ámbito, que generó una contestación de la comunidad web de JavaScript?

Sobre el autor

Picture of the article author Robert Nyman

Robert Nyman ha trabajado en el desarrollo de interfaces web durante una década, donde JavaScript ha sido siempre su principal interés. Tiene blog muy apasionado sobre desarrollo web, Robert’s talk

Vive con su querida familia en Suecia, escribiendo durante la noche mientras ellos duermen. Además, alimenta un sueño secreto de conseguir un dia ser un rico apestoso, y tener la oportunidad de escribir un libro sobre cosas reales, más allá de los confines del mundo web.

This article is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported license.

Comments

The forum archive of this article is still available on My Opera.

No new comments accepted.