Product teams should be thinking differently about documents.Read blog post.
Anvil Logo
Products
Industries
Resources
Developers
Engineering

Using `MessageChannel` to call functions within iframes

Author headshot
By Winggo Tse

Do you frequently use iframes in your apps? Figuring out clean simple ways to communicate between the app and iframes can be tricky with all sorts of edge cases. This post will give you some insight on how I solved a problem I'm sure many encounter.

Back to all articles
Using `MessageChannel` to call functions within iframes

An iframe, or inline frame, is a HTML element that embeds another HTML page in an existing page. Remember seeing blog posts on the internet with the Facebook 'like' button? Those use iframes. But iframes can be used for all sorts of purposes: show advertisements, play videos, present a tweet, or display a page to sign a contract. At Anvil, we build our product with iframes in mind so customers can embed our product into their application.

A parent page and an iframe hold different browsing contexts. Parents and iframes within the same origin can freely access each other's information because it's all shared. But more often than not, the parent and the iframe will have different origins. In this situation, the same-origin policy becomes an issue. The policy is a security mechanism that blocks code from different origins from interacting with each other. An iframe won't have access to a function from the parent window, for example.

Luckily, Window.postMessage() provides a way to securely communicate between cross-origin environments. The API works by serializing the data using the structured clone algorithm and sending it over to the destination. The data will automatically be deserialized in the receiving end.

The problem

There are many cases where information crucially needs to be passed between iframes and their parent window. Many Anvil customers embed Etch e-sign, a PDF electronic signature tool, into their applications. Messages need to be passed from the e-sign page to the customer app informing it that the signature process is complete. In this case, the messages between the iframe and parent app act as webhooks.

Another scenario where communication with iframes is necessary is when building a sandbox to execute potentially malicious code. Code within iframes live in an isolated browsing context, so the code won't have access to cookies or sensitive information.

In my case, I need to write a function that tells an iframe to execute some code which then returns a response based on the result of the executed code.

As an example, let's imagine a function that accepts an array of numbers as a parameter. Let's call the function sumArrayInIframe(). The function will send the array to an iframe using Window.postMessaage(). The iframe will receive the message, sum up the numbers, then transmit the sum back to the parent window to be returned by sumArrayInIframe().

Let's use postMessage() and add an event listener on both the parent and iframe end.

Parent window code:

// our function which takes an array of numbers and returns the sum
async function sumArrayInIframe (arr) {
  return new Promise((resolve, reject) => {
    // listen for message from the iframe
    window.addEventListener('message', (event) => {
      if (event.origin !== 'http://exampleIframeWindow.com') return

      if (event.data instanceof Error) reject(event.data)
      else resolve(event.data)
    }, false)

    // send message to iframe
    document.getElementById('codeExecutionIframe').contentWindow.postMessage(arr, 'http://exampleIframeWindow.com')
  })
}

// call the function
const sum = await sumArrayInIframe([5,2,3]) // sum is 10

Iframe code:

function sumArray (arr) {
  return arr.reduce((partialSum, num) => partialSum + num, 0)
}

// listen for event from parent window and immediately send back a response
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://exampleParentWindow.com') return

  const sum = sumArray(event.data)
  window.parent.postMessage(sum, 'http://exampleParentWindow.com')
}, false)

This code allows freely transmitting information between the parent and the iframe using window.postMessage(). The crux is the window event handler and the message sending function. Messages can be sent to different destinations using postMessage(), but all messages pass through the same window.addEventListener() handler in global scope.

This could become an issue if two different calls to sumArrayInIframe() are called simultaneously. What if the two responses from the iframe get mixed up and each is received by the other sumArrayInIframe() function call?

For example, sumArrayInIframe([1,-1]) could return 10 while sumArrayInIframe([5,2,3]) could return 0.

The solution

We need to address the issue of associating each response to its respective function call. Instead of having an event listener that is global in scope, it needs to be locally scoped to each function call.

Here we introduce MessageChannel, a channel messaging API interface, that allows scripts from different browsing contexts attached to the same document to communicate directly with each other using two-way channels with ports on each end. Messages can be sent between iframes, a parent and an iframe, and web workers.MessageChannel does not replace Window.postMessage(), but rather complements it.

A MessageChannel is created using the MessageChannel() constructor and consists of two MessagePorts. The first port is attached to the origin context and kept in the parent window, while the second port is attached to the destination context. Thus port2 is transferred to and kept in the iframe.

Instead of adding a global event listener such as window.addEventListener(), we'll be listening through channel.port1 and channel.port2 on the parent and iframe respectively.

Let's take a look at my code using MessageChannel. Parent window:

async function sumArrayInIframe (arr) {
  return new Promise((resolve, reject) => {
    // construct a message channel for each function call
    const channel = new MessageChannel()

    // listen for message on port1 which is attached to the origin
    channel.port1.onmessage = (event) => {
      if (event.data instanceof Error) reject(event.data)
      else resolve(event.data)
      channel.port1.close()
    }

    // send message AND port2 to iframe
    document.getElementById('codeExecutionIframe').contentWindow.postMessage(arr, 'http://exampleIframeWindow.com', [channel.port2])
  })
}

Iframe window:

window.addEventListener('message', (event) => {
  if (event.origin !== 'http://exampleParentWindow.com') return

  const sum = sumArray(event.data)
  // send the result back using port2 which is attached to the destination
  event.ports[0].postMessage(sum)
})

Imagine a railway system. The messages are the trains while the ports are the train stations. Port1 is station A, the train station next to where you live. Port2 is station B, the train station located next to where you work. In this example, we're simply using MessageChannel to receive messages back from the destination. port2 is used to send the returning message, while port1 is used to receive the returning message. By designing our solution this way, you can ensure each returning message arrives at the right destination.

Summary

Many have never encountered the MessageChannel API before as it's kind of niche, but I've been using it and it is a spectacular API that solves many problems for us as developers. While simply using postMessage() can get the job done in many cases, there are specific scenarios where MessageChannel makes your life a whole lot easier. The strength in MessageChannel lies in the fact that you can compartmentalize channels of communication. I've given just one example of how this API can be leveraged, but I'm sure you can find other use cases as well.

We've applied this API and other best practices to our code at Anvil and believe sharing our experiences helps everyone in creating awesome products. If you're developing something neat with PDFs or paperwork automation, let us know at developers@useanvil.com. We'd love to hear from you.

Sign up for a live demo

Request a 30-minute live demo today and we'll get in touch shortly. During the meeting our Sales team will help you find the right solution, including:
  • Simplifying data gathering
  • Streamlining document preparation
  • Requesting e-signatures
  • Building and scaling your business
Want to try Anvil first?Sign up for free
Want to try Anvil first?Sign up for free