Configuring Events and Propagation in LWC

Lightning web components dispatch standard DOM events. Components can also create and dispatch custom events. Events in Lightning web components are built on DOM Events, a collection of APIs and objects available in every browser. To communicate up the component containment hierarchy in Lightning Web Components, we have to implement the custom events. 

Creating an event takes following programming design pattern:

  • An event name, called a type
  • A configuration to initialize the event
  • A JavaScript object that emits the event
At the same time one also has to keep in mind while naming the event that
  • No uppercase letters are allowed
  • No spaces are allowed
  • Use underscores to separate words
If you are novice in creating the component events, please visit Create and Dispatch Events module of lightning web component document library.

Now consider you have built an application which uses the component hierarchy modal and now you want to communicate up the hierarchy. Well, to understand this let's take an example. We have following component hierarchy.

Component Hierarchy

Now suppose the Grand Child wants to notify of its behavior change to its parents in the hierarchy. What are the possible ways. 
  1. Simply Grand child fires an event and it will be listened by the parents in the components markup hierarchy - Answer to this is BIG NO Wondered why? 

    LWC is all about shadow DOM and one can't simply cross/leak through DOM unless otherwise is authorized to do so.

    So how to create and dispatch event from Grand Child which will propagate through the DOM up to its root i.e. Grand Parent? Let's crack on it.

  2. Configure Event Propagation - Once an event is fired by Grand Child, it can propagate up through the DOM but when and where this event can be handled, we have to understand the event propagation. 
Configure Event Propagation
Whenever an event is bubbled up, 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  components support only the BUBBLING up of events and NOT CAPTURE phase. Simply think of the event’s path as starting with your component and then moving to its parent, and then grandparent, and so on.

An event, inside the component or more specifically inside the shadow tree, is bound with the target itself. For eg, if we look at the event fired from onchange of lightning-input or onclick of lightning-button in Grand Child, we can refer to the target that fired the event and get the property values with Event.target but what if we are firing an event from Grand Child and if it is handled by Child or Parent or Grand Parent, is it still true to get the target who fired the event? 
Well, the answer is again NO. Why? 

Well, when an event is handled by another component or let's say from outside the shadow boundary, the value of Event.target changes to match the scope of the listener. This change is called Event Retargeting. The event is retargeted so the listener can’t see into the shadow DOM of the component that dispatched the event. Event retargeting preserves shadow DOM encapsulation.
Getting too much complicated. Let's come back to our main agenda i.e. event propagation and you will be all sorted. The propagation depends on two properties of event 
  1. bubbles: Indicates whether an event can bubble up the DOM or not defaulted to false.
  2. composed: Indicates whether an event can pass through the SHADOW boundary or not defaulted to false.
Phew!! we have been through a lot with the event and propagation concept, let's dive into some examples to understand the propagation behavior more practically.

Consider our component structure outlined in the above Component Hierarchy image. 

Grand Child Component:  This has a lightning button which fires an event called notify and other component in the hierarchy up the order wants to listen to this event.

grandChild.html
<template>
    <lightning-layout class="slds-var-m-around_medium">
        <lightning-layout-item class="wide">
            You're in grand child component.
            <lightning-button label="Hit Me On Grand Child" variant="brand" onclick={handleClick}></lightning-button>
            <br /><span class="slds-text-color_success">{text}</span>
        </lightning-layout-item>
    </lightning-layout>
</template>
grandChild.js
import { LightningElement } from 'lwc';

export default class GrandChild extends LightningElement {
    text;

    handleClick(event) {
        event.preventDefault();
        this.text = 'Notify event fired in grand child.';
        // Creates the event
        const customEvent = new CustomEvent('notify');
        // Dispatches the event.
        console.log(this.text);
        this.dispatchEvent(customEvent);
    }
}

Child Component: This component is container for Grand Child component and listens to the event notify.

child.html
<template>
    <lightning-layout class="slds-var-m-around_medium">
        <lightning-layout-item class="wide">
            You're in child component.
            <c-grand-child onnotify={handleNotify}></c-grand-child>
            <span class="slds-text-color_success">{text}</span>
        </lightning-layout-item>
    </lightning-layout>
</template>
child.js
import { LightningElement } from 'lwc';

export default class Child extends LightningElement {
    text;

    handleNotify(event) {
        event.preventDefault();
        this.text = 'Notify event listened in child.';
        console.log(this.text);
    }
}
Parent Component: This component is container for Child component and also listens to the event notify.

parent.html
<template>
    <lightning-layout class="slds-var-m-around_medium">
        <lightning-layout-item class="wide">
            You're in parent component.
            <c-grand-child onnotify={handleNotify}></c-grand-child>
            <span class="slds-text-color_success">{text}</span>
        </lightning-layout-item>
    </lightning-layout>
</template>
parent.js
import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {
    text;

    handleNotify(event) {
        event.preventDefault();
        this.text = 'Notify event listened in parent.';
        console.log(this.text);
    }
}
Grand Parent Component: This is the root container in the component hierarchy and also listens to the event notify.

grandParent.html
<template>
    <lightning-layout class="slds-var-m-around_medium">
        <lightning-layout-item class="wide">
            You're in grand parent component.
            <c-grand-child onnotify={handleNotify}></c-grand-child>
            <span class="slds-text-color_success">{text}</span>
        </lightning-layout-item>
    </lightning-layout>
</template>
grandParent.js
import { LightningElement } from 'lwc';

export default class GrandParent extends LightningElement {
    text;

    handleNotify(event) {
        event.preventDefault();
        this.text = 'Notify event listened in grand parent.';
        console.log(this.text);
    }
}
grandParent.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>51.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
Conclusion: Now let's deploy our code and drag-drop our grantParent component to the home page to test our so far discussed solution. Our home page looks like below

Let's examine the behavior based on certain combination.
  1. bubbles:false and composed:false - This is same as firing event without these event properties since they are defaulted to false by component framework. Just look at grandChild.js file and firing of event.
    const customEvent = new CustomEvent('notify');
    which is same as
    const customEvent = new CustomEvent('notify', {
        bubbles: false,
        composed: false
    });
    Now when you click on Hit Me On Grand Child button on Grand Child component, see the output
    Event is stopped to bubble up higher because of SHADOW DOM boundary. It can't go past the immediate container and hence Child is the ultimate destination where the event has stopped its propagation.

  2. bubbles:true and composed:false -
    const customEvent = new CustomEvent('notify', {
        bubbles: true,
        composed: false
    });
    Here again since the bubbles property is set to true but composed property, which is responsible for an event to cross the SHADOW Boundary, is set to false so we don't see much change in the behavior and output is same as above.
  3. bubbles:true and composed:true -
    const customEvent = new CustomEvent('notify', {
        bubbles: true,
        composed: true
    });
    Here since bubbles property is set to true which allows the event to bubble up the hierarchy and setting composed property to true makes the event accessible outside the SHADOW DOM but with the concept of event retargeting as discussed under Configure Event Propagation section. Look at the output now.
    The event notify has crossed the SHADOW Boundary and reached to its root i.e. Grand Parent in the containment hierarchy.
Stopping the Event Propagation: There might be situation where we don't want the event to be propagated after certain level, how to stop this propagation. Consider we don't want our event to be available after Parent call. Well, simply replace the handleNotify function in parent.js file with the following

parent.js
handleNotify(event) {
    event.preventDefault();
    this.text = 'Notify event listened in parent.';
    console.log(this.text);
    event.stopPropagation();// stops the propagation here and event can't go up the hierarchy
}
Now as you hit the button on Grand Child and compare the result

The event has stopped its propagation beyond the Parent and thus the Grand Parent is no longer able to listen to this notify event.

If you like this blog content and find inciteful, please comment and let me know. 

Comments

  1. Great blog! Very informative.
    If I may, I would like to ask a question.
    I understand that we use event.detail when we are listening to this notify event from a child or up the hierarchy. When do we use event.target? Could you explain this from the above example?

    ReplyDelete
    Replies
    1. Sure, Thanks for asking this question. I will explain the event.target and event.detail in my upcoming blog.

      Delete
  2. Thank you so much.

    ReplyDelete
  3. we use Event.Target to get the access or reference of the component who has fired that event. As it is already mentioned in blog because of event retargeting it will give the reference of only 'c-grand-child' not the 'lightning-button' to preserve the encapsulation. so in parent if you console the event.target.value it will print 'c-grand-child'.

    ReplyDelete
  4. Amazing blog and the gem in crown is the example which explained it so easily when its theory seemed mind boggling. Can you also explain Lightning Messaging Service ?

    ReplyDelete
    Replies
    1. Thanks for the feedback. I will definitely cover Lightning Messaging Service in my upcoming blog.

      Delete
  5. You can now check out my new blog as requested by you on Lightning Message Service
    https://inevitableyogendra.blogspot.com/2021/06/communicate-across-dom-with-lightning-message-service.html
    Hope this will be equally helpful in your learning curve.

    ReplyDelete
  6. while executing your code, getting the error, pls advise.

    Action.prototype.finishAction Error [Error in $A.getCallback() [LightningElement is not defined]
    Callback failed: serviceComponent://flexipage.editor.aura.component.FlexipageComponentController/ACTION$loadComponentDefinitions]
    new Aura.externalLibraries()@https://playful-hawk-5k9krt-dev-ed.lightning.force.com/auraFW/javascript/7FPkrq_-upw5gdD4giTZpg/aura_prod.js:354:407

    ReplyDelete
  7. Amazing blog and very profitable. thanks.

    ReplyDelete
  8. i have a few doubts :

    1. If the properties are set to bubble : false n composed : false but still its crossing the shadow DOM and here child can react to the event triggered in grandchild. But as u have said if composed : false then how its crossing the shadow DOM.

    2. whats the use of bubble : true and composed : false as its showing the same result as bubble : false n composed : false. Pease cite an real time example .

    3. So when a child component is wrapped by the parent component . Then only the shadow DOM gets created or is there any way by which shadom DOM can be created into the component itself.

    4. As you are calling the grandchild component in child and the granchild component in parent as well as in grand parent. So i this manner the child , parent and grandparent would be siblings because if grandchild is called in child , child called into parent and parent called into child the it would have formed a hierarchy.

    Please clear my doubts thanks in advance.

    ReplyDelete
  9. i have a few doubts :

    1. If the properties are set to bubble : false n composed : false but still its crossing the shadow DOM and here child can react to the event triggered in grandchild. But as u have said if composed : false then how its crossing the shadow DOM. Its contadicting the statement.

    2. whats the use of bubble : true and composed : false as its showing the same result as bubble : false n composed : false. Please cite an real time example .

    3. So when a child component is wrapped by the parent component . Then only the shadow DOM gets created or is there any way by which shadom DOM can be created into a component itself.

    4. As you are calling the grandchild component in child and the granchild component in parent as well as in grand parent. So i this manner the child , parent and grandparent would be siblings because if grandchild is called in child , child called into parent and parent called into grand parent then it would have formed a hierarchy.

    Please clear my doubts thanks in advance.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete

Post a Comment