Eventos

Comenzamos con este nuevo episodio en el que vamos a trabajar con los eventos, que son sucesos o acciones que ocurrirán en cualquier página web.

Vamos a dar los pasos necesarios desde la base en Javascript hasta conseguir aplicar estos conceptos en Qwik, para poder usarlos en cualquier situación que requieran la participación de ellos.

Contenido que encontraremos en este capítulo

Introducción a los eventos. Definición y características

En el desarrollo web, JavaScript es un lenguaje de programación que nos va a permitir crear páginas web interactivas y dinámicas.

Una parte fundamental de la programación en JavaScript van a ser los eventos, que son acciones o sucesos que ocurren en una página web y que pueden ser detectados y manejados mediante el uso de esta tecnología.

Estos eventos pueden ser provocados por el usuario, el navegador o el propio documento, y nos permitirán que la página web responda de manera activa a la interacción del usuario.

Características de los eventos en JavaScript:

  1. Interactividad: Los eventos permiten que los usuarios interactúen con los elementos de una página web. Al hacer click en botones, enlaces, imágenes, o realizar otras acciones, se activan eventos que pueden desencadenar respuestas específicas.

  2. Detección y Captura: JavaScript es capaz de detectar cuándo ocurre un evento en la página web. A través de métodos como addEventListener, podemos capturar esos eventos y definir funciones que se ejecutarán cuando ocurra el evento específico.

  3. Respuestas Personalizadas: Cada evento puede tener una respuesta personalizada asociada. Esto significa que los desarrolladores pueden programar qué acción se llevará a cabo cuando ocurra un evento determinado. Por ejemplo, mostrar un mensaje, cambiar el contenido de la página o realizar una operación específica.

  4. Amplia Variedad de Eventos: JavaScript ofrece una amplia colección de eventos que se pueden utilizar, desde eventos de clics y teclado hasta eventos de carga de página, cambios en formularios y mucho más.

  5. Mejora de la Experiencia del Usuario: El uso adecuado de eventos en JavaScript permite crear una experiencia más fluida e interactiva para los usuarios, lo que mejora la usabilidad y la sensación de interacción con la página.

  6. Asincronía: Muchos eventos ocurren de forma asíncrona, lo que significa que pueden suceder en cualquier momento mientras el usuario interactúa con la página. Esto permite que la página sea más receptiva y no se bloquee mientras espera una acción del usuario.

Lista de algunos eventos

Lista de algunos de los eventos más usados que estarán disponibles en JavaScript donde se añadirá un ejemplo sencillo para mostrar su comportamiento:

El código:

<button id="myButton">Haz click aquí</button>

<script>
  const boton = document.getElementById('myButton');
  boton.addEventListener('click', () => {
    alert('¡Has hecho click en el botón!');
  });
</script>

Al cargar:

Haciendo click en el botón se ejecuta la acción definida:

El código:

<p>Selecciona y copia el siguiente texto:</p>
<p id="contentToCopy">Texto que puedes copiar.</p>

<script>
  const contenido = document.getElementById('contentToCopy');
  contenido.addEventListener('copy', () => {
    alert('¡Has copiado el texto!');
  });
</script>

Al cargar:

Copiando el contenido Texto que puedes copiar, seleccionándolo y haciendo la acción de copiar (CTRL+C o click derecho Copiar) mostrará una alerta:

  1. mouseenter/ mouseleave: Estos eventos ocurren cuando el cursor del ratón entra (mouseenter) o sale (mouseleave) de un elemento en la página web, respectivamente.

El código:

<div id="myElement" style="width: 100px; height: 50px; background-color: red;"></div>

<script>
  const element = document.getElementById('myElement');
  element.addEventListener('mouseenter', () => {
    element.style.backgroundColor = 'blue';
  });
  
  element.addEventListener('mouseleave', () => {
    element.style.backgroundColor = 'red';
  });
</script>

Al iniciar:

Si ponemos el cursor sobre el elemento rojo:

Si quitamos el foco del elemento azul pasará de nuevo al rojo:

El código:

<input type="text" id="myInput">

<script>
  const input = document.getElementById('myInput');
  input.addEventListener('keydown', (event) => {
    console.log('Tecla pulsada:', event.key);
  });
</script>

Al cargar:

Al añadir el cursor en el campo de texto junto con tres pulsaciones de teclas (e, e y r):

Aparte de estos ejemplos ejecutando eventos en Javascript existen muchos otros eventos que se pueden utilizar para crear experiencias web interactivas y dinámicas.

¿Cómo podemos saber más sobre los eventos existentes? Toda la información sobre los eventos que podemos encontrar la tenéis en el siguiente enlace:

https://shorten-up.vercel.app/NeOCRedUad

Y accediendo a ese contenido, nos encontramos con una tabla con todos los eventos especificados con una explicación:

Hay que tener en cuenta que estos eventos los ejecutaremos, dependiendo del caso y nuestras necesidades mediante el elemento window y en otros casos con document. ¿En que se diferencian? Seguid leyendo y lo descubriréis.

Diferencias entre usar window y document {#differences-window-document}

La diferencia principal entre registrar eventos en window y en document radica en el ámbito de captura del evento.

Elemento Descripcion Ejemplo
window Escucha el evento a nivel global en toda la ventana del navegador. Esto significa que el evento se disparará sin importar en qué parte del documento ocurra la interacción Si registramos el evento 'mousemove' en window, se activará cada vez que se mueva el ratón dentro de la ventana, sin importar en qué elemento HTML específico se encuentre el puntero del ratón.
document Escucha el evento a nivel del documento. ¿Qué significa esto? El evento se va a disparar cuando ocurra la interacción del ratón dentro del contenido del documento HTML. Si hay elementos HTML anidados dentro del documento y el evento ocurre en uno de esos elementos, el evento se propagará desde el elemento objetivo hasta el elemento document Si registramos el evento 'mousemove' en document, mediante la selección de un elemento identificado con un id (document.getElementById('id-elemento')) se activará cada vez que se mueva el ratón dentro de ese elemento específico.

Sabiendo como se diferencian, tenemos que tener claro lo siguiente de manera resumida sobre la captura de eventos aplicándolo al uso del ratón como ejemplo:

  • A nivel global: Movimiento del ratón en cualquier parte de la ventana del navegador, puedes registrarlos en window.
  • Dentro del contenido específico de nuestro documento HTML: Se pueden registar en document o en elementos HTML individuales utilizando sus identificadores, clases o selectores.

Es más eficiente y preciso registrar eventos en elementos específicos del documento utilizando document o seleccionando elementos por su ID, clase o selector.

Ventajas de emplear document:

  • Mayor control sobre qué elementos van a estar escuchando los eventos y así podremos garantizar que esa lógica asociada al evento se ejecutará en la situación/es que deseamos.
  • Reducción de la posibilidad de conflictos o interferencias con otros eventos o funcionalidades en tu aplicación.
  • Si solo necesitas capturar eventos en un área específica de tu página web, no es necesario escuchar eventos a nivel global en todo el window.

Por lo tanto, es una buena práctica utilizar document o seleccionar elementos específicos para registrar eventos, ya que te proporciona mayor control y seguridad en cuanto a dónde se ejecutará la lógica relacionada con los eventos.

Casos en los que se podría usar window {#when-use-window}

El objeto window es útil para registrar eventos que deben ser capturados a nivel global en toda la ventana del navegador. A continuación, os proporciono algunos casos en los que podríamos utilizar window para registrar eventos:

  1. Eventos de redimensionamiento de ventana (resize): Puedes utilizar window para capturar eventos como resize, que se disparan cuando el usuario cambia el tamaño de la ventana del navegador. Esto puede ser útil si necesitas ajustar dinámicamente el diseño de tu página en función del tamaño de la ventana.

  2. Eventos de desplazamiento (scroll): Puedes registrar eventos como scroll en window para detectar cuándo el usuario realiza un desplazamiento vertical u horizontal en la página. Esto puede ser útil si deseas implementar efectos o funcionalidades relacionadas con el desplazamiento.

  3. Eventos de carga (load) y descarga (unload): Puedes utilizar window para registrar eventos como load y unload, que se disparan cuando se carga o se cierra la ventana del navegador. Estos eventos pueden ser útiles para realizar tareas específicas al cargar o cerrar una página, como enviar solicitudes de seguimiento o limpiar recursos.

  4. Eventos de teclado global: Cuando necesitemos capturar eventos de teclado en toda la ventana, como keydown o keyup, podemos registrarlos en window. Útil para implementar atajos de teclado o funcionalidades de accesibilidad en nuestra aplicación.

En resumen, window es útil cuando queremos registrar eventos a nivel global en toda la ventana del navegador, como eventos de redimensionamiento, eventos de desplazamiento, eventos de carga o descarga, o eventos de teclado globales. En estos casos, no es necesario restringir el ámbito de captura del evento a elementos específicos como podrían ser campos de un formulario por ejemplo.

Eventos en Qwik

Introducción

Los eventos en Qwik son los mismos, pero la forma en la que hacemos su llamada es mediante el registro de las funciones de devolución de llamada en la plantilla JSX.

Los manejadores de eventos (Event handlers) se van a registrar utilizando el atributo on{NombreDelEvento}$.

Información acerca de los eventos en Qwik

Os dejo a continuación la referencia principal de la documentación oficial para trabajar con los eventos: https://shorten-up.vercel.app/y05bhcVFpv

Esta información la podéis consultar por si queréis contrastar algún dato concreto.

Por ejemplo, se utiliza el atributo onClick$ para escuchar los eventos de click. Formándose de la siguiente forma:

  • on: Para especificar que se va a crear una acción.
  • Evento que se formará en el tipo de notación PascalCase, por ejemplo si tenemos first name, será FirstName y en el caso de click asignaremos como Click.
  • $: Para especificar el final del evento en Qwik. Esto va a indicar tanto el Optimizador de Qwik como al desarrollador de que ocurre una transformación especial en esta ubicación. La presencia del sufijo $ implica un límite cargado de forma diferida aquí. El código asociado con el manejador de click no se cargará en la VM (Máquina virtual) de JavaScript hasta que el usuario active el evento de click; sin embargo, se cargará en la caché del navegador de forma anticipada para no causar retrasos en las primeras interacciones.

Por lo tanto, esto es lo que tenemos que tener en cuenta:

  • click: on + Click + $ => onClick$

Junto con otros ejemplos:

  • mouseenter: on + MouseEnter + $ => onMouseEnter$
  • mouseleave: on + MouseLeave + $ => onMouseLeave$
  • copy: on + Copy + $ => onCopy$
  • keydown: on + KeyDown + $ => onKeyDown$

Anteriormente, ya hemos visto algún caso como el manejo de la acción del click para forzar cambios de estado y otros ejemplos.

Con esta pequeña introducción vamos a empezar a profundizar con las diferentes formas y variantes que tenemos para trabajar con los eventos en Qwik.

Inline handler - Manejador en línea

El manejador en línea será cuando realicemos el registro del evento mediante un atributo que queramos usar dentro del código JSX, donde ejecutaremos una función para emplear la acción deseada:

Trabajando con el evento click, dentro de un elemento <button> mediante el atributo onClick$ deberemos de implementarlo de la siguiente forma:

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);

  return (
    <button onClick$={() => count.value++}>Incrementar: {count.value}</button>
  );
});

Y como se puede observar, dentro de onClick$ registrado desde el elemento <button> se debe ejecutar una devolución de llamada () => count.value++ cada vez que se dispara el evento de click en este <button>.

Y el siguiente código, lo podremos visualizar de la siguiente forma:


Haciendo 3 clicks sobre la acción onClick$ debería de sumar 3 unidades, quedando 3 como resultado:

Reutilización de manejadores de eventos

Si deseamos reutilizar el mismo manejador de eventos (por ejemplo la acción de click que hemos visto) para varios elementos o eventos, necesitamos importar $ de @builder.io/qwik y envolver el manejador de eventos en él con una función normal y corriente.

De esta manera, debemos extraer el manejador de eventos en un QRL y pasarlo al escuchador de eventos.

¿Qué es QRL?

Es una forma particular de URL (mediante función) que usa Qwik para cargar contenido de manera diferida dentro de los componentes, cuando ya estamos trabajando con el contenido inicial renderizado y usar lo que está englobado dentro del QRL cuando tengamos la necesidad por petición haciendo esa carga diferida.

Imaginaros que tenemos una función increment donde vamos a realizar el +1 del elemento contador tendríamos esta función:

  const increment = () => count.value++;

Que si la añadimos de esta forma:

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const increment = () => count.value++;
  return (
    <>
      <button onClick$={increment}>+1</button>
      <p>Valor actual: {count.value}</p>
    </>
  );
});

Nos va a dar un error como el siguiente, donde básicamente dice que debemos de englobarlo dentro de esa función $ para poder cargarlo de manera diferida (Lazy Loading) cuando sea necesario por nuestra demanda al ejecutar el click del botón:

Por lo tanto, para hacer que funcione bien, debemos de englobarlo dentro de $:

import { ...,  $ } from '@builder.io/qwik';
...
  const increment = $(() => count.value++);
...

Y ahora si arrancará el elemento contador:

El funcionamiento será igual que el punto anterior, solo que ahora hemos conseguido desacoplar esa función del boton y podríamos llamar a ese código mediante otro boton ejecutando el evento onClick$ o cualquier otro como podría ser $onMouseEnter (o el que deseemos).

Ejemplo

Vamos a ver esto, añadiendo un nuevo botón y un elemento como un div, que si ponemos el cursor sobre ese elemento, incrementa el valor del contador.

El código:

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const increment = $(() => count.value++);
  return (
    <>
      <button onClick$={increment}>+1</button>
      <button onClick$={increment}>+1 (Botón 2)</button>
      <div
        onMouseEnter$={increment}
        style='height: 100px; width: 400px; background-color: blue'
      >
        Poner el cursor del ratón sobre el cuadro azul para hacer + 1
      </div>
      <p>Valor actual: {count.value}</p>
    </>
  );
});

Al cargarlo:


Incrementará +1 siempre que hagamos lo siguiente:

  • click en Increment o Increment (Button 2).
  • Mover el cursor dentro del rectángulo azul, cuyo evento que se ejecutará será mouseenter

Probad haciendo diferentes variantes clickando, introduciendo el cursor sobre el rectángulo,... y veréis como se va incrementando el valor del contador.

Múltiples manejadores de eventos

Si deseamos registrar varios manejadores de eventos para UN MISMO EVENTO, podemos pasar un array de manejadores de eventos al atributo on{NombreDelEvento}$.

Recordad, con un manejador de evento sería así:

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Incrementar: {count.value}
    </button>
  );
});

Y si queremos añadir la opción para registrar más de un manejador de eventos, dentro de { () => count.value++ } debemos de pasar a { [ $(() => count.value++)] } dando opción a que podamos registrar más de un manejador evento mediante una acción que en este caso es el evento onClick$.

Al hacer esta modificación, tendremos que introducirlo dentro de la función $() para que no haya errores al implementarlo.

Vamos a adaptar el código e implementaremos las siguientes acciones:

import { component$, useSignal, $ } from '@builder.io/qwik';

export default component$(() => {
  const count = useSignal(0);
  // 1.- Añadimos el valor de última actualziación
  const lastUpdate = useSignal(new Date().toISOString())
  const eventTwo = $(() => console.log('Event Two with action'));
  return (
    <>
      <button
        onClick$={[
          $(() => count.value++), // 2
          eventTwo /* 3 - Usando la función desacoplada */,
          $(() => { // 4
              // Actualizamos la fecha y hora actual
              lastUpdate.value = new Date().toISOString();
              // Registramos mensaje en la consola
              console.log(lastUpdate.value, 'Trabajando con eventos - Multiple Handlers / Manejador de eventos múltiple');
          }),
        ]}
      >
        Incrementar: {count.value}
      </button> <span>Última actualización: {lastUpdate.value}</span>
    </>
  );
});

Al cargarlo, Tendremos el siguiente contenido con count cuyo valor es 0 y lastUpdate es 2023-09-12<HORA>, que irá modificando en base a los clicks que hagamos en el botón cuyo evento registrado es onClick$ con las tres acciones:

Al hacer un click, en la aplicación se pueden observar los cambios en los valores count (pasa a 1) (1) y lastUpdate (pasa a la hora actual de la acción) (2) junto con los logs que se registran en la consola del navegador respectivamente a la acción del renderizado (1 y 2) y los registros en consola (3) ejecutados con console.log:

Podéis probar de nuevo a hacer click, pasaría a lo siguiente:

Con este ejemplo ya podemos afirmar que sabemos trabajar con múltiples manejadores de eventos al ejecutar un evento de Javascript en Qwik.

Os animo a que experimentéis con otros tipos de eventos como dblclick, mouseenter,...

Objeto de Evento (Event Object)

Dentro de los manejadores de eventos el primer argumento será el objeto Evento. Este objeto va a contener información sobre el evento que ha activado el manejador.

Por ejemplo, el objeto Evento para un evento de click va a contener:

Hasta ahora solo estabamos usando los eventos de esta forma:

on<Evento>$={() => console.log('ejecutando evento')}}

Y para usar el objeto del evento y obtener su información, extraemos ese primer argumento:

on<Evento>$={(event: <EventType>) => console.log('ejecutando evento')}}

Eventos DOM - Documentación al detalle

Podemos consultar la documentación de MDN para obtener más detalles sobre cada evento del DOM en:
https://shorten-up.vercel.app/M8JYQ-0Sb8

Vamos a hacer un ejemplo con la acción click mediante onClick$, evento que es del ratón, por lo que debemos de especificar como MouseEvent.

Información más detallada de MouseEvent

Toda la información relacionado sobre los eventos de ratón de la documentación oficial:
https://shorten-up.vercel.app/9wl8umH4gg

Añadimos el siguiente código:

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const position = useSignal<{ x: number; y: number }>();
  return (
    <div
      onClick$={(event: MouseEvent) => {
        // Mostrar toda la info del evento
        console.log(event); 
        // Consultamos las propiedades de este tipo de evento:
        // https://shorten-up.vercel.app/n7499gVNKF
        // Asignar los valores de x e y
        (position.value = { x: event.x, y: event.y });
      }}
      style="height: 20vh; border: 2px solid green"
    >
      <p>
        Hemos realizado click en la posición [x, y]: ({position.value?.x}, {position.value?.y})
      </p>
      <p>Obtiene la posición solo haciendo click dentro del cuadro verde</p>
    </div>
  );
});

Con esto vamos a obtener la información al detalle del evento que se ejecuta, en este caso click mediante MouseEvent, como la posición en el eje x e y entre otros muchos datos. Una vez guardados los cambios:

Y hacemos click en cualquier parte, donde más deseemos (dentro de los límites del borde verde):

Se actualizará la posición del eje x y del eje y

Y aquí (haciendo click en el apartado PointerEvent) se ve toda la información recogida en el evento, donde se puede observar que tenemos la información de los ejes mencionados:

Eventos Asíncronos

Debido a la naturaleza asíncrona de Qwik, la ejecución del manejador de un evento podría retrasarse porque la implementación aún no se ha cargado en la VM (Máquina Virtual) de JavaScript.

Debido a la naturaleza asíncrona del procesamiento de eventos en Qwik, las siguientes APIs en un objeto Evento no funcionarán tal y como solemos usarlas:

Prevent default

Debido a que el manejo de eventos es asíncrono, no podemos usar event.preventDefault() de esta manera.

¿Y cómo lo podemos hacer? Para resolver esto, Qwik introduce una forma declarativa de prevenir el comportamiento predeterminado a través del atributo preventdefault:{nombreDelEvento} donde nombreDelEvento como bien sabéis puede ser cualquier evento visto anteriormente como click, mousedown, dblclick,...

Si el evento es click debemos de introducir preventdefault:click.

Aplicándolo en el código, con un evento click:

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return (
      <a
      href="/about"
      preventdefault:click // Esto evitará el comportamiento de "click" en este caso, que sería navegar a "/about" después de mostrar la alerta de `onClick$`.
      onClick$={() => {
        alert('Haz algo más para simular la navegación...No podemos hacer nada debido a "preventdefault:click"');
      }}
      >
        Navegar a la página "about"
      </a>
  );
});

Guardamos y esto es lo que tenemos, donde se ve un enlace normal y corriente visualmente, pero no en cuanto a funcionalidad:

Si intentamos realizar la acción de click, lo que hará es mostrar el mensaje de alerta:

Una vez cerrado (con Aceptar), se queda sin realizar ninguna acción más, que lo normal sería que navegase a la ruta /about (con el código especificado no lo hace porque lo bloquea).

Probad a quitar preventdefault:click y ejecutad la acción del click, ya veréis como muestra el mensaje y al cerrar esa alerta nos llevará a la ruta /about.

Objetivo del evento (Event target)

Debido a que el manejo de eventos es asíncrono, no podremos usar event.currentTarget.

¿Cómo solucionamos esto en Qwik? No os preocupéis, Qwik resuelve esto proporcionando como segundo argumento el elemento currentTarget, después del Event Object (Objeto del evento) visto anteriormente.

Más información sobre Event Target

A continuación tenéis disponible la información de la documentación oficial de Qwik sobre este apartado:
https://shorten-up.vercel.app/ihBBRTd7N7

Se implementa de la siguiente forma aplicando el método onClick$:

...
  onClick$={(event: MouseEvent, currentTarget) => {
    currentElement.value = currentTarget; // este será el valor
    targetElement.value = event.target as HTMLElement;
  }}
...

En este caso, siempre que hagamos click en un elemento HTML que contiene el manejador de eventos (por ejemplo onClick$), siempre devolverá en el currentTarget el valor de su selector.

Por ejemplo, si lo añadiesemos dentro de un elemento <section></section> siempre que hagamos click sobre ese elemento, independientemente de si lo hacemos en un lado, el medio, arriba o donde sea, que si es dentro de ese elemento, el valor currentTarget se corresponderá a <section></section>.

Y aquí un ejemplo donde estamos trabajando con el objetivo del evento, asignándolo en el elemento <section></section> y donde vamos a ir obteniendo la información del elemento que hemos hecho click:

import { component$, useSignal } from '@builder.io/qwik';

export default component$(() => {
  const targetElement = useSignal<HTMLElement>();
  const currentElement = useSignal<HTMLElement>();
  return (
    <section
      onClick$={(event, currentTarget) => {
        currentElement.value = currentTarget;
        targetElement.value = event.target as HTMLElement;
      }}
    >
      Haga clic en cualquier texto <code>target</code> y{' '}
      <code>currentElement</code> del evento por ser donde se realiza el click siempre.
      <hr />
      <p>
        ¡Hola <b>Mundo</b>!
      </p>
      <hr />
      <ul>
        <li>
          Elemento actual (Current Element): {currentElement.value?.tagName}
        </li>
        <li>
          Elemento objetivo (Target Element): {targetElement.value?.tagName}
        </li>
      </ul>
    </section>
  );
});

A tener en cuenta: currentTarget en el DOM apunta al elemento al que se adjuntó el escuchador de eventos (en este caso a onClick$).

En el ejemplo anterior, siempre será el elemento SECTION por lo que prácticamente en todo momento el valor de currentTarget va a estar asignado con SECTION al valor del estado currentElement.

En cambio, el resultado del valor target irá cambiando independientemente de donde hagamos la acción (en este caso click).

La apariencia inicial es la siguiente:

Aquí nos muestra la información del elemento al que se ha hecho click.

Ejemplos:

  • Click dentro de section y click en el texto Mundo que está en negrita:

Al hacer click aquí, el resultado del currentTarget será section (reflejado en la pantalla como valor de currentElement) y el target (reflejado como target), el elemento objetivo donde se ha realizado ese click será b.

Esto se refleja teniendo en cuenta esta información añadida en el código anterior:

<p>¡Hola <b>Mundo</b>!</p>
  • Click dentro de section y click en el texto Hola:

Al hacer click aquí, el resultado del currentTarget será section también, manteniendo el valor anterior en la pantalla asignado a currentElement.

Esto, como se ha mencionado, es por estar dentro de este elemento donde se registra el manejador de eventos y el target (reflejado en target), el elemento objetivo donde se ha realizado ese click será p.

Esto se refleja teniendo en cuenta esta información añadida:

<p>¡Hola <b>Mundo</b>!</p>

Podéis curiosear haciendo click en diferentes apartados como los que corresponden a los selectores li, hr,... Os invito a que los probéis y veréis que el valor reflejado en currentElement, en este caso, siempre será SECTION y targetElement irá modificándose.

Manejo de eventos síncronos (Synchronous event handling)

En algunos casos, es necesario manejar un evento de manera tradicional porque algunas APIs deben utilizarse de manera síncrona.

Por ejemplo, el evento dragstart debe procesarse de manera síncrona y, por lo tanto, no se puede combinar con la ejecución de código diferida de Qwik.

Más información sobre Manejo de Eventos Síncronos

A continuación tenéis disponible la información de la documentación oficial de Qwik sobre este apartado: https://shorten-up.vercel.app/1W7OJX1DyT

Para hacer esto, podemos aprovechar el hook useVisibleTask (recordad que tenemos todo al detalle en el capítulo Ciclos de vida) para agregar de manera programática un escuchador de eventos utilizando directamente la API del DOM.

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
 
export const SynchronousEventHandling = component$(() => {
  // 1.- Aquí tratamos la referencia del elemento HTML para el arrastre
  const draggableRef = useSignal<HTMLElement>();
  // 2.- Almacaremos el valor del estado del arrastre (dragstart / dragend)
  const dragStatus = useSignal(
    'A la espera de arrastrar para cambiar de estado'
  );

  useVisibleTask$(({ cleanup }) => {
    if (draggableRef.value) {
      // 3.- Usamos la API de DOM para añadir el escuchador de eventos (event listener).
      const dragstart = () => (dragStatus.value = 'dragstart (Arrastrando)');
      const dragend = () =>
        (dragStatus.value = 'dragend (Arrastre finalizado)');

      // 4.- Registro de los eventos
      draggableRef.value!.addEventListener('dragstart', dragstart);
      draggableRef.value!.addEventListener('dragend', dragend);

      // 5.- Limpiamos
      cleanup(() => {
        draggableRef.value!.removeEventListener('dragstart', dragstart);
        draggableRef.value!.removeEventListener('dragend', dragend);
      });
    }
  });

  return (
    <div>
      <button draggable ref={draggableRef}>
        {dragStatus.value === 'dragstart (Arrastrando)' ? 'Arrastrando :)' : '¡¡Arrastrame!!'}
      </button>
      <p>{dragStatus.value}</p>
    </div>
  );
});

Y así se visualizará:

Si empezamos a arrastrar haciendo click sobre el botón (y manteniendo para arrastrar):

  • dragStatus.value = dragstart (Arrastrando)
  • Texto del boton: Arrastrando :)

Y si soltamos, vuelve al estado original:

A tener en cuenta y como recordatorio

Utilizar useVisibleTask$ para escuchar eventos es una práctica desaconsejada en Qwik.

¿Cuál es el motivo? Esto se dará porque provoca la ejecución inmediata del código en el navegador, lo que anula la reanudabilidad.

Solo deberíamos usarlo cuando no tengamos otra opción. En la mayoría de los casos, podemos utilizar JSX para escuchar eventos:
onClick$={...} o los métodos de eventos useOn(...) (que lo veremos en breves en este capítulo) si necesitamos escuchar de manera programática.

PropFunction - pasar funciones mediante props

Al crear nuestros componentes, a menudo es útil pasar lo que parecen ser manejadores de eventos como acciones de click (entre otros) que ya hemos visto durante este capítulo.

Más información sobre PropFunction

A continuación tenéis disponible la información de la documentación oficial de Qwik sobre este apartado donde se verá como pasar funciones mediante props:
https://shorten-up.vercel.app/JQ0-ZOOEFh

Los límites de los componentes en Qwik deben ser serializables, y las funciones no son serializables a menos que se conviertan en un QRL utilizando un optimizador.

Esto se logra a través del sufijo $ como ya hemos ido viendo en los puntos anteriores. Los QRL son asíncronos y, por lo tanto, debemos indicarle a TypeScript que la función no se puede llamar de manera síncrona. Esto lo hacemos a través del tipo PropFunction.

Imaginaros que tenemos este elemento en nuestro componente principal que renderiza desde la ruta principal:

<button onClick$={() => alert('¡PULSADO!')}>click me!</button>

Vamos a pasarlo a componente individual, que podríamos llamar <Button /> donde implementamos ese código y en ese nuevo componente, dentro de los props obtenemos el PropFunction:

import { type PropFunction, component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return <Button onClick$={() => alert('¡PULSADO!')}/ label="Pulsa sobre mí">;
});
 
export const Button = component$<{
  // Es importante indicarle a TypeScript que esto es asíncrono y lo hacemos así
  onClick$?: PropFunction<() => void>;
  label: string;
}>((props) => {
 ...
});

Y una vez implementado de esta manera, aplicamos ese prop en el evento onClick$ dentro del componente Button:

import { type PropFunction, component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <Button onClick$={() => alert('¡PULSADO!')} label="Púlsame para ver lo que ocurre :)" />;
});
 
export const Button = component$<{
  // Important to tell TypeScript that this is async
  onClick$?: PropFunction<() => void>;
  label: string;
}>((props) => {
  return (
    <button onClick$={props.onClick$}>
      {props.label}
    </button>
  );
});

Y su funcionamiento es el mismo, solo que ya estamos creando un componente reusable para cualquier acción a realizar que le pasemos, mediante props.

Haciendo click:

Otro ejemplo, pasando varias opciones en un evento como hemos visto anteriormente:

Tenemos un componente botón donde lo personalizamos mediante estas propiedades:

Vamos a crear un valor inicial entero que irá cambiando haciendo sumas y / o restas de unidades, es decir, sumando 1 o restando 1.

Para efectuar estas acciones, tenemos el componente botón que se va a reutilizar y va a obtener las dos propiedades mencionadas, por un lado el valor del label y por otro la lógica de la función a ejecutar.

Teniendo en cuenta estos datos, debemos de conseguir lo siguiente visualmente para el estado inicial:

Haciendo click 3 veces en +1:

Haciendo click 1 vez en -1:

Código de la solución con mi propuesta:

import { type PropFunction, component$, useSignal, useStyles$ } from '@builder.io/qwik';
 
export default component$(() => {
  const counter = useSignal(0);
  useStyles$(`
    b {
      font-size: 2.5rem;
    }
  `)
  return <div>
    <p>Valor actual del contador: <b>{counter.value}</b></p>
    <p>A continuación vamos a añadir dos botones reutilizando el componente <code>Button</code></p>
    <p><Button onClick$={() => counter.value += 1} label="+1" /> <Button onClick$={() => counter.value -= 1} label="-1" /></p>
  </div>;
});
 
export const Button = component$<{
  onClick$?: PropFunction<() => void>;
  label: string;
}>((props) => {
  return (
    <button onClick$={props.onClick$}>
      {props.label}
    </button>
  );
});

Window / Document Events (Eventos de Window y Documento)

Hasta ahora, hemos estado trabajando con la opción de cómo escuchar eventos que se crean desde diferentes elementos.

Existen eventos (por ejemplo, scroll y mousemove) que requieren que los escuchemos en la ventana (window) o el documento (document), aspectos que ya hemos visto en el punto Diferencias entre usar Window y Document y Casos en los que se podría usar Window.

Por esta razón, Qwik permite el uso de los prefijos document:on y window:on al escuchar eventos.

El propósito de window:on / document:on: será registrar un evento en una ubicación actual del DOM del componente, para hacer que reciba eventos desde la ventana (window) o el documento (document) especificando en la mejor situación posible.

Vamos a procurar que el registro que hagamos afecte en lo menor posible, analizamos si se va a aislar a un elemento concreto (click, dragstart, etc.) usamos document:<evento> y si es algo más global como scroll y resize lo haremos con window:<evento>.

Esto nos va a proporcionar un par de ventajas:

A continuación podemos ver las diferentes opciones.

useOn[|window|document] Hook

En este apartado tenemos disponible otra opción mediante el uso del hook useOn|window|document va a agregar un escuchador de eventos basado en el DOM a nivel de componente de manera programática.

Encontramos los tres tipos asociados a esta implementación, diferenciando a que elemento estará escuchando:

Estos hooks nos serán útiles para cuando vayamos a crear nuestros propios hooks (Lo veremos en el capítulos Custom Hooks) o si no conoces el nombre del evento en el momento de la compilación.

useOn

El hook useOn escucha eventos en el elemento raíz del componente. El primer parámetro es el nombre del evento que se desea escuchar. El segundo parámetro es una función que se ejecutará cuando se produzca el evento.

  • click: Evento click dentro del componente, en su elemento raíz, que en este caso es un elemento p. Mostrará un mensaje alert con el texto Hello World siempre y cuando hagamos click dentro de ese elemento p que visualmente podremos verlo acotado con un borde sólido de color rojo.
import { component$, useOn, $, useStyles$} from '@builder.io/qwik';

export default component$(() => {
  useStyles$(
    `
      .useon {
        border: 2px solid red;
        height: 300px
      }
    `
  );
  useOn(
    'click',
    $(() => alert('Hola Mundo con useOn'))
  );

  return <p class='useon'>App Component. Haz click sobre mí.</p>;
});

Guardamos los cambios y esto es lo que se podemos ver:

Si hacemos clicks de las 3 formas indicadas solo en la 1 mostrará el mensaje del alert ya que la interacción se realiza únicamente en ese caso dentro de ese elemento p (que se delimita con el borde rojo), por lo que en los casos 2 y 3 no realizará esa acción.

useOnDocument

El hook useOnDocument escucha eventos en el documento. El primer parámetro es el nombre del evento que se desea escuchar. El segundo parámetro es una función que se ejecutará cuando se produzca el evento.

Uno de los eventos que podríamos usar dentro del elemento document fuera ya del elemento raíz de un componente podría ser el evento mousemove que se ejecuta cuando movemos el cursor del ratón dentro del elemento document. El siguiente código nos permitirá saber en todo momento mediante el movimiento del cursor del ratón en que posición se encuentra en los ejes x e y y con document.body tenemos las propiedades principales del documento, entre ellas la altura disponible mediante clientHeight siendo lo siguiente:

import { component$, $, useOnDocument, useStore } from '@builder.io/qwik';

export default component$(() => {
  const store = useStore({
    position: {
      x: 0,
      y: 0,
    },
  });

  useOnDocument(
    'mousemove',
    $((event) => {
      // Altura del cliente 
      // (la parte donde se muestran los contenidos)
      console.log('Height: ' + document.body.clientHeight);
      store.position.x = (event as MouseEvent).x;
      store.position.y = (event as MouseEvent).y;
    })
  );

  return (
    <div>
      <p>
        Mi posición actual [x, y]: [{store.position.x}, {store.position.y}]
      </p>
    </div>
  );
});

Al guardar los cambios y no introducir el cursor dentro de la aplicación, nos mostrará con los valores por defecto que sería la posición [x, y] = [0, 0]:

Si introducimos el cursor y apuntamos donde pone Mi posición actual, esto sería lo que visualizaremos:

Actividad práctica

Basándonos en lo que hemos visto en este apartado vamos a crear un elemento que cambie de color de fondo cuando pasemos del 50% de la pantalla horizontalmente (fijaros que yo os he enseñado como obtener el alto clientHeight). Solo debemos de llamar a la propiedad que corresponde al ancho.

Cuando lo rebasamos cambiamos el color de fondo "#f0f0f0" inicial a rojo ("red") (o color que deseéis) y si volvemos hacia la izquierda, cambiamos a al color del estado original ("#f0f0f0"), a su estado inicial.

Por ejemplo, imaginaros que la anchura de la ventana es 100px, por lo que si rebasa la la mitad hacia la derecha a partir de 50px, se vuelve el fondo rojo y si volvemos de 50px hacia la izquierda, vuelve a su estado original ("#f0f0f0")

¿Seriáis capaces de hacer algo con lo que os pido como en el siguiente resultado?

Estado inicial. Para añadir la estructura correspondiente a la apariencia, debemos de tener en cuenta la siguiente información tanto en el CSS y el HTML. https://shorten-up.vercel.app/ZP_A5JITrC
Ya sabemos como aplicarlo, por lo que partimos con esta información y esta será la apariencia, aunque es necesario que añadáis la lógica de la posición actual y el límite de línea de la mitad (almacenando el estado) para que visualmente tengamos esa referencia:


Al superar el 50% de ancho (la raya vertical de la mitad). La posición actual de x es mayor a la que marca el límite del ancho en la mitad:


Recordad, yo haré mi propuesta, la idea es practicar con estos eventos e ir aprendiendo su uso para afrontar retos futuros

El resultado con mi propuesta lo podréis ver en este apartado:
https://shorten-up.vercel.app/zbUsfJ-TwN

useOnWindow

El hook useOnWindow escucha eventos en la ventana. El primer parámetro es el nombre del evento que se desea escuchar. El segundo parámetro es una función que se ejecutará cuando se produzca el evento.

Ejemplos básico usando resize donde vamos a tener en todo momento las propiedades de altura (height) y ancho (width) de la pantalla que irá actualizando a medida que hacemos más pequeños o más grande.:

import {
  component$,
  useOnWindow,
  $,
  useSignal,
  useVisibleTask$,
} from '@builder.io/qwik';

export default component$(() => {
  // 1.- Almacenamos el tamaño de la pantalla actual
  const actualScreenSize = useSignal({ x: 0, y: 0 });

  // 2.- Actualizar al estado que se encuentra la pantalla cuando inicia / redimensiona
  const updateScreenSize = $(() => {
    actualScreenSize.value = {
      x: window.innerWidth,
      y: window.innerHeight,
    };
  });

  // 3.- Al cargar, añadimos el tamaño inicial
  useVisibleTask$(() => {
    updateScreenSize();
  });

  // 4.- Redimensionando
  useOnWindow(
    'resize',
    $(() => {
      // Se ejecuta cuando la ventana se redimensiona
      updateScreenSize();
    })
  );

  // 5.- Se muestra
  return (
    <div>
      <h3>Redimensionar navegador para ver como se actualiza</h3>
      <p>
        Tamaño actual: {actualScreenSize.value.x}px (width) /{' '}
        {actualScreenSize.value.y}px (height)
      </p>
    </div>
  );
});

Actividad práctica - 1

Teniendo en cuenta el tamaño de la pantalla que estamos usando

Vamos a catalogar los tamaños siguiendo está tabla:

Sobre las medidas usadas

Estas medidas no son las exactas, son las que usamos para el ejercicio, para prácticar. Podéis usar otras si así lo creéis oportuno.

El resultado es el siguiente, espero que lo hayáis intentado:

import {
  component$,
  useOnWindow,
  $,
  useSignal,
  useVisibleTask$,
} from '@builder.io/qwik';

export default component$(() => {
  // 1.- Almacenamos el tamaño de la pantalla actual y tipo de pantalla
  const actualScreenSize = useSignal({ x: 0, y: 0 });
  const screenName = useSignal("---");

  // 2.- Actualizar al estado que se encuentra la pantalla cuando inicia / redimensiona
  const updateScreenSize = $(() => {
    actualScreenSize.value = {
      x: window.innerWidth,
      y: window.innerHeight,
    };
    
    // Actualizar que tamaño de pantalla tiene

    if ( actualScreenSize.value.x < 600 ) {
      screenName.value = "SMALL";
    } else if (actualScreenSize.value.x >= 600 && actualScreenSize.value.x < 1025 ) {
      screenName.value = "MEDIUM";
    } else {
      screenName.value = "LARGE";
    }
  });

  // 3.- Al cargar, añadimos el tamaño inicial
  (IGUAL)

  // 4.- Redimensionando
 (IGUAL)

  // 5.- Se muestra
  return (
    <div>
      <h3>Redimensionar navegador para ver como se actualiza</h3>
      <p>
        Tamaño actual: {actualScreenSize.value.x}px (width) /{' '}
        {actualScreenSize.value.y}px (height) / Tipo de pantalla: {screenName.value}
      </p>
    </div>
  );
});

Estado inicial con la pantalla al completo (eso puede variar dependiendo de vuestra pantalla):

Si vamos reduciendo el tamaño del navegador y pasamos a menos de 1025px y más de 600px de ancho:

Si ya hemos reducido a menos de 600px de ancho:

Actividad práctica - 2

¿Seriáis capaces de añadir una nueva funcionalidad dentro de `resize` que nos permita contabilizar el número de actualizaciones que se han realizado al ejecutar esa acción.

Una vez trabajado en esta actividad, esta es mi propuesta:

import {
  component$,
  useOnWindow,
  $,
  useSignal,
  useVisibleTask$,
} from '@builder.io/qwik';

export default component$(() => {
  // 1.- Almacenamos el tamaño de la pantalla actual con tipo de pantalla y número de actualizaciones
  const actualScreenSize = useSignal({ x: 0, y: 0 });
  const screenName = useSignal("---");
  const refreshCount = useSignal(0);

  // 2.- Actualizar al estado que se encuentra la pantalla cuando inicia / redimensiona
  const updateScreenSize = $(() => {
    actualScreenSize.value = {
      x: window.innerWidth,
      y: window.innerHeight,
    };

    // Cada vez que hacemos una actualización +1
    refreshCount.value += 1;

    // Actualizar que tamaño de pantalla tiene

    if ( actualScreenSize.value.x < 600 ) {
      screenName.value = "SMALL";
    } else if (actualScreenSize.value.x >= 600 && actualScreenSize.value.x < 1025 ) {
      screenName.value = "MEDIUM";
    } else {
      screenName.value = "LARGE";
    }
  });

  // 3.- Al cargar, añadimos el tamaño inicial
  useVisibleTask$(() => {
    updateScreenSize();
  });

  // 4.- Redimensionando
  useOnWindow(
    'resize',
    $(() => {
      // Se ejecuta cuando la ventana se redimensiona
      updateScreenSize();
    })
  );

  // 5.- Se muestra
  return (
    <div>
      <h3>Redimensionar navegador para ver como se actualiza</h3>
      <p>
        Tamaño actual: {actualScreenSize.value.x}px (width) /{' '}
        {actualScreenSize.value.y}px (height) / Tipo de pantalla: {screenName.value} / Actualizaciones de tamaño efectuadas: { refreshCount.value}
      </p>
    </div>
  );
});

La diferencia respecto al primero es que hemos añadido un elemento refreshCount con un useSignal para ir almacenando la cantidad de veces que se ejecuta el redimensionado y posteriormente se añade a la pantalla para ir viendo ese resultado:

Resultado de lo trabajado en este capítulo

El código que encontráis es el resultado final de todo el proceso realizado durante el capítulo. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y en este caso, pienso que os vendría bien realizar pruebas con más ejemplos.

El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/YZNjTYxgdR

¿Qué hemos aprendido en este capítulo?

Llegados a este punto hemos visto todo lo indispensable para trabajar con los eventos en Javascript y como se aplican en Qwik con sus diferentes opciones y variantes. Ha sido un capítulo bastante extenso pero era necesario ver las bases para repasar y luego aplicarlo a Qwik con los ejemplos vistos. En resumen, hemos aprendido lo siguiente:

Conclusión

Los eventos junto con los ciclos de vida son fundamentales y os recomiendo que leáis de nuevo el capítulo siempre y cuando lo necesitéis, probéis todas las variantes y probéis con diferentes situaciones para interiorizar el uso de estos conceptos.

Como habéis visto en capítulos anteriores, hemos hecho uso del evento onClick$ más de una ocasión para efectuar acciones desde la parte del usuario y este es uno de los motivos para darle la importancia que se merece en cualquier aplicación web / móvil / escritorio.

Y aunque es un capítulo que pueda resultar no muy importante, os recomiendo que lo tengáis a mano, ya que estos conceptos los vais a usar prácticamente en todos los proyectos en los que trabajéis con esta tecnología.