Event Communication in Salesforce Lightning Web Components

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:

  1. Custom Events – Component-to-parent communication
  2. 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

  1. The child component creates and dispatches a custom event
  2. The parent component listens for this event using an event handler
  3. 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

  1. 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 event
      • bubbles: Whether the event bubbles up through the DOM tree
      • composed: Whether the event can cross shadow DOM boundaries
  2. The parent component listens for the event using an attribute in the format oneventname={handlerMethod}.
  3. 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

  1. Use events for loose coupling: Events help maintain component independence, making your components more reusable.
  2. 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.
  3. Name events clearly: Use descriptive names that indicate the action or change that occurred (e.g., ‘contactselect’, ‘statuschange’).
  4. Clean up event listeners: Always remove event listeners in the disconnectedCallback() lifecycle hook.
  5. Document your events: Add JSDoc comments that describe the events your component fires, including the structure of the payload.
  6. Consider event directionality:
    • Use Custom Events for child-to-parent communication
    • Use @api properties for parent-to-child communication
  7. 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

OptionTypeDefaultDescription
bubblesBooleanfalseEvent bubbles up through DOM tree
composedBooleanfalseEvent can cross shadow DOM boundaries
cancelableBooleanfalseEvent can be canceled with preventDefault()
detailAnynullData to be passed with the event

Event Naming Conventions

  1. Use camelCase for event names: selectitem, statuschange, recordupdated
  2. Use verbs or verb phrases that describe what happened: click, change, submit, select
  3. Be specific to avoid conflicts: contactselect is better than just select

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

  1. User Input Events: change, input, keyup, keydown, click, blur, focus
  2. Form Events: submit, reset
  3. Mouse Events: mouseenter, mouseleave, mouseover, mouseout, mousemove
  4. 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!

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading