Closing an iframe the easy way

I’d like to show you our thought process for a specific but important part of this integration, namely closing the configurator.
Geschreven door Ben Cavens

A few days ago we implemented a small but interesting feature in one of our clients' Shopify shop. The client liked to integrate a 3D product configurator of their catalog in the shop. The requirement was that when the configurator gets closed, the chosen products would automatically get placed in the shopping cart. 

This is where we come in. I’d like to show you our thought process for a specific but important part of this integration, namely closing the configurator.

How it started 

The idea was to keep this closing logic as simple as possible. The third party app has its own set of css and scripts so it was immediately clear that the configurator should be rendered inside an iframe. 
Our main focus points were:

  • The configurator should be able to close itself.
  • When reopening the configurator, it’s previous state should be preserved.

Leveraging the postmessage api

When working with an iframe - and especially when the iframe domain doesn't belong to the same origin - the options are limited. The best solution with decent browser support is the postmessage api. The window.postMessage() method safely enables cross-origin communication between a page and an iframe embedded within it. (mdn web docs on postmessage)

In a cross domain communication, you cannot use parent to retrieve or manipulate its inner DOM elements. However pushing and retrieving a message is allowed and valid.

We have setup the following script on the shop side:

const configuratorIframe = document.getElementById('configuratorIframe');

window.addEventListener('message', function (e) {
    if (e.origin !== PRODUCT_CONFIGURATOR_URL) return;

    // Close the iframe request coming from the 3D configurator
    if (e.data === "close-iframe" || e.message === "close-iframe") {
        configuratorIframe.style.display = 'block';
    }
});

On the iframe side of things, this could now be triggered by the following script:

parent.postMessage('close-iframe', PRODUCT_CONFIGURATOR_URL);


Yay we’re done! Well…. 

Adding some protection

There are two minor but important security measures to take into account when you listen for messages. Since these messages could come from any source you should always validate the origin and only act on the message when the origin is valid. Otherwise the request cannot be trusted and should be aborted. 

if (e.origin !== PRODUCT_CONFIGURATOR_URL) return;

The other thing is that you should never trust the payload. Always check and sanitize the incoming value. Here we explicitly check the message against our expected 'close-iframe' string. Should this not match, nothing will happen, but more importantly neither will bad things.

Preserve state upon return

There is still an improvement to make. When we close the iframe and reopen it, the DOM content will be fetched again and its state is reset. We don't want that. The current state within the iframe should be maintained. Luckily there is an easy fix for this.

We create a small function that will take care of opening the iframe. When it opens for the first time, the iframe src will be set. When opening the iframe a second time, the same src reference is kept which will retain its state. As an added bonus this will make sure that the src is deferred to only load when needed.

function openConfigurator(){
    if(!configuratorIframe.src) {
        configuratorIframe.src = PRODUCT_CONFIGURATOR_URL;
    }
    configuratorIframe.style.display = 'block';
}


Altogether, and with a bit of refactoring, the shop script looks something like this:

const configuratorIframe = document.getElementById('configuratorIframe');

window.addEventListener('message', function (e) {
    if (e.origin !== PRODUCT_CONFIGURATOR_URL) return;
    
    // Close the iframe request coming from the 3D configurator
    if (e.data === "close-iframe" || e.message === "close-iframe") {
        closeConfigurator();
    }
});

function openConfigurator(){
    if(!configuratorIframe.src) {
        configuratorIframe.src = PRODUCT_CONFIGURATOR_URL;
    }
    configuratorIframe.style.display = 'block';
}

function closeConfigurator(){
    configuratorIframe.style.display = 'none';
}

Conclusion

I hope this peak into one of our daily coding challenges could show you that web development is far more than just execution. It’s taking ownership of a problem and tackle it with creativity and simplicity in mind. Take the time to think. Finding an easy fix is often the hardest challenge. 

If you'd like to hear more of these kind of topics or get us to touch on other subjects as well, I would love to hear from you.