Event communication is a fundamental concept in Lightning Web Components (LWC) development. It allows components to communicate with each other efficiently.
Types of Events in LWC
In Lightning Web Components, there are two primary types of events:
- Custom Events – Component-to-parent communication
- Lightning Events – Parent-to-child communication
Let’s dive into each of these with detailed examples.
Custom Events (Component-to-Parent Communication)
Custom events allow child components to communicate with their parent components. This follows the standard web components model where events bubble up the DOM tree.
How Custom Events Work
- The child component creates and dispatches a custom event
- The parent component listens for this event using an event handler
- Event data is passed from child to parent
Example Implementation
Let’s create a simple contact form. A child component dispatches an event when a form is submitted. The parent component handles this event.
Child Component (contactForm.js)
import { LightningElement } from 'lwc';
export default class ContactForm extends LightningElement {
firstName = '';
lastName = '';
email = '';
handleFirstNameChange(event) {
this.firstName = event.target.value;
}
handleLastNameChange(event) {
this.lastName = event.target.value;
}
handleEmailChange(event) {
this.email = event.target.value;
}
handleSubmit(event) {
event.preventDefault();
// Create contact object
const contact = {
firstName: this.firstName,
lastName: this.lastName,
email: this.email
};
// Create custom event
const submitEvent = new CustomEvent('contactsubmit', {
detail: contact,
bubbles: true,
composed: true
});
// Dispatch the event
this.dispatchEvent(submitEvent);
// Reset form fields
this.resetForm();
}
resetForm() {
this.firstName = '';
this.lastName = '';
this.email = '';
}
}
Child Component (contactForm.html)
<template>
<div class="contact-form">
<lightning-card title="Contact Form">
<div class="slds-p-around_medium">
<lightning-input
label="First Name"
value={firstName}
onchange={handleFirstNameChange}>
</lightning-input>
<lightning-input
label="Last Name"
value={lastName}
onchange={handleLastNameChange}>
</lightning-input>
<lightning-input
label="Email"
type="email"
value={email}
onchange={handleEmailChange}>
</lightning-input>
<div class="slds-m-top_medium">
<lightning-button
label="Submit"
variant="brand"
onclick={handleSubmit}>
</lightning-button>
</div>
</div>
</lightning-card>
</div>
</template>
Parent Component (contactManager.js)
import { LightningElement, track } from 'lwc';
export default class ContactManager extends LightningElement {
@track contacts = [];
// Event handler for the custom event from child component
handleContactSubmit(event) {
// Extract contact data from the event detail
const newContact = event.detail;
// Add the new contact to our array
this.contacts = [...this.contacts, newContact];
// Show success message
this.showToast('Success', 'Contact created successfully!', 'success');
}
showToast(title, message, variant) {
const toastEvent = new ShowToastEvent({
title: title,
message: message,
variant: variant
});
this.dispatchEvent(toastEvent);
}
}
Parent Component (contactManager.html)
<template>
<div class="contact-manager">
<!-- Child component with event listener -->
<c-contact-form oncontactsubmit={handleContactSubmit}></c-contact-form>
<!-- Display contacts -->
<lightning-card title="Contacts" if:true={contacts.length}>
<div class="slds-p-around_medium">
<template for:each={contacts} for:item="contact">
<div key={contact.email} class="slds-box slds-m-bottom_small">
<p><strong>Name:</strong> {contact.firstName} {contact.lastName}</p>
<p><strong>Email:</strong> {contact.email}</p>
</div>
</template>
</div>
</lightning-card>
</div>
</template>
Key Points About Custom Events
- The
new CustomEvent()constructor creates a custom event with:- A name (first parameter): ‘contactsubmit’ in our example
- Configuration object (second parameter) with:
detail: The data you want to pass with the eventbubbles: Whether the event bubbles up through the DOM treecomposed: Whether the event can cross shadow DOM boundaries
- The parent component listens for the event using an attribute in the format
oneventname={handlerMethod}. - The event handler receives an event object as a parameter, with the passed data accessible via
event.detail.
Lightning Events (Parent-to-Child Communication)
For parent-to-child communication, we use properties with the @api decorator. This isn’t technically an event system, but it’s how data flows downward in LWC.
Example Implementation
Let’s create a component that filters contacts based on a search term provided by the parent component.
Child Component (contactList.js)
import { LightningElement, api, track } from 'lwc';
export default class ContactList extends LightningElement {
@api
get allContacts() {
return this._allContacts;
}
set allContacts(value) {
this._allContacts = value;
this.filterContacts();
}
@api
get searchTerm() {
return this._searchTerm || '';
}
set searchTerm(value) {
this._searchTerm = value;
this.filterContacts();
}
@track filteredContacts = [];
_allContacts = [];
_searchTerm = '';
filterContacts() {
if (!this._allContacts) {
this.filteredContacts = [];
return;
}
if (!this._searchTerm || this._searchTerm === '') {
// If no search term, show all contacts
this.filteredContacts = [...this._allContacts];
} else {
// Filter contacts based on search term
const term = this._searchTerm.toLowerCase();
this.filteredContacts = this._allContacts.filter(contact =>
contact.firstName.toLowerCase().includes(term) ||
contact.lastName.toLowerCase().includes(term) ||
contact.email.toLowerCase().includes(term)
);
}
}
handleContactSelect(event) {
const contactId = event.currentTarget.dataset.id;
const selectedContact = this.filteredContacts.find(contact => contact.id === contactId);
// Create and dispatch custom event
const selectEvent = new CustomEvent('contactselect', {
detail: selectedContact
});
this.dispatchEvent(selectEvent);
}
}
Child Component (contactList.html)
<template>
<div class="contact-list">
<lightning-card title="Filtered Contacts">
<div class="slds-p-around_medium">
<template if:true={filteredContacts.length}>
<ul class="slds-has-dividers_bottom-space">
<template for:each={filteredContacts} for:item="contact">
<li key={contact.id} class="slds-item" onclick={handleContactSelect} data-id={contact.id}>
<div class="slds-p-around_small">
<p><strong>{contact.firstName} {contact.lastName}</strong></p>
<p>{contact.email}</p>
</div>
</li>
</template>
</ul>
</template>
<template if:false={filteredContacts.length}>
<div class="slds-text-align_center slds-p-around_medium">
No contacts found
</div>
</template>
</div>
</lightning-card>
</div>
</template>
Parent Component (contactApp.js)
import { LightningElement, track } from 'lwc';
export default class ContactApp extends LightningElement {
@track contacts = [
{ id: '001', firstName: 'John', lastName: 'Doe', email: '[email protected]' },
{ id: '002', firstName: 'Jane', lastName: 'Smith', email: '[email protected]' },
{ id: '003', firstName: 'Bob', lastName: 'Johnson', email: '[email protected]' }
];
@track searchTerm = '';
@track selectedContact = null;
handleSearchChange(event) {
this.searchTerm = event.target.value;
}
handleContactSubmit(event) {
const newContact = {
id: String(Date.now()),
...event.detail
};
this.contacts = [...this.contacts, newContact];
}
handleContactSelect(event) {
this.selectedContact = event.detail;
}
}
Parent Component (contactApp.html)
<template>
<div class="contact-app slds-p-around_medium">
<lightning-card title="Contact Application">
<div class="slds-grid slds-gutters">
<div class="slds-col slds-size_1-of-3">
<!-- Add new contacts -->
<c-contact-form oncontactsubmit={handleContactSubmit}></c-contact-form>
<!-- Search box -->
<div class="slds-m-top_medium">
<lightning-input
label="Search Contacts"
type="search"
onchange={handleSearchChange}>
</lightning-input>
</div>
</div>
<div class="slds-col slds-size_1-of-3">
<!-- List of contacts with parent-to-child communication -->
<c-contact-list
all-contacts={contacts}
search-term={searchTerm}
oncontactselect={handleContactSelect}>
</c-contact-list>
</div>
<div class="slds-col slds-size_1-of-3">
<!-- Display selected contact -->
<template if:true={selectedContact}>
<lightning-card title="Selected Contact">
<div class="slds-p-around_medium">
<p><strong>Name:</strong> {selectedContact.firstName} {selectedContact.lastName}</p>
<p><strong>Email:</strong> {selectedContact.email}</p>
</div>
</lightning-card>
</template>
</div>
</div>
</lightning-card>
</div>
</template>
Advanced Event Techniques
Event Bubbling and Composition
In our examples, we used bubbles: true and composed: true in our custom event configuration. Let’s understand what these options do:
- bubbles: When set to
true, the event bubbles up through the DOM tree, allowing ancestor components to catch it. - composed: When set to
true, the event can cross shadow DOM boundaries, which is necessary for events to work properly in LWC.
Creating a Generic Event Service Component
For more complex applications, you might want to create a reusable event service component that can be used across your app for event communication.
// eventService.js
import { LightningElement, api } from 'lwc';
export default class EventService extends LightningElement {
@api
fireEvent(eventName, payload) {
const customEvent = new CustomEvent(eventName, {
detail: payload,
bubbles: true,
composed: true
});
this.dispatchEvent(customEvent);
return true;
}
@api
registerListener(eventName, callback) {
// Store event listener reference for later removal
if (!this.eventListeners) {
this.eventListeners = {};
}
if (!this.eventListeners[eventName]) {
this.eventListeners[eventName] = [];
}
// Add the event listener
this.addEventListener(eventName, callback);
this.eventListeners[eventName].push(callback);
return true;
}
@api
unregisterListener(eventName, callback) {
// Remove the event listener
this.removeEventListener(eventName, callback);
// Remove from our internal tracking
if (this.eventListeners && this.eventListeners[eventName]) {
this.eventListeners[eventName] = this.eventListeners[eventName].filter(
listener => listener !== callback
);
}
return true;
}
@api
unregisterAllListeners(eventName) {
// Remove all listeners for a specific event
if (this.eventListeners && this.eventListeners[eventName]) {
this.eventListeners[eventName].forEach(callback => {
this.removeEventListener(eventName, callback);
});
this.eventListeners[eventName] = [];
}
return true;
}
disconnectedCallback() {
// Clean up all event listeners when component is destroyed
if (this.eventListeners) {
Object.keys(this.eventListeners).forEach(eventName => {
this.unregisterAllListeners(eventName);
});
}
}
}
<!-- eventService.html -->
<template>
<!-- Empty template as this is a service component -->
</template>
Example of Using the Event Service
// eventDemo.js
import { LightningElement, track } from 'lwc';
export default class EventDemo extends LightningElement {
@track message = '';
connectedCallback() {
// Register event listener when component is inserted into the DOM
this.template.addEventListener('notification', this.handleNotification.bind(this));
}
disconnectedCallback() {
// Clean up event listener when component is removed from the DOM
this.template.removeEventListener('notification', this.handleNotification);
}
handleNotification(event) {
this.message = event.detail.message;
}
handleButtonClick() {
// Get a reference to our event service component
const eventService = this.template.querySelector('c-event-service');
// Fire an event
eventService.fireEvent('notification', {
message: 'Button clicked at ' + new Date().toLocaleTimeString()
});
}
}
<!-- eventDemo.html -->
<template>
<div class="event-demo">
<lightning-card title="Event Service Demo">
<div class="slds-p-around_medium">
<!-- Include our service component -->
<c-event-service></c-event-service>
<lightning-button
label="Fire Notification Event"
variant="brand"
onclick={handleButtonClick}>
</lightning-button>
<div class="slds-m-top_medium" if:true={message}>
<p><strong>Message:</strong> {message}</p>
</div>
</div>
</lightning-card>
</div>
</template>
Best Practices for Event Communication in LWC
- Use events for loose coupling: Events help maintain component independence, making your components more reusable.
- Keep events simple: Pass only the necessary data in your event detail. If you need to pass complex objects, consider using IDs that can be used to look up the full object.
- Name events clearly: Use descriptive names that indicate the action or change that occurred (e.g., ‘contactselect’, ‘statuschange’).
- Clean up event listeners: Always remove event listeners in the
disconnectedCallback()lifecycle hook. - Document your events: Add JSDoc comments that describe the events your component fires, including the structure of the payload.
- Consider event directionality:
- Use Custom Events for child-to-parent communication
- Use @api properties for parent-to-child communication
- Be cautious with bubbling: While bubbling is powerful, it can make debugging difficult if overused. Use it judiciously.
Event Communication in LWC – Cheat Sheet
Custom Events (Child-to-Parent)
Creating and Dispatching Custom Events
// Creating a custom event
const myEvent = new CustomEvent('myevent', {
detail: {
// Data to pass with the event
id: this.recordId,
name: this.name,
status: 'updated'
},
bubbles: true, // Event bubbles up the DOM tree
composed: true // Event can cross shadow DOM boundaries
});
// Dispatching the event
this.dispatchEvent(myEvent);
Handling Custom Events in Parent Component
HTML:
<template>
<c-child-component onmyevent={handleMyEvent}></c-child-component>
</template>
JavaScript:
handleMyEvent(event) {
// Access event data from event.detail
const id = event.detail.id;
const name = event.detail.name;
const status = event.detail.status;
// Process the event data
console.log(`Received event: ${name} was ${status}`);
}
API Properties (Parent-to-Child)
Passing Data from Parent to Child
Parent HTML:
<template>
<c-child-component
record-id={recordId}
record-name={recordName}
is-active={activeStatus}>
</c-child-component>
</template>
Receiving Data in Child Component
Child JavaScript:
import { LightningElement, api } from 'lwc';
export default class ChildComponent extends LightningElement {
@api recordId;
@api recordName;
// Using getters/setters for more control
_isActive = false;
@api
get isActive() {
return this._isActive;
}
set isActive(value) {
this._isActive = value;
// Do something when value changes
this.handleActiveChange();
}
handleActiveChange() {
// React to changes in the isActive property
if (this._isActive) {
// Perform actions when active
}
}
}
Event Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
bubbles | Boolean | false | Event bubbles up through DOM tree |
composed | Boolean | false | Event can cross shadow DOM boundaries |
cancelable | Boolean | false | Event can be canceled with preventDefault() |
detail | Any | null | Data to be passed with the event |
Event Naming Conventions
- Use camelCase for event names:
selectitem,statuschange,recordupdated - Use verbs or verb phrases that describe what happened:
click,change,submit,select - Be specific to avoid conflicts:
contactselectis better than justselect
Event Listener Lifecycle Management
import { LightningElement } from 'lwc';
export default class MyComponent extends LightningElement {
connectedCallback() {
// Add event listeners when component is inserted in DOM
this.addEventListener('click', this.handleClick);
// For events from outside the component
window.addEventListener('resize', this.handleResize);
}
disconnectedCallback() {
// Clean up event listeners when component is removed
this.removeEventListener('click', this.handleClick);
window.removeEventListener('resize', this.handleResize);
}
// Using bound methods to maintain proper 'this' context
handleClick = (event) => {
// Handle click event
}
handleResize = (event) => {
// Handle resize event
}
}
Advanced: Event Propagation Control
handleEvent(event) {
// Stop event from bubbling further up DOM tree
event.stopPropagation();
// Stop event and prevent default browser action
event.preventDefault();
// Stop immediate propagation (prevents other listeners on same element)
event.stopImmediatePropagation();
}
Common Event Types to Listen For
- User Input Events:
change,input,keyup,keydown,click,blur,focus - Form Events:
submit,reset - Mouse Events:
mouseenter,mouseleave,mouseover,mouseout,mousemove - Custom Events: Your own event names like
recordselected,statuschange, etc.
DOM Query Selectors
// Get a single element
const element = this.template.querySelector('.my-class');
// Get multiple elements
const elements = this.template.querySelectorAll('.list-item');
// Get a child component
const childCmp = this.template.querySelector('c-child-component');
// Call a public method on child component
if (childCmp) {
childCmp.publicMethod();
}
Resources
Happy coding!

One thought on “Event Communication in Salesforce Lightning Web Components”
Comments are closed.