Two-way communication between an iframe and its parent page

Iframes get a bad wrap for everything from security problems, to usability and SEO issues. Despite this, they are one of the tools at our disposal, and knowing how to use them effectively could open the door to new solutions to old problems. Being able to send data between the iframe and the parent page is a useful trick for delivering more integrated solutions, rather than the traditional boring "page-in-a-page" way iframes get used.

The method examined here isn't just for iframes though, it will work in any case where you have access to another page's window object (so popups, and embedded web-browsers can join in on the fun too). Iframes are easy to play with though, so we will use them for this example.

Establishing 2-way communication between a child page and its parent can be done a few ways, but generally, the recommended approach is to use window.postMessage, if the technologies you are using support it (sorry IE users).

So let's create a basic page to act as our parent.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Parent Page</title>
  </head>
  <body>
    <h2>Container</h2>
    <textarea id="output" cols="30" rows="10" disabled>awaiting data...</textarea>
    <div>
      <input type="text" id="field" value="type something fun here" />
      <button id="send">Send</button>
    </div>
    <div>
      <iframe
        height="500px"
        id="inner"
        src=""
        frameborder="0"
      ></iframe>
    </div>
  </body>
</html>

Here we have a very basic page. I will use a disabled textarea for showing data from incoming messages from the embedded page, and an input and button for sending messages to said page. Aside from that, we have the guest of honor, the iframe itself, currently with no source for it, because we need to make a page to embed first. Let's do that.

The Embedded Page

This is the page which will be running inside the iframe or popup. Structurally we will make something very similar to the parent page.

<!-- inner.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Embedded Page</title>
  </head>
  <body>
    <h2>Embedded</h2>
    <textarea cols="30" rows="10" disabled id="output">awaiting data...</textarea>
    <input type="text" id="field" />
    <button id="send">Send</button>
  </body>
</html>

Now we need to load this page into the iframe. The easiest way to do this is with something like the npm package serve. If you have npm installed, navigate to the directory where these files are located, and run npx serve.

You should get some output indicating which port the assets are being served on. In my case, they are being served on port 5000. When I visit http://localhost:5000 I will be served the parent page, index.html, and if I visit http://localhost:5000/inner.html, I will get the page I want to embed.

This means I can use that URL as the source for my iframe, so I will set that as the value of my src attribute now.

<iframe
    height="500px"
    id="inner"
    src="http://localhost:5000/inner.html"
    frameborder="1"
></iframe>

With that out of the way, if we visit the index.html page we should now see the iframe containing our other page below it. The next step is to write some JavaScript to facilitate communication between the two pages.

The JavaScript

Firstly, we need to write some code on the index page which will wait for the iframe to load, and the get the reference to its window object so that we can call the window.postMessage function on it.

We will also need to add an event listener to send a message when we click our send button, and an event listener to display any messages we receive from the embedded page.

In index.html, create a <script> tag before the end of your </body> tag which contains something like the following snippet.

<script>
      // assign variables with references to the DOM nodes we will be interacting with
      const output = document.getElementById("output");
      const iframe = document.getElementById("inner");
      const button = document.getElementById("send");
      const field = document.getElementById("field");
      // we will assign this value once the iframe is ready
      let iWindow = null;

      // This event listener will run when we click the send button
      button.addEventListener("click", () => {
        // don't do anything if the iframe isn't ready yet
        if (iWindow === null) {
          return;
        }

        // otherwise, get the value of our text input
        const text = field.value;

        // and send it to the embedded page
        iWindow.postMessage(text);
      });

      // This event listener will run when the embedded page sends us a message
      window.addEventListener("message", (event) => {
        // extract the data from the message event
        const { data } = event;

        // display it in our textarea as formatted JSON
        output.value = JSON.stringify(data, null, 2);
      });

      // Once the iframe is done loading, assign its window object to the variable we prepared earlier
      iframe.addEventListener("load", () => {
        iWindow = iframe.contentWindow;
      });
    </script>

That JavaScript alone won't do much though, we need to add the corresponding code in the embedded page, so add another script tag in inner.html containing the following

<script>
      // set up references to DOM nodes
      const output = document.getElementById("output");
      const button = document.getElementById("send");
      const field = document.getElementById("field");

      // create a variable for the parent window. We will assign it once we get the first message.
      let parent = null;

      // add an event listener to send messages when the button is clicked
      button.addEventListener("click", () => {
        // don't do anything if there is no parent reference yet
        if (parent === null) {
          return;
        }

        // otherwise get the field text, and send it to the parent
        const text = field.value;
        parent.postMessage(text);
      });

      // add an event listener to run when a message is received
      window.addEventListener("message", ({ data, source }) => {
        // if we don't have a reference to the parent window yet, set it now
        if (parent === null) {
          parent = source;
        }

        // now we can do whatever we want with the message data.
        // in this case, displaying it, and then sending it back
        // wrapped in an object
        output.textContent = JSON.stringify(data);
        const response = {
          success: true,
          request: { data },
        };
        parent.postMessage(response);
      });
    </script>

Save all your files, refresh your browser, and give it a try.

iframe-demo.gif

Using this method of sending messages opens up a whole new level of interactivity when it comes to popups, iframes, and embedded browsers, paving the way for some pretty cool interactions if you play your cards right.

Pastebin links to source: