Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Legacy Apps - Dealing with IFRAME Mess (Window.postMessage)

5.00/5 (4 votes)
5 Nov 2018CPOL5 min read 12.3K  
Legacy Apps - Dealing with IFRAME Mess (Window.postMessage)

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:

JavaScript
someIframe.contentWindow.postMessage({ a: 'aaa', b: 'bbb' }, '*');

and to parent of an iframe like this:

JavaScript
window.parent.postMessage({ a: 'aaa', b: 'bbb' }, '*');

This is how you can receive the message:

JavaScript
window.addEventListener('message', function (e) {
    // Do something with e.data...         
});  

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, iframes 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 iframes where you need to modify state of one iframe based on action that happened in another iframe:

Page with many iframes...

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 iframes 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:

JavaScript
document.getElementById('someIframe').contentWindow.document.getElementById('someInput').value = 'test';

and reaching element in parent can be done with:

JavaScript
window.parent.document.getElementById('someInput').value = 'test';

The problem with this approach is that it tightly couples iframes and that’s unfortunate since the iframes 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 iframes. In this post, I’m assuming that the application with iframes 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 iframes 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:

JavaScript
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, iframes 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 iframes about the need to update user information:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)