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.
document
y window
. Cuando usar cada uno de ellos.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:
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.
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.
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.
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.
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.
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 de los eventos más usados que estarán disponibles en JavaScript donde se añadirá un ejemplo sencillo para mostrar su comportamiento:
click
: Se dispara cuando se hace click con el botón izquierdo del ratón en un elemento de la página, como podría ser un botón, un enlace o una imagen. Para capturar este evento, se puede utilizar el método addEventListener
y luego especificar la acción deseada en respuesta al click ejecutado.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:
copy
: Se activa cuando el usuario copia contenido en el portapapeles. Este evento puede ser útil para realizar acciones adicionales al copiar contenido importante.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:
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:
keydown
: Se produce cuando el usuario presiona una tecla en el teclado. Puede utilizarse para detectar qué tecla se ha pulsado y tomar acciones en consecuencia.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:
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.
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:
global
: Movimiento del ratón en cualquier parte de la ventana del navegador, puedes registrarlos en window
.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
:
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.
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:
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.
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.
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.
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.
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}$
.
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.
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:
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.
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)
.mouseenter
Probad haciendo diferentes variantes clickando, introduciendo el cursor sobre el rectángulo,... y veréis como se va incrementando el valor del contador.
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:
count.value++
);Event Two with action
.Working with events - Multiple Handlers
) y una asignación de un valor añadiendo la fecha y hora del momento en el que se ejecuta la acción partiendo de un valor que iniciamos con useSignal
con la fecha / hora actual en formato ISO. El código será el siguiente: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
,...
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:
click
.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')}}
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
.
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:
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:
event.preventDefault()
event.currentTarget
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
.
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.
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:
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>
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.
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.
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)
Arrastrando :)
Y si soltamos, vuelve al estado original:
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.
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.
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:
label
: Valor de tipo string
que será una etiqueta con el texto que se va a visualizar en el botón.actionFunction
: Prop que será una función para realizar la función que nosotros le pasemos mediante el props
junto con el label
.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>
);
});
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:
eventos
se pueden registrar de manera declarativa en tu JSX
.eventos se eliminan automáticamente cuando el componente se destruye
(no se necesita llevar a cabo explícitamente el seguimiento y la limpieza, algo que nos va a ahorrar código y muchos quebraderos de cabeza).A continuación podemos ver las diferentes opciones.
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:
useOn()
: Escucha los eventos en el elemento raíz del componente actual. Esto sería algo similar a hacer una selección de un elemento mediante el uso de document
para seleccionar elementos concretos y no a nivel más global.useOnWindow()
: Escucha los eventos en el objeto window
.useOnDocument()
: Escucha los eventos en el objeto document
.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.
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.
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:
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
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>
);
});
Teniendo en cuenta el tamaño de la pantalla que estamos usando
Vamos a catalogar los tamaños siguiendo está tabla:
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:
¿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:
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
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:
Introducción a los eventos en Javascript.
Formas y utilidades a la hora de trabajar con los eventos en Qwik.
Uso de los hooks useOn()
, useOnDocument()
y useOnWindow()
para implementar los eventos de una forma más eficiente y ordenada.
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.