I have a visualforce page in lightning console, and I need it to send messages other components in the page. This is troublesome because Salesforce always embeds VF Pages in an iframe, even in classic.
In my research, and from folks in a few different communities I heard about window.postMessage as a possible solution.
I found that the Locker API Viewer did not block it, so I sent about figuring it out.
The following regails my misconceptions and pitfalls along the way.
- .postMessage does not bubble like other events. It is a method invoked on your target window for receiving the message.
- My first instinct was to
console.log(window.parent)
from within the iframe before calling .postMessage from it.- This was a mistake as directly referencing
parent
in an iframe causes a cross site scripting error. - We can invoke
parent.postMessage(...)
, but we cannot directly reference the parent.
- This was a mistake as directly referencing
- The listening window must have an event listener on it (not
document
)- window.addEventListener('message', handlePostMessage)
- Post message takes two arguments
- data, targetOrigin
data
was simple enough, it was my object of information I wanted to send.targetOrigin
was a little more complex. I had to derive the target window's URL from my current location in the iframe window.- First I used $CurrentPage.URL to get the Visualforce page's URL
- Then I stripped that down to the subdomain which is the org's My Domain plus '--c' for a visual force page.
- Then I concatenated that with the lightning domain to end up with
https://<mydomain>.lightning.force.com
. This is the origin of my iframes parent window.
- Now my postMessage from the iframe is securely targeting exactly the parent I want with a strict match on target origin.
- With that, the listener evaluates the message's source origin to make sure it came from my vf page, and I added an additional property in
event.data
to further verify.
Some Code Examples:
<!-- VF Page inside a Lightning Console Tab's iframe -->
<apex:page>
<script>
// derive window.parent.location.href
const visualforceUrl = "{!$CurrentPage.URL}"
const parser = document.createElement('a')
const subdomain = parser.host.substring(0, parser.host.indexOf('.'))
const mydomain = subdomain.replace('--c', '')
const parentOrigin = 'https://' + mydomain + '.lightning.force.com')
const postMessage = () => {
parent.postMessage({
channel: 'someChannelId',
message: 'hi'
},
parentOrigin)
}
postMessage()
</script>
</apex:page>
// EventBusController.js in the Lightning Console Utility Bar
({
init: function(cmp, event, helper) {
// function to handle the postMessage
const handlePostMessage = event => {
// verify the message came from the expected window
if (event.origin.endsWith('.visual.force.com') && event.data.channel === 'someChannelId') {
// handle post message
console.log('post message data', event.data)
}
}
// Add the event listener
window.addEventListener('message', handlePostMessage)
// Before the page unloads, clean up and remove the event listener.
document.addEventListener('beforeunload', () => {
window.removeEventListener('message', handlePostMessage)
})
}
})