Esta es una traducción que desde EGA Futura ofrecemos como cortesía a toda la Ohana y comunidad de programadores , consultores , administradores y arquitectos de Salesforce para toda Iberoamérica .
El enlace a la publicación original, lo encontrarás al final de este artículo.
…
Cuando está componiendo componentes web personalizados, necesita comprender cómo los eventos fluyen a través del DOM porque así es como los niños y los padres se comunican: apoyos hacia abajo, eventos hacia arriba.
Cuando un evento burbujea, se convierte en parte de la API de su componente y todos los consumidores a lo largo de la ruta del evento deben comprender el evento. Ya sea que esté componiendo un componente simple o complejo, es importante comprender cómo funciona el burbujeo, para que pueda elegir la configuración de burbujeo más restrictiva que funcione para su componente.
Este artículo cubre dos patrones de composición: estático y dinámico, que utiliza ranuras. En cada patrón, un componente secundario que se encuentra en la parte inferior de la composición envía un evento personalizado a través del árbol de componentes. Veré cómo el evento burbujea para cada configuración de bubbles
y composed
, para que comprenda profundamente cómo configurar eventos en sus composiciones.
NOTA :
Este artículo se aplica al trabajo con Lightning Web Components en Lightning Platform y Open Source. Debido a que Salesforce admite versiones de navegador más antiguas que no son totalmente compatibles con el DOM de sombra nativo, los componentes web Lightning en la plataforma Lightning utilizan una versión sintética del DOM de sombra. En código abierto, puede optar por utilizar DOM de sombra sintético o nativo. Cuando hay una diferencia de comportamiento entre los dos, lo llamamos. Debido a la sombra sintética, cuando usa Dev Tools para ver el marcado en un navegador, no ve la etiqueta #shadow-root
DOM de sombra
Si ya está familiarizado con el DOM de sombra y las ranuras, pase al primer patrón: Composición estática .
Los componentes web crean y distribuyen eventos DOM, pero hay dos cosas acerca de los componentes web que hacen que trabajar con eventos sea un poco diferente: DOM en la sombra y ranuras. Primero explicaré el DOM de la sombra y luego las ranuras.
El DOM de cada componente web está encapsulado en un DOM en la sombra que otros componentes no pueden ver. Cuando un evento burbujea ( bubbles = true
), no cruza un límite de sombra a menos que lo configure en ( composed = true
) .
Shadow DOM permite la encapsulación de componentes. Permite que un componente tenga su propio árbol de sombra de nodos DOM al que no se puede acceder accidentalmente desde el documento principal y puede tener reglas de estilo locales.
La raíz de sombra es el nodo superior en un árbol de sombra. Este nodo está adjunto a un nodo DOM regular llamado elemento host.
El límite de la sombra es la línea entre la raíz de la sombra y el elemento anfitrión. Es donde termina el DOM en la sombra y comienza el DOM normal. Las consultas DOM y las reglas CSS no pueden cruzar el límite de sombra, lo que crea encapsulación. El DOM regular también se llama DOM ligero para distinguirlo del DOM sombra.
Si un DOM es un DOM ligero o un DOM de sombra depende del punto de vista.
- Desde el punto de vista de la clase JavaScript de un componente, los elementos de su plantilla pertenecen al DOM ligero. El componente los posee; son elementos DOM regulares.
- Desde el punto de vista del mundo exterior, esos mismos elementos son parte del DOM de sombra del componente. El mundo exterior no puede verlos ni acceder a ellos.
<!-- flattened DOM --> <body>
<p>
I'm just a regular paragraph element, I'm part of the light DOM
</p>
<!-- The c-child element is part of the light DOM.
But everything below #shadow-root is hidden,
because it's part of c-child's shadow DOM.
-->
<c-child>
#shadow-root
<p>
Enc-child, I'm light DOM.
To everyone else, I'm shadow DOM.
</p>
</c-child>
</body>
Ranuras
Un componente web puede contener elementos <slot></slot>
. Otros componentes pueden pasar elementos a una ranura, lo que le permite componer componentes de forma dinámica.
Cuando un componente tiene una <slot></slot>
, un componente contenedor puede pasar elementos DOM ligeros a la ranura.
<!-- childSlot.html -->
<template>
<h2>
I can host other elements via slots
</h2>
<slot></slot>
</template>
<!-- container.html -->
<template>
<c-child-slot>
<p>
Passing you some Light DOM content.
</p>
</c-child-slot>
</template>
El navegador muestra el árbol aplanado , que es lo que ve en la página. Lo importante a entender es que el DOM que se pasa a la ranura no se convierte en parte del DOM en la sombra del niño; es parte del DOM de sombra del contenedor.
<!-- flattened DOM -->
<cuerpo><c-container>
#shadow-root
<c-child-slot> #shadow-root
<h2>
I can host other elements via slots
</h2>
<slot> // To the outside world, // I'm not part of c-child-slot shadow DOM // I'm part of c-container shadow DOM
<p>
To c-container, I'm light DOM.
</p>
</slot>
</c-child-slot>
</c-container> </body>
Cuando usamos tragamonedas, aunque el contenido parece estar renderizado dentro del elemento de la ranura, el elemento real no se mueve. Más bien, se inserta un "puntero" al contenido original en la ranura. Este es un concepto importante de entender para poder entender lo que está sucediendo con nuestros eventos.
Eventos
Para crear eventos en un componente web Lightning, use la CustomEvent
, que hereda de Event
. En Lightning Web Components, CustomEvent
proporciona una experiencia más consistente en todos los navegadores, incluido Internet Explorer.
Cuando cree un evento, defina el comportamiento de propagación del evento utilizando dos propiedades: bubbles
y composed
.
bubbles
Un valor booleano que indica si el evento fluye a través del DOM o no. El valor predeterminado es false
.
composed
Un valor booleano que indica si el evento puede atravesar el límite de sombra. El valor predeterminado es false
.
importar {LightningElement} de 'lwc'; exportar clase predeterminada MyComponent extiende LightningElement { renderingCallback () { this.dispatchEvent ( new CustomEvent ('notificar', {burbujas: verdadero, compuesto: verdadero}) ); } }
Importante
Los eventos pasan a formar parte de la API de su componente, por lo que es mejor utilizar la configuración menos disruptiva y más restrictiva que funcione para su caso de uso.
Para obtener información sobre un evento, utilice la API de Event
-
event.target
: una referencia al elemento que distribuyó el evento. A medida que sube por el árbol, el valor deltarget
cambia para representar un elemento en el mismo ámbito que el elemento de escucha. Este retargeting de eventos conserva la encapsulación de componentes. Veremos cómo funciona esto más adelante.
-
event.currentTarget
: una referencia al elemento al que está adjunto el controlador de eventos. -
event.composedPath()
: interfaz que devuelve la ruta del evento, que es una matriz de los objetos en los que se invocarán los oyentes, según la configuración utilizada.
Composición estática
Una composición estática no usa ranuras. Aquí tenemos el ejemplo más simple: c-app
compone el componente c-parent
, que a su vez compone c-child
.
<! - cuerpo -> <c-app onbuttonclick = {handleButtonClick> </c-app>
<! - aplicación -> <plantilla> <h2> Mi aplicación </h2> <c-padre onbuttonclick = {handleButtonClick}> </c-parent> </template>
<! - parent.html -> <plantilla> <h3> Soy un componente principal </h3> <div class = 'envoltorio' onbuttonclick = {handleButtonClick}> <c-niño onbuttonclick = {handleButtonClick}> </c-child> </div> </template>
<! - niño.html -> <plantilla> <h3> Soy un componente c-child </h3> <button onclick = {handleClick}> haz clic en mí </button> </template>
buttonclick
un evento, buttonclick, de c-child
cada vez que ocurre una acción de click
en su elemento de button
Hemos adjuntado detectores de eventos para ese evento personalizado en los siguientes elementos:
-
body
- host de
c-app
- anfitrión
c-parent
-
div.wrapper
- anfitrión
c-child
El árbol aplanado se ve así:
<body> <! - Escuchando el evento "buttonclick" -> <c-app> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h2> Mi aplicación </h2> <c-parent> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h3> Soy un componente principal </h3> <div> <! - Escuchando el evento "buttonclick" -> <c-child> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h3> Soy un componente c-child </h3> <button> haz clic en mí </button> # / raíz-sombra </c-child> </div> # / raíz-sombra </c-parent> # / raíz-sombra </c-app> </body>
Aquí hay una representación visual:
Ahora veremos cómo el evento burbujea con la configuración de cada evento.
{burbujas: falso, compuesto: falso}
Con esta configuración, solo c-child
puede reaccionar al buttonclick
disparado desde c-child
. El evento no pasa por el anfitrión. Esta es la configuración recomendada porque proporciona la mejor encapsulación para su componente. Aquí es donde comienza, luego desde aquí puede comenzar a incorporar otras configuraciones más permisivas, como las que estamos a punto de explorar en las próximas secciones, para adaptarse a sus requisitos.
Si inspeccionamos el controlador c-child
, encontramos estos valores en el objeto de event
-
event.currentTarget = c-child
-
event.target = c-child
{burbujas: verdadero, compuesto: falso}
Con esta configuración, el evento buttonclick
c-child
viaja de abajo hacia arriba hasta que encuentra una raíz oculta o el evento se cancela. El resultado, además de c-child
, div.wrapper
también puede reaccionar al evento.
Utilice esta configuración para generar un evento dentro de la plantilla del componente, creando un evento interno. También puede usar esta configuración para manejar un evento en el abuelo de un componente.
Y nuevamente, esto es lo que nos dicen los eventos para cada controlador:
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
controlador div.childWrapper
-
event.currentTarget = div.childWrapper
-
event.target = c-child
{burbujas: falso, compuesto: verdadero}
Esta configuración es compatible con Shadow DOM nativo, lo que significa que no es compatible con Lightning Platform. Incluso para el código abierto de LWC, esta configuración no se sugiere, pero es útil para comprender cómo los eventos burbujean en un contexto DOM en la sombra.
Los eventos compuestos pueden romper los límites de las sombras y rebotar de un host a otro a lo largo de su trayectoria. No continúan burbujeando más allá de eso a menos que también establezcan bubbles:true
.
En este caso, c-child
, c-parent
y c-app
pueden reaccionar al evento. Es interesante notar que div.wrapper
no puede manejar el evento, porque el evento no burbujea en la sombra.
Veamos lo que el controlador tiene que decir sobre el evento:
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
controlador c-parent
-
event.currentTarget = c-parent
-
event.target = c-parent
controlador de c-app
-
event.currentTarget = c-app
-
event.target = c-app
Es interesante notar que div.wrapper
no puede manejar el evento porque incluso si el evento se propaga de una sombra a otra, no burbujea en la sombra en sí.
Hagamos una pausa aquí y observemos que aunque el evento fue disparado desde c-child
, cuando llega a c-parent
y c-app
, muestra el host como target
y currentTarget
.
¿Qué esta pasando? Reorientación de eventos en su máxima expresión. A medida que el evento buttonclick
c-child
, el evento se trata como un detalle de implementación y su target
se cambia para que coincida con el alcance del oyente.
Esta es una de las razones por las que las composed:true
deben usarse con precaución, ya que la semántica del c-child
y su receptor no coinciden. c-child
disparó el evento, pero para c-app
, parece que c-app
activó.
Usar la { bubbles:false, composed:true }
es un anti-patrón. El patrón correcto es para receptores que pueden entender el buttonclick
de botón para volver a empaquetar y enviar el evento con la semántica adecuada. Por ejemplo, c-parent
podría recibir el evento de c-child
y exponer un nuevo evento personalizado, para que los elementos en el árbol de luz de c-app
{burbujas: verdadero, compuesto: verdadero}
Esta configuración no se sugiere porque crea un evento que cruza todos los límites. Cada elemento recibe el evento, incluso los elementos DOM regulares que no forman parte de ninguna sombra. El evento puede burbujear hasta el elemento del cuerpo.
Al disparar eventos de esta manera, puede contaminar el espacio del evento, filtrar información y crear semánticas confusas. Los eventos se consideran parte de la API de su componente, así que asegúrese de que cualquier persona en la ruta del evento pueda comprender y manejar la carga útil del evento, si la tiene.
Finalmente, exploremos los valores del evento:
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
manejador div.wrapper
-
event.currentTarget = div.wraper
-
event.target = c-child
controlador c-parent
-
event.currentTarget = c-parent
-
event.target = c-parent
controlador de c-app
-
event.currentTarget = c-app
-
event.target = c-app
manipulador del body
-
event.currentTarget = body
-
event.target = c-app
Composición dinámica con ranuras
Ahora exploraremos cómo los eventos burbujean en composiciones que usan espacios. Tenemos un c-parent
que acepta cualquier contenido a través del elemento especial <slot>
. Usando c-app
, componimos c-parent
y pasamos c-child
como su hijo.
<! - cuerpo -> <c-app> </c-app>
<! - app.html -> <plantilla> <h2> Mi aplicación </h2> <div class = 'envoltorio'> <c-padre> <c-child> </c-child> </c-parent> </div> </template>
<! - parent.html -> <plantilla> <h3> Soy un componente principal </h3> <slot> </slot> </template>
<! - niño -> <plantilla> <h3> Soy un componente secundario </h3> <button onclick = {handleClick}> haz clic en mí </button> </template>
El código dispara un evento de c-child
llamado buttonclick
:
handleClick () { const buttonclicked = new CustomEvent ('buttonclick', {// opciones de evento}); this.dispatchEvent (botón clic); }
El código adjunta detectores de eventos en los siguientes elementos:
- anfitrión
c-child
-
slot
- anfitrión
c-parent
-
div.wrapper
- host de
c-app
-
body
Este es el árbol aplanado tal como se ve en las herramientas de desarrollo del navegador.
<cuerpo> <c-app> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h2> Mi aplicación </h2> <div class = 'envoltorio'> <c-parent> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h3> Soy un componente principal </h3> <slot> <! - puntero a c-child -> </slot> # / raíz-sombra <c-child> <! - Escuchando el evento "buttonclick" -> # raíz-sombra <h3> Soy un componente secundario </h3> <button> haz clic en mí </button> # / raíz-sombra </c-child> </c-parent> </div> </c-app> </body>
Recuerde que cuando usamos ranuras, aunque el contenido parece estar renderizado dentro del elemento de la ranura, el elemento real no se mueve. Más bien, se inserta un "puntero" al contenido original en la ranura. Este es un concepto importante de entender para poder entender lo que está sucediendo con nuestros eventos.
Esta vista del árbol aplanado le muestra dónde se encuentra realmente el contenido pasado en la ranura en el DOM. c-child
es parte del DOM de sombra c-ap
No es parte del DOM en la sombra c-parent
Con eso en mente, veamos los resultados que obtenemos con todas las diferentes configuraciones.
{burbujas: falso, compuesto: falso}
Al igual que con el ejemplo de composición anterior, el evento no pasa de c-child
, que es donde se disparó. Esta es también la configuración recomendada para composiciones dinámicas porque proporciona la mejor encapsulación para su componente.
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
{burbujas: verdadero, compuesto: falso}
NOTA : Esta configuración se comporta de manera diferente cuando está utilizando Shadow DOM nativo con Lightning Web Components: Open Source. Con el DOM de sombra nativo, el evento no sale de la ranura a menos que la composed
también sea true
.
Con esta configuración, el connectedchild
disparado desde el evento c-child
viaja de abajo hacia arriba hasta que encuentra una raíz oculta o el evento se cancela.
En nuestro ejemplo estático, mencionamos cómo un evento con esta configuración burbujea hasta que viaja de abajo hacia arriba hasta que encuentra una raíz de sombra o el evento se cancela. Exploremos lo que event.composedPath()
tiene que decir sobre este caso de uso:
0: mi-hijo 1: ranura 2: fragmento de documento // raíz de sombra de mi padre 3: mi padre 4: envoltura div. 5: fragmento de documento // raíz de la sombra de mi aplicación
Como puede ver, parece que el evento podría salir de #shadow-root
my-parent
y aparecer en el exterior, a pesar de que, composed
se establece en false
. ¿Confundido? Usted debería ser. Miremos más de cerca.
Aquí hay un efecto de "doble burbuja". Debido a que el contenido ranurado no mueve realmente las cosas, sino que crea punteros desde el DOM ligero y hacia un espacio en particular, cuando buttonclick
evento buttonclick, el evento viaja desde ambos lugares, lo que crea una composedPath
como la que acabamos de ver. .
Finalmente, esto es lo que nuestros controladores de eventos nos dicen sobre los objetivos:
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
controlador c-parent
-
event.currentTarget = c-parent
-
event.target = c-child
manejador div.wrapper
-
event.currentTarget = div
-
event.target = c-child
Observe cómo el objetivo de div.wrapper
c-child
, ya que técnicamente el evento nunca cruzó la shadow root
c-parent
.
{burbujas: falso, compuesto: verdadero}
Al igual que la composición simple, esta configuración es un anti-patrón y no es compatible con Lightning Platform, pero es útil para comprender cómo los eventos burbujean en un contexto DOM en la sombra.
El evento rebota de un anfitrión a otro siempre que estén en diferentes sombras. En este caso, c-child
no forma parte de c-parent
, sino que forma parte de la sombra de c-app
, por lo que salta directamente a c-app
.
Los resultados de event.composedPath()
también arrojan algunos resultados interesantes:
0: my-child
1: slot
2: document-fragment
3: my-parent
4: div.wrapper
5: document-fragment
6: my-test
7: body
8: html
9: document
10: Window
Muestra que cuando establecemos composed:true
, el evento puede salir de cada raíz de sombra. En este caso, no es así, ya que la bubbles
se establece en true
. En cambio, salta de un host a otro.
Objetivos:
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
controlador de c-app
-
event.currentTarget = c-app
-
event.target = c-child
Nuevamente, es útil mirar esta vista del árbol aplanado para recordar que c-child
en la slot
es solo un puntero.
{burbujas: verdadero, compuesto: verdadero}
Este es el escenario de "fuerza bruta" y, como nuestro ejemplo de composición estática, el evento burbujea por todas partes, lo cual es un anti-patrón. Cada elemento en la ruta compuesta del evento puede manejar el evento.
c-child
manejador de niños:
-
event.currentTarget = c-child
-
event.target = c-child
controlador c-parent
-
event.currentTarget = c-parent
-
event.target = c-child
manejador div.wrapper
-
event.currentTarget = div
-
event.target = c-child
controlador de c-app
-
event.currentTarget = c-app
-
event.target = c-app
manipulador del body
-
event.currentTarget = body
-
event.target = c-app
Conclusión
Los eventos son parte de su API. Tenga en cuenta a los consumidores de la API. Cada consumidor a lo largo de la ruta del evento debe comprender el evento y cómo manejar su carga útil.
Tenga cuidado al configurar las composed
y bubbles
, ya que pueden tener resultados no deseados. Utilice la configuración menos disruptiva que funcione para su caso de uso, con el ser menos disruptivo ( bubbles: false, composed: false
).
Y finalmente, recuerde que las composiciones dinámicas que usan tragamonedas introducen un nivel adicional de complejidad ya que tiene que lidiar con los nodos asignados dentro de los elementos de su tragamonedas.
Sobre el Autor
Gonzalo Cordero trabaja como ingeniero de software en Salesforce.
Github: @gonzalocordero
…
Esta es una traducción realizada por EGA Futura, y este es el link a la publicación original: https://developer.salesforce.com/blogs/2021/08/how-events-bubble-in-lightning-web-components.html