Communicate Across the DOM with Lightning Message Service

In my previous blog, we learned about how to Configuring Events and Propagation in LWC. While that blog primarily focuses on the communication behavior within the component markup hierarchy, this particular blog zeroes in on the disconnected architecture i.e. when the components are not in the markup hierarchy, how to do the handshaking. 

The Lightning message service lets you define the scope of where subscribing components receive messages in your application. You can limit the scope to the active area of the application or set the scope to the entire application. Scope of LMS can be understood with the following flow. 

LMS Scope

To know more on Scope of Lightning Message Service, click here.

Let's consider an example to understand Lightning Message Service and when to use it. We have a main container component, housing couple of child components(Inner Components). So how to make the child components(Inner Components) listen to the behavior change in the parent component(Container Component in our case)

  1. Using Lightning Messaging Service👍 (Highly Recommended) 
  2. Using public method(@api decorated) in the Inner Component : 👀(Look out for possibility)
    Only applicable when we have parent-child relationship. If components are fully disconnected, this option is completely ruled out.
  3. Using pub-sub mechanism : 👎(Used where Lightning Messaging Service has limitation. To know more on LMS limitation, click here.
Since the main topic of our discussion is to know how to communicate across DOM using Lightning Message Service, let's focus on that approach only. Consider the following component modal.
Disconnected Component Modal

Now assume the main component 1 wants to notify of its behavior change to its inner components as well as another main component 2. To implement this scenario, we have to think of all possible scenarios of communicating to DOMs. 
NOTE: We can't simply use the Custom Event from main component 1 to make the other components listen to its behavior change. The reason behind this is the fact that the Custom Event programming modal works within the component hierarchy i.e. connected architecture and not outside the DOM or disconnected architecture.

Let's build up the solution to understand such communication behavior. Let's simplify our design with the following.

  1. Create a Lightning Message Channel: This is the first step to implement the lightning messaging  service. Create a folder first under force-app/main/default/messageChannels as below:

    Once done, create a message channel notify.messageChannel-meta.xml as below:

    notify.messageChannel-meta.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
        <masterLabel>NotifyMessageChannel</masterLabel>
        <isExposed>false</isExposed>
        <description>This is a sample Lightning Message Channel for Demo purpose.</description>
        <lightningMessageFields>
            <fieldName>textOne</fieldName>
            <description>This is to change the text of inner component one</description>
        </lightningMessageFields>
        <lightningMessageFields>
            <fieldName>textTwo</fieldName>
            <description>This is to change the text of inner component two</description>
        </lightningMessageFields>
        <lightningMessageFields>
            <fieldName>textContainerComponentTwo</fieldName>
            <description>This is to change the text of container component two</description>
        </lightningMessageFields>
        <lightningMessageFields>
            <fieldName>textClass</fieldName>
            <description>This is to set the text color in the inner component</description>
        </lightningMessageFields>
    </LightningMessageChannel>
  2. Inner Component 1
    innerComponentOne.html
    <template>
        <span class={textClass}>{text}</span>
    </template>
    innerComponentOne.js : 
    import { LightningElement, api, wire } from 'lwc';
    // Import message service features required for subscribing and the message channel
    import {
        subscribe,
        unsubscribe,
        MessageContext
    } from 'lightning/messageService';
    import notify from '@salesforce/messageChannel/notify__c';
    
    export default class InnerComponentOne extends LightningElement {
        @api text;
        @api textClass;
    
        @wire(MessageContext)
        messageContext;
    
        // Encapsulate logic for Lightning message service subscribe and unsubsubscribe
        subscribeToNotifyMessageChannel() {
            if (!this.subscription) {
                this.subscription = subscribe(
                    this.messageContext,
                    notify,
                    (message) => this.handleNotifyMessage(message),
                );
            }
        }
    
        unsubscribeToNotifyMessageChannel() {
            unsubscribe(this.subscription);
            this.subscription = null;
            console.log('Notify message unsubscribed in inner component one.');
        }
    
        // Handler for message received by component
        handleNotifyMessage(message) {
            console.log('Notify message subscribed in inner component one with message.');
            this.text = message.textOne;
            this.textClass = message.textClass;
        }
    
        // Standard lifecycle hooks used to subscribe and unsubsubscribe to the message channel
        connectedCallback() {
            this.subscribeToNotifyMessageChannel();
        }
    
        disconnectedCallback() {
            this.unsubscribeToNotifyMessageChannel();
        }
    }
  3. Inner Component 2 : 
    innerComponentTwo.html : 
    <template>
        <span class={textClass}>{text}</span>
    </template>
    innerComponentTwo.js : 
    import { LightningElement, api, wire } from 'lwc';
    // Import message service features required for subscribing and the message channel
    import {
        subscribe,
        unsubscribe,
        MessageContext
    } from 'lightning/messageService';
    import notify from '@salesforce/messageChannel/notify__c';
    
    export default class InnerComponentTwo extends LightningElement {
        @api text;
        @api textClass;
    
        @wire(MessageContext)
        messageContext;
    
        // Encapsulate logic for Lightning message service subscribe and unsubsubscribe
        subscribeToNotifyMessageChannel() {
            if (!this.subscription) {
                this.subscription = subscribe(
                    this.messageContext,
                    notify,
                    (message) => this.handleNotifyMessage(message),
                );
            }
        }
    
        unsubscribeToNotifyMessageChannel() {
            unsubscribe(this.subscription);
            this.subscription = null;
            console.log('Notify message unsubscribed in inner component two.');
        }
    
        // Handler for message received by component
        handleNotifyMessage(message) {
            console.log('Notify message subscribed in inner component two with message.');
            this.text = message.textTwo;
            this.textClass = message.textClass;
        }
    
        // Standard lifecycle hooks used to subscribe and unsubsubscribe to the message channel
        connectedCallback() {
            this.subscribeToNotifyMessageChannel();
        }
    
        disconnectedCallback() {
            this.unsubscribeToNotifyMessageChannel();
        }
    }
  4. Main Component 1: This component contains the above inner components.
    containerComponentOne.html
    <template>
        <lightning-card>
            <lightning-layout class="slds-var-m-around_medium">
                <lightning-layout-item class="wide" style="width: 100%;">
                    <div class="slds-var-p-around_small">
                        <p>You're in container component one.</p>
                        <lightning-button label="Hit Me On Container" variant="brand" onclick={handleClick}>
                        </lightning-button>
                    </div>
                    <div class="slds-box">
                        <p>Inner Component One : <c-inner-component-one text={textOne} text-class={textClass}>
                            </c-inner-component-one>
                        </p>
                    </div>
                    <div class="slds-var-m-top_medium slds-box">
                        <p>Inner Component Two : <c-inner-component-two text={textTwo} text-class={textClass}>
                            </c-inner-component-two>
                        </p>
                    </div>
                </lightning-layout-item>
            </lightning-layout>
        </lightning-card>
    </template>
    containerComponentOne.js
    import { LightningElement, wire } from 'lwc';
    // Import message service features required for publishing and the message channel
    import { publish, MessageContext } from 'lightning/messageService';
    import notify from '@salesforce/messageChannel/notify__c';
    
    export default class ContainerComponentOne extends LightningElement {
        textOne = 'Inner Component one didn\'t receive message from container component one';
        textClass = 'slds-text-color_destructive';
        textTwo = 'Inner Component two didn\'t receive message from container component one';
    
        @wire(MessageContext)
        messageContext;
    
        connectedCallback() {
            console.log('Container component is called.');
        }
    
        handleClick(event) {
            event.preventDefault();
            const payload = {
                textOne: 'Inner Component One has received text from container component one via lms.',
                textTwo: 'Inner Component Two has received text from container component one via lms.',
                textContainerComponentTwo: 'Container Component Two has received text from container component one vis lms',
                textClass: 'slds-text-color_success'
            };
            console.log('notify message channel published with the message to communicate across DOM');
            publish(this.messageContext, notify, payload);
        }
    }
    containerComponentOne.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>
  5. Main Component 2: This component has no relationship to any of the above components and is stand alone component on the home page which tries to listen to behavior change of Main Component 1
    containerComponentTwo.html
    <template>
        <lightning-card>
            <lightning-layout class="slds-var-m-around_medium">
                <lightning-layout-item class="wide" style="width: 100%;">
                    <div class="slds-var-p-around_small">
                        <p>You're in container component Two.</p>
                        <span class={textClass}>{text}</span>
                    </div>
                </lightning-layout-item>
            </lightning-layout>
        </lightning-card>
    </template>
    containerComponentTwo.js
    import { LightningElement, wire } from 'lwc';
    // Import message service features required for subscribing and the message channel
    import {
        subscribe,
        unsubscribe,
        MessageContext
    } from 'lightning/messageService';
    import notify from '@salesforce/messageChannel/notify__c';
    
    export default class ContainerComponentTwo extends LightningElement {
        text = 'Container Component two didn\'t receive message from container component one';
        textClass = 'slds-text-color_destructive';
    
        @wire(MessageContext)
        messageContext;
    
        // Encapsulate logic for Lightning message service subscribe and unsubsubscribe
        subscribeToNotifyMessageChannel() {
            if (!this.subscription) {
                this.subscription = subscribe(
                    this.messageContext,
                    notify,
                    (message) => this.handleNotifyMessage(message),
                );
            }
        }
    
        unsubscribeToNotifyMessageChannel() {
            unsubscribe(this.subscription);
            this.subscription = null;
            console.log('Notify message unsubscribed in container component two.');
        }
    
        // Handler for message received by component
        handleNotifyMessage(message) {
            console.log('Notify message subscribed in container component two with message.');
            this.text = message.textContainerComponentTwo;
            this.textClass = message.textClass;
        }
    
        // Standard lifecycle hooks used to subscribe and unsubsubscribe to the message channel
        connectedCallback() {
            this.subscribeToNotifyMessageChannel();
        }
    
        disconnectedCallback() {
            this.unsubscribeToNotifyMessageChannel();
        }
    }
    containerComponentTwo.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 containerComponentOne and  containerComponentTwo to the home page to test our so far discussed solution. Our home page looks like below

Now Consider hitting on the button "Hit Me On Container" on containerComponentOne and look at the behavior change on both inner components and container component two.
If you further wish to learn on how to use LMS with Aura and VisualForce application, visit this blog.

Hurrah!! You have mastered on using Lightning Message Service and communicating across the DOM.

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

Comments