0

A new component I'm building uses Platform Events to communicate async server-side changes to the front end. This works great in LWC in Lightning Experience using empApi, but empApi isn't supported in Classic, and most of my users have not migrated yet. In the meantime, we have to embed our new Lightning Web Components in Visualforce via Lightning Out.

Searching for a workaround, I have successfully implemented CometD in the Visualforce container to subscribe to Platform Events. But I'm not sure there is any way to pass the event payloads from Visualforce down to the LWC components when they're received. All the docs I've read recommend Aura Events or Lightning Message Service, but these approaches rely on Lightning Experience / one.app. And again, my users are in Classic.

What else can I try?

1

Yes cometD is the way for VisualForce Page. And to communicate from VF page to LWC you can use postMessage. (Posting a message from VF Page and listening in LWC). Also there is a Lightning messaging channel in salesforce which communicates in different domains, but I have not tried that.

But I know for sure postMessage works without any issues, even if the VFPage is hosted as an iframe.

For eg:

LWC for eg named : (customSearchSobjectLWC)

// eslint-disable-next-line @lwc/lwc/no-async-await
async connectedCallback() {
    window.addEventListener('message', objEvent => {
        alert(JSON.parse(objEvent.data));
    });
}

VFPage:

<apex:page id="testPage">
    <apex:includeLightning />

    <div id="lightning" />

    <script>
        $Lightning.use("c:VFApp", function() {
            $Lightning.createComponent("c:customSearchSobjectLWC",
                {},
                "lightning",
                function(cmp) {
                    console.log("button was created");
                    // do some stuff
                }
            );
        });
        function firePostMessage(){
            const objParentMessage = {
                strDestinationId: 'AnyDestID',
                attrValue: 'Unique Attr Value'
            };
            window.postMessage(JSON.stringify(objParentMessage), '*');
        }
    </script>
    <div id="firePostMessage">
    <button onclick="firePostMessage()">FirePostMessage</button>
    </div>
</apex:page>

Application:

<aura:application access="GLOBAL" extends="ltng:outApp">
    <aura:dependency resource="c:customSearchSobjectLWC"/>
</aura:application>
4
  • I'd like to better understand this approach. Can you provide a code example that illustrates the postMessage call in Visualforce and the listener/handler in LWC? – Matthew Souther Nov 25 '20 at 1:06
  • 1
    Sure updated an example of postMessage being fired from VFPage which is hosting a LWC component and LWC is listening to those events. – Nagendra Singh Nov 25 '20 at 1:23
  • 1
    While the answer I put forward does work, I'm going to mark this answer as the best one. Unlike my solution, any LWC in the containment hierarchy can listen for these message events, without the top-level LWC needing to handle anything. Quick note: for security's sake, I set the 2nd arg in the postMessage call to "/" instead of "*". This checks to make sure only target elements with the same domain as the sender can receive the messages. The LWC was still able to receive messages after I did this. – Matthew Souther Nov 25 '20 at 17:17
  • Yes, postMessage 2nd argument is target origin, we put "*" generally for cross domain message transfer. I am a bit surprised to see "/" working as the message is triggered from VFDomain and being listened in Lightning domain. But if it works its good. – Nagendra Singh Nov 25 '20 at 17:24
0

I was able to get this to work. For posterity...

  1. Create an @api-decorated function in your Lightning Web Component, like this:
@api addEvent = event => {
    this.events.push(event);
};
  1. In Visualforce, the $Lightning.createComponent() callback function gets an argument, which in these docs is called cmp. cmp is an object with a property called elements, which is an Array of DOM elements including the one that has just been created. The big news to me is you can call the @api-decorated function on that element. Like so:
<apex:page standardController="Opportunity" title="My Page">

    <apex:includeScript value="{!URLFOR($Resource.cometd_5_0_2, '/cometd.js')}"/>
    <apex:includeScript value="{!URLFOR($Resource.cometd_5_0_2, '/cometdReplayExtension.js')}"/>

    <apex:includeLightning />
    
    <div id="lwcApp" />

    <script>
        // SUBSCRIBE TO PLATFORM EVENTS
        const cometd = new org.cometd.CometD();
        const channel = '/event/My_Custom_Event__e';
        cometd.configure({
            url: `https://${window.location.hostname}/cometd/50.0/`,
            requestHeaders: { Authorization: 'OAuth {!$Api.Session_ID}'},
            appendMessageTypeToURL: false,
        });
        cometd.websocketEnabled = false; 

        let lwcCmp;
        // Note the cmp arg in the callback function
        const lwcCreatedCallback = function(cmp) {
            console.log("Component created."); 
            lwcCmp = cmp.elements[0];
            // Handshake with the server.
            cometd.handshake(function(h) {
                if (h.successful) {
                    // Subscribe to receive messages from the server.
                    cometd.subscribe(channel, function(m) {
                        // THIS IS HOW YOU COMMUNICATE WITH LWC
                        lwcCmp.addEvent(m.data.payload);
                    });
                }
            });
        }
        
        // CREATE LIGHTNING COMPONENT
        $Lightning.use("c:My_Lightning_Out_App", function() {
            $Lightning.createComponent(
                "c:myLwcComponent", 
                { recordId: '{! Opportunity.Id }' }, 
                "lwcApp",
                lwcCreatedCallback
            ); 
        });
    </script>
</apex:page>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.