Event communication is a fundamental concept in Lightning Web Components (LWC) development. It allows components to communicate with each other efficiently.
In Lightning Web Components, there are two primary types of events:
Let’s dive into each of these with detailed examples.
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.
Let’s create a simple contact form. A child component dispatches an event when a form is submitted. The parent component handles this event.
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 = '';
}
}
<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>
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);
}
}
<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>
new CustomEvent() constructor creates a custom event 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 boundariesoneventname={handlerMethod}.event.detail.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.
Let’s create a component that filters contacts based on a search term provided by the parent component.
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);
}
}
<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>
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;
}
}
<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>
In our examples, we used bubbles: true and composed: true in our custom event configuration. Let’s understand what these options do:
true, the event bubbles up through the DOM tree, allowing ancestor components to catch it.true, the event can cross shadow DOM boundaries, which is necessary for events to work properly in LWC.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>
// 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>
disconnectedCallback() lifecycle hook.// 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);
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}`);
}
Parent HTML:
<template>
<c-child-component
record-id={recordId}
record-name={recordName}
is-active={activeStatus}>
</c-child-component>
</template>
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
}
}
}
| 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 |
selectitem, statuschange, recordupdatedclick, change, submit, selectcontactselect is better than just selectimport { 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
}
}
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();
}
change, input, keyup, keydown, click, blur, focussubmit, resetmouseenter, mouseleave, mouseover, mouseout, mousemoverecordselected, statuschange, etc.// 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();
}
Happy coding!
[…] Event Communication in Salesforce Lightning Web Components […]