Create and Dispatch Events
Handle Events
Configure Event Propagation
Communicate Across the DOM
Events Best Practices
Dynamic Event Listeners Considerations
Develop Secure Code
After an event is fired, it can propagate up through the DOM. To understand where events can be handled, understand how they propagate.
Events bubble up through the DOM; that’s how children and parents communicate—props down, events up. When an event bubbles, it becomes part of your component’s API and every consumer along the event’s path must understand the event. It’s important to understand how bubbling works so you can choose the most restrictive bubbling configuration that works for your component.
Lightning web component events propagate according to the same rules as DOM events. Lightning web components use only the bubbling phase. Dispatching events or adding listeners to the capture phase isn’t supported. Simply think of the event’s path as starting with your component and then moving to its parent, and then grandparent, and so on.
Event targets don’t propagate beyond the shadow root of the component instance. From outside the component, all event targets are the component itself. However, inside the shadow tree, you can handle events from specific targets in the tree. Depending on where you attach a listener for the event, and where the event happens, you can have different targets.
This content is adapted from the Salesforce Developer blog post, How Events Bubble in Lightning Web Components.
Note
When you create an event, define event propagation behavior using two properties on the event, bubbles and composed.
Event.bubbles
A Boolean value indicating whether the event bubbles up through the DOM or not. Defaults to false.
Event.composed
A Boolean value indicating whether the event can pass through the shadow boundary. Defaults to false.
To get information about the event, use these properties and method of the Event Web API.
Event.target
The element that dispatched the event.
Each component’s internal DOM is encapsulated in a shadow DOM. The shadow boundary is the line between the regular DOM (also called the light DOM) and the shadow DOM. If an event bubbles up and crosses the shadow boundary, the value of Event.target changes to represent an element in the same scope as the listener. Event retargeting preserves component encapsulation and prevents exposing a component’s internals.
For example, a click listener on <my-button> always receives my-button as the target, even if the click happened on the button element.
1<!-- myButton.html -->
2<template>
3 <button>{label}</button>
4</template>Event.currentTarget
As the event traverses the DOM, this property always refers to the element to which the event handler has been attached.
Event.composedPath()
An array of the event targets on which listeners are invoked as the event traverses the DOM.
A static composition doesn’t use slots. In this simple example, c-app composes c-parent, which in turn composes c-child.
1<c-app onbuttonclick={handleButtonClick}></c-app>The parent component in the app handles the button click.
1<!-- app.html -->
2<template>
3 <h2>My app</h2>
4 <c-parent onbuttonclick={handleButtonClick}></c-parent>
5</template>The parent component contains a wrapper with a child component, both listening for the button click event.
1<!-- parent.html -->
2<template>
3 <h3>I'm a parent component</h3>
4 <div class="wrapper" onbuttonclick={handleButtonClick}>
5 <c-child onbuttonclick={handleButtonClick}></c-child>
6 </div>
7</template>The child component contains the button with the onclick handler.
1<!-- child.html -->
2<template>
3 <h3>I'm a child component</h3>
4 <button onclick={handleClick}>click me</button>
5</template>1// child.js
2handleClick() {
3 const buttonclicked = new CustomEvent('buttonclick', {
4 //event options
5 });
6 this.dispatchEvent(buttonclicked);
7}The example fires an event, buttonclick, from c-child when the button is clicked. Event listeners are attached for the custom event on the following elements:
bodyc-app hostc-parentdiv.wrapperc-child hostThe flattened tree looks like this:
1<body>
2 <!-- Listening for buttonclick event -->
3 <c-app>
4 <!-- Listening for buttonclick event -->
5 #shadow-root
6 | <h2>My app</h2>
7 | <c-parent>
8 | <!-- Listening for buttonclick event -->
9 | #shadow-root
10 | | <h3>I'm a parent component</h3>
11 | | <div class="wrapper">
12 | | <!-- Listening for buttonclick event -->
13 | | <c-child>
14 | | #shadow-root
15 | | | <!-- Listening for buttonclick event -->
16 | | | <h3>I'm a child component</h3>
17 | | | <button>click me</button>
18 | | </c-child>
19 | | </div>
20 | </c-parent>
21 </c-app>
22</body>The default configuration. The event doesn’t bubble up through the DOM and doesn’t cross the shadow boundary. The only way to listen to this event is to add an event listener directly on the component that dispatches the event.
This configuration is recommended because it’s the least disruptive and provides the best encapsulation for your component.
The event bubbles up to c-child only.
1<body>
2 <c-app>
3 #shadow-root
4 | <c-parent>
5 | #shadow-root
6 | | <div class="wrapper">
7 | | <c-child>
8 | | <!-- Event bubbles up here -->
9 | | #shadow-root
10 | | | <h3>I'm a child component</h3>
11 | | | <button>click me</button>
12 | | </c-child>
13 | | </div>
14 | </c-parent>
15 </c-app>
16</body>Inspecting c-child handlers returns these values on the event.
event.currentTarget = c-childevent.target = c-childFrom here, you can start implementing more permissive configurations, as shown in the next few sections.
The c-event-with-data component in the lwc-recipes repo consumes a c-contact-list-item component, which creates an event with bubbles: false and composed: false.
Tip
The event bubbles up through the DOM, but doesn’t cross the shadow boundary. As a result, both c-child and div.wrapper can react to the event.
1<body>
2 <c-app>
3 #shadow-root
4 | <c-parent>
5 | #shadow-root
6 | | <div class="wrapper">
7 | | <!-- Event bubbles up here -->
8 | | <c-child>
9 | | <!-- Event bubbles up here -->
10 | | #shadow-root
11 | | | <h3>I'm a child component</h3>
12 | | | <button>click me</button>
13 | | </c-child>
14 | | </div>
15 | </c-parent>
16 </c-app>
17</body>The event handlers return the following.
c-child handler
event.currentTarget = c-childevent.target = c-childdiv.childWrapper handler
event.currentTarget = div.childWrapperevent.target = c-childThere are two use cases for using this configuration.
Create an internal event
To bubble an event inside the component’s template, dispatch the event on an element in the template. The event bubbles up to the element’s ancestors inside the template only. When the event reaches the shadow boundary, it stops.
1// myComponent.js
2this.template.querySelector("div").dispatchEvent(new CustomEvent("notify", { bubbles: true }));The event must be handled in myComponent.js. Handlers in the containing component don’t execute because the event doesn’t cross the shadow boundary.
1<!-- container.html -->
2<template>
3 <!-- handleNotify doesn’t execute -->
4 <c-my-component onnotify={handleNotify}></c-my-component>
5</template>Send an event to a component’s grandparent
If a component is passed into a slot, and you want to bubble an event from that component to the template that contains it, dispatch the event on the host element. The event is visible only in the template that contains your component.
Let’s look at sample code abridged from the eventBubbling component in the lwc-recipes repo. The component hierarchy from child to grandparent is c-contact-list-item-bubbling -> lightning-layout-item -> c-event-bubbling.
The c-contact-list-item-bubbling component dispatches a custom event called contactselect with bubbles: true.
The event listener, oncontactselect is on its parent, lightning-layout-item, and the event is handled in its grandparent, c-event-bubbling.
1<!-- eventBubbling.html -->
2<template>
3 <lightning-card title="EventBubbling" icon-name="standard:logging">
4 <template lwc:if={contacts.data}>
5 <lightning-layout class="slds-var-m-around_medium">
6 <!-- c-contact-list-item-bubbling emits a bubbling event so a single listener on a containing element works -->
7 <lightning-layout-item class="wide" oncontactselect={handleContactSelect}>
8 <template for:each={contacts.data} for:item="contact">
9 <c-contact-list-item-bubbling
10 class="slds-show slds-is-relative"
11 key={contact.Id}
12 contact={contact}
13 ></c-contact-list-item-bubbling>
14 </template>
15 </lightning-layout-item>
16 </lightning-layout>
17 </template>
18 </lightning-card>
19</template>1// contactListItemBubbling.js
2import { LightningElement, api } from "lwc";
3
4export default class ContactListItemBubbling extends LightningElement {
5 @api contact;
6
7 handleSelect(event) {
8 // Prevent default behavior of anchor tag click which is to navigate to the href url
9 event.preventDefault();
10 const selectEvent = new CustomEvent("contactselect", {
11 bubbles: true,
12 });
13 this.dispatchEvent(selectEvent);
14 }
15}The event bubbles up through the DOM, crosses the shadow boundary, and continues bubbling up through the DOM to the document root.
If an event uses this configuration, the event type becomes part of the component’s public API. It also forces the consuming component and all of its ancestors to include the event as part of their APIs.
Important
Because this configuration bubbles your event all the way to the document root, it can cause name collisions. Name collisions can cause the wrong event listeners to fire.
1<body>
2<!-- Event bubbles up here -->
3 <c-app>
4 <!-- Event bubbles up here -->
5 #shadow-root
6 | <c-parent>
7 | <!-- Event bubbles up here -->
8 | #shadow-root
9 | | <div class="wrapper">
10 | | <!-- Event bubbles up here -->
11 | | <c-child>
12 | | <!-- Event bubbles up here -->
13 | | #shadow-root
14 | | | <h3>I'm a child component</h3>
15 | | | <button>click me</button>
16 | | </c-child>
17 | | </div>
18 | </c-parent>
19 </c-app>
20</body>If you do use this configuration, prefix your event type with a namespace, like mydomain__myevent. The HTML event listener would have the awkward name onmydomain__myevent.
Lightning web components don’t use this configuration.
See Also