It's October 2018 so I should probably write something about React 16.5, Angular 7.0 or Blazor 0.6... But... How about some fun with iframe-infested legacy application instead? ;)
TL;DR
You can pass a message to embedded iframe
with:
someIframe.contentWindow.postMessage({ a: 'aaa', b: 'bbb' }, '*');
and to parent of an iframe
like this:
window.parent.postMessage({ a: 'aaa', b: 'bbb' }, '*');
This is how you can receive the message:
window.addEventListener('message', function (e) {
});
Mind the security! See my live example and its code on GitHub or go for framebus library if you want some more features.
The Iframes
Unless you work for a startup, chances are that part of your duties is to keep some internal web application alive and this app remembers the glorious era of Internet Explorer 6. It’s called work for a reason, right? ;) In the old days, iframe
s were used a lot. Not only for embedding content from other sites, cross domain Ajax or hacking an overlay that covered selects but also to provide boundaries between page zones or mimic desktop-like windows layout…
So let’s assume that you have a site with nested iframe
s where you need to modify state of one iframe
based on action that happened in another iframe
:
In the example above, top iframe 0
has a text field and if Update User Name button is clicked, we should modify User Name labels in nested iframe 1a
and iframe 1b
. When Update Account Number button is pressed, Account Number in deeply nested iframe 2a
should change. Clicking on Update News should modify text in iframe 2b
. That last iframe
contains a Clear News button, and when it's clicked, a notification should be passed to top iframe 0
...
Direct Access (The Bad Way)
One way of implementing interactions between iframe
s is through direct access to nested/parent iframe
's DOM elements (if same-origin policy allows). State of element in nested iframe
can be modified by such code:
document.getElementById('someIframe').contentWindow.document.getElementById('someInput').value = 'test';
and reaching element in parent can be done with:
window.parent.document.getElementById('someInput').value = 'test';
The problem with this approach is that it tightly couples iframe
s and that’s unfortunate since the iframe
s were likely used to provide some sort of encapsulation. Direct DOM access has another flaw: it gets really nasty in case of deep nesting: window.parent.parent.parent.document
...
Messaging (The Good Way)
Window.postMessage method was introduced into browsers to enable safe cross-origin communication between Window
objects. The method can be used to pass data between iframe
s. In this post, I’m assuming that the application with iframe
s is old but it can be run in Internet Explorer 11, which is the last version that Microsoft released (in 2013). From what I’ve seen, it’s often the case that Internet Explorer has to be supported but at least it’s the latest version of it. Sorry if that assumption doesn’t work for you, I’ve suffered my share of old Internet Explorer support...
Thanks to postMessage
method, it’s very easy to create a mini message bus so events triggered in one iframe
can be handled in another if the target iframe
chooses to take an action. Such approach reduces coupling between iframe
s as one frame doesn't need to know any details about elements of the other...
Take a look at an example function that can send messages down to all directly nested iframes:
const sendMessage = function (type, value) {
console.log('[iframe0] Sending message down, type: ' + type + ', value: ' + value);
var iframes = document.getElementsByTagName('iframe');
for (var i = 0; i < iframes.length; i++) {
iframes[i].contentWindow.postMessage({ direction: 'DOWN', type: type, value: value }, '*');
}
};
In the code above, iframe
s are found with document.getElementsByTagName
and then a message is sent to each of them through contentWindow.postMessage
call. First parameter of postMessage
method is the message (data) we want to pass. Browser will take care of its serialization and it's up to you to decide what needs to be passed. I've chosen to pass an object with 3 properties: first designate in which direction message should go (UP or DOWN), second states the message type (UPDATE_USER
for example) and the last one contains the payload of the message. In the case of our sample app, it will be a text user put into input and which should affect elements in nested iframes. The '*' value passed to contentWindow
method determines how browser dispatches the event. Asterisk means no restrictions - it's ok for our code sample but in the real world, you should consider providing an URI as the parameter value so browser will be able to restrict the event based on scheme, host name and port number. This is a must in case you need to pass sensitive data (you don't want to show it to any site that got loaded into iframe
)!
This is how sendMessage
function can be used to notify nested iframe
s about the need to update user information:
document.getElementById('updateUserName').addEventListener('click', function (event) {
sendMessage('UPDATE_USER', document.getElementById('textToSend').value);
});
Code shown above belongs to iframe 0
which contains two nested iframes: 1a
and 1b
. Below is the code from iframe 1b
which can do two things: handle a message in case it is interested in it or just pass it UP
or DOWN
:
window.addEventListener('message', function (e) {
console.log('[iframe1b] Message received');
if (e.data.type === 'UPDATE_USER') {
console.log('[iframe1b] Handling message - updating user name to: ' + e.data.value);
document.getElementById('userName').innerText = e.data.value;
} else {
if (e.data.direction === 'UP') {
console.log('[iframe1b] Passing message up');
window.parent.postMessage(e.data, '*');
} else {
console.log('[iframe1b] Passing message down');
document.getElementById('iframe2b').contentWindow.postMessage(e.data, '*');
}
}
});
You can see that messages can be captured by listening to message
event on window
object. Passed message is available in event's data
field, hence the check for e.data.type
is done to see if code should handle the message or just pass it. Passing UP
is done with window.parent.postMessage
, passing DOWN
works with contentWindow.postMessage
called on an iframe
element.
iframe 2b
has a button with the following click
handler:
document.getElementById('clearNews').addEventListener('click', function () {
document.getElementById('news').innerText = '';
console.log('[iframe2b] News cleared, sending message up, type: NEWS_CLEARED');
window.parent.postMessage({ direction: 'UP', type: 'NEWS_CLEARED' }, '*');
);
It clears news
text and sends notification to parent window
(iframe
). This message will be received by iframe 1b
and passed up to iframe 0
which will handle it by displaying 'News cleared
' text:
window.addEventListener('message', function (e) {
console.log('[iframe0] Message received');
if (e.data.type === 'NEWS_CLEARED') {
console.log('[iframe0] Handling message - notifying about news clear');
document.getElementById('newsClearedNotice').innerText = 'News cleared!';
}
});
Notice that this time message
handler is quite simple. This is because in the case of top iframe 0
, we don't want to pass received messages.
Example
That's it! Here's a working sample of iframe
"rich" page. Open the browser console to see how messages fly around. Check the repo to see the code, it's vanilla JS with no fancy features since we assumed that Internet Explorer 11 has to be directly supported (checked also in Firefox 62, Chrome 69 and Edge 42).
In my original post a demo iframe is here, but it seams like CodeProject doesn't like such embedding. Go here to see the example: https://en.morzel.net/post/legacy-apps-dealing-with-iframe-mess-window-postmessage#example