javascript: listen for postMessage events from specific iframe
Asked Answered
T

8

40

I have multiple iframes in a page. Now I have one message event listener for the page, which gets the messages from all of the iframes. I have a workaround to know from which iframe the message is coming.

I would like to make event listeners for each iframe separately. Is this possible?

Terraterrace answered 28/4, 2013 at 18:52 Comment(3)
Don't think that is possible. The window can receive message events from anywhere. If you have a workaround, that is probably ok.Hostler
Just for the sake of curiosity, how do you do your workaround?Mada
I know this is super old, but if you have control of the framed content, you can use the Channel Messaging API to set up distinct channels with separate event listeners for each frame.Bernetta
O
1

No, it's not possible. Best you can do is to have a single handler that routes received messages to helper handlers based on the origin of the message sender.

Oaxaca answered 2/5, 2013 at 13:24 Comment(3)
Can be done by setting a unique name attribute for each iframe. Then inside iframe window.name is the name of the iframe and can be send with postMessage.Gunnel
The comment above should be an answer it's the best solution of all.Phenice
The (currently) highest rated answer shows that it is possible by applying a source filter to the event.Inland
H
106

You must listen on the global message event of the window object, but you can filter the source iframe using the source property of MessageEvent.

Example:

const childWindow = document.getElementById('test-frame').contentWindow;
window.addEventListener('message', message => {
    if (message.source !== childWindow) {
        return; // Skip message in this event listener
    }

    // ...
});
Handbag answered 6/12, 2017 at 13:54 Comment(1)
It didn't always work for me, for some reason. So instead of message.source I had to use message.target[0] then it worked like a charm....Lynn
G
11

If the src attribute of each iframe is unique then you can try this:

On the child:

function sendHeight() {
  // sends height to parent iframe
  var height = $('#app').height();
  window.parent.postMessage({
    'height': height,
    'location': window.location.href
  }, "*");
}

$(window).on('resize', function() {
  sendHeight();
}).resize();

On the parent:

$(window).on("message", function(e) {
    var data = e.originalEvent.data;
    $('iframe[src^="' + data.location + '"]').css('height', data.height + 'px');
});

The child sends it's height and URL to the iframe parent using postMessage(). The parent then listens for that event, grabs the iframe with that URL and sets the height to it.

Gecko answered 2/12, 2015 at 1:3 Comment(2)
I guess I could strip out the height bits. Shall I?Gecko
I think the OP simply wants individual eventListeners for each iframe, but it'd be useless since multiple eventListeners listening on any iframe would be a duplicate because it's the same event. Any duplicated eventListeners are just wasted spice since only one will work whilst any duplicates will be ignored.Mada
G
9

Actually you can. Add a unique name attribute to each iframe. iframe name is passed down to the contentWindow. So inside iframe window.name is the name of the iframe and you can easily send it in post message.

Gunnel answered 7/2, 2017 at 13:13 Comment(0)
G
4

You could use e.originalEvent.origin to identify the iframe.

On the iframe child:

window.parent.postMessage({
  'msg': 'works!'
}, "*");

On the iframe parent:

Javascript

window.addEventListener('message', function(e) {
  console.log(e.origin); // outputs "http://www.example.com/"
  console.log(e.data.msg); // outputs "works!"
  if (e.origin === 'https://example1.com') {
    // do something
  } else if (e.origin === 'https://example2.com'){
    // do something else
  }
}, false);

jQuery

$(window).on('message', function(e) {
  ...
}, false);

So origin contains the protocol and domain from which the postMessage() was fired from. It does not include the URI. This technique assume all iframes have a unique domain.

Gecko answered 2/12, 2015 at 5:9 Comment(1)
Does e.originalEvent.origin return a url as a result of being a serialized string? I'm not all that familiar with jQuery and all I find on that is it returns certain properties inside an objectEvent that was wrapped up for convenience by jQuery. So origin actually can show an event's location.protocol + location.hostname?Mada
O
1

No, it's not possible. Best you can do is to have a single handler that routes received messages to helper handlers based on the origin of the message sender.

Oaxaca answered 2/5, 2013 at 13:24 Comment(3)
Can be done by setting a unique name attribute for each iframe. Then inside iframe window.name is the name of the iframe and can be send with postMessage.Gunnel
The comment above should be an answer it's the best solution of all.Phenice
The (currently) highest rated answer shows that it is possible by applying a source filter to the event.Inland
A
1

One way of detecting where the message came from is by checking which iframe is focused or for my specific scenario which iframe is visible.

Actress answered 15/3, 2016 at 14:34 Comment(0)
S
0

I implemented an iframe proxy. Iframe with in an iframe ( nesting them ). Each iFrame proxy creates it's own unique I'd. Than every message that is sent from child iframe to parent is gets an added field of the iframe proxy I'd. In the parent you then route every message from the iframeproxy to it's dedicated handler. This mechanism separate iframes perfectly

Stannary answered 30/11, 2020 at 17:40 Comment(1)
You should probably show some code (or at least pseudocode) for that.Vagus
L
0

I would like to make event listeners for each iframe separately. Is this possible?

This is very possible if you're willing to give each frame a dedicated MessageChannel.

Doing so will also make your program less vulnerable to certain CSRF attacks if your codebase starts to get more complex, as well as marginally more performant, since you only have to put your major authenticity checks in 1 place (making sure the channel is established securely) rather than having to weave your major authenticity checks into a general-purpose bastion message handler on the child.

async function openSecureChannel_a(exactWindow) {
    const { port1, port2 } = new MessageChannel();
    const { port1: meta_port1, port2: meta_port2 } = new MessageChannel();
    await new Promise((resolve, reject) => {
        meta_port1.onmessage = ((event) => {
            switch (event.data.command) {
                case 'ok':
                    resolve();
                    break;
                default:
                    reject(new Error('Unexpected response.', { cause: event.data }));
                    break;
            }
        });
        exactWindow.postMessage({
            command: 'openSecureChannel',
            port: port2,
            meta_port: meta_port2
        }, { transfer: [port2, meta_port2], targetOrigin: '*' });
        setTimeout(() => reject(new DOMException('The operation timed out.', 'TimeoutError')), 2718.2818284590453);
    });
    return port1;
}


async function openSecureChannel_b(expectedOrigin) {
    if (expectedOrigin === undefined)
        expectedOrigin = location.origin;
    return await new Promise((resolve, reject) => {
        window.addEventListener('message', ((event) => {
            if (event.origin !== expectedOrigin && expectedOrigin !== '*')
                return;
            switch (event.data.command) {
                case 'openSecureChannel':
                    if (event.data.port instanceof MessagePort) {
                        event.data.meta_port.postMessage({
                            command: 'ok'
                        });
                        resolve(event.data.port);
                    } else {
                        event.data.meta_port.postMessage({
                            command: 'error',
                            message: `Expected type MessagePort, got type ${Object.getPrototypeOf(event.data.port).constructor.name}.`
                        });
                    }
            }
        }));
        setTimeout(() => reject(new DOMException('The operation timed out.', 'TimeoutError')), 2718.2818284590453);
    });
}
<p>Demo:</p>

<script type="module">
let frame = document.createElement('iframe');
frame.src = `data:text/html,%3Cscript type="module"%3E
${openSecureChannel_b.toString().trim()};
const securePort_inIFrame = await openSecureChannel_b();
securePort_inIFrame.onmessage = ((event) => {
    const p = document.createElement('p');
    const s = \`Got message from parent: \$\{JSON.stringify(event.data)\}\`;
    p.appendChild(document.createTextNode(s));
    document.body.appendChild(p);
});
securePort_inIFrame.postMessage("Oh, by the way, these channels are bidirectional!");

%3C/script%3E`;
document.body.appendChild(frame);
await new Promise((resolve) => frame.addEventListener('load', () => resolve(), { once: true }));
const securePort_inParent = await openSecureChannel_a(frame.contentWindow);
securePort_inParent.onmessage = ((event) => {
    const p = document.createElement('p');
    const s = `Got message from child: ${JSON.stringify(event.data)}`;
    p.appendChild(document.createTextNode(s));
    document.body.appendChild(p);
});
securePort_inParent.postMessage("HELLO FROM THE PARENT!");
securePort_inParent.postMessage("This messagePort can be re-used!");
</script>
Loralyn answered 26/6, 2024 at 17:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.