DEV Community

Cover image for Javascript and Iframes
Supun Kavinda
Supun Kavinda

Posted on • Originally published at groups.hyvor.com

Javascript and Iframes

From the title you know I'm going to write about Javascript and Iframes. In everyday life as a developer or a blogger, embedding an Iframe into your website is pretty straightforward.

For instance, if you need to embed a youtube video to your website, you can simply copy the code from Youtube and paste it on your website.

<iframe width="560" height="315" src="https://www.youtube.com/embed/$SOME_ID" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Enter fullscreen mode Exit fullscreen mode

That's what I called "straightforward".

I knew this and started developing a commenting plugin, Hyvor Talk a few months back. It was not straightforward. In this article, I'm going to explain the reason for that. I learned many things and devised some tricks while creating this plugin. I'll explain those in this article. If you ever plan to develop a plugin or anything that loads on other websites using Iframes (something like a commenting plugin) this article will help you to understand how to do certain tasks in an organized way. Also, I'll explain how to overcome the obstacles you will probably face while developing such thing.

For this tutorial, let's assume that you are creating a plugin that is loaded in an iframe on other websites (I'm calling the owner of this other website "client"). Also, your website's domain is pluginx.com and the client's web site's domain is client.com.

Iframes

First, let's look into what's an Iframe.

The HTML Inline Frame element () represents a nested browsing context, embedding another HTML page into the current one - MDN

Instead of just adding HTML code to a website, in this article, I'm going to focus on how to do work with Iframes using Javascript.

Why do we really need Javascript?

Javascript can give you more control with the Iframe. Especially, auto-resizing an iframe when the content in the iframe changes can only be done with Javascript. This requires communication between the iframe (your website) and the website (other one's website). So, the best way is not to give an <iframe> code to the client but to let them add a Javascript file.

The client adds the code to his website. The code includes a <script> tag and a <div> tag (the iframe container).

<html>
<head>
   ...
</head>
<body>
   ...
   <div id="pluginx-iframe-container"></div>
   <script src="https://pluginx.com/iframe-loader.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

Now, we can write the iframe-loader.js which loads the HTML page hosted on your domain.

Adding the Iframe

This task requires simple Javascript. One thing you should remember is to keep all the variables in a local scope. This can be done by wrapping the code inside a self-invoking function. This prevents the collision between your JS variables and the client's website's JS variables. If you use a bundler like webpack, this is not required as webpack makes all the variables local.

(function() {

   var iframe = document.createElement("iframe");
   iframe.src = "https://pluginx.com/iframe"; // iframe content (HTML page)
   iframe.name = "pluginx-main-iframe"; // this is important

})(); 
Enter fullscreen mode Exit fullscreen mode

I'll explain the importance of the name attribute later. Make sure to set a unique name for that.

I found that adding the following attribute makes your iframe well-looking in all the browsers.

iframe.width = "100%";
iframe.allowTranparency = true;
iframe.tabIndex = 0;
iframe.scrolling = "no";
iframe.frameBorder = 0;

iframe.style.border = "none";
iframe.style.overflow = "hidden";
iframe.style.userSelect = "none";
iframe.style.height = "250px"; // set initial height
Enter fullscreen mode Exit fullscreen mode

Now, we can append the <iframe>.

var container = document.getElementById("pluginx-iframe-container");
container.appendChild(iframe);
Enter fullscreen mode Exit fullscreen mode

THE PROBLEM (Same Origin Policy)

If both websites were in the same domain, you can simply execute each other page's function, access variables, and so. But, according to the Same Origin Policy, this cannot be done. The browser restricts parent websites from accessing the iframe window's functions and variables and vice versa if those two are not in the same domain. At first, I thought this is an unnecessary security implementation. But, now I understand it's one of the best ways to secure websites. Why? If your client could access your website with Javascript, They can run your functions, update content, click buttons, add likes, steal user data, and more.

There are several ways to circumvent the Same Origin Policy. The best method is HTML 5 Post Message API.

Post Message API

Never think of any other best friend than the Post Message API until you finish developing this application!

The window.postMessage() method safely enables cross-origin communication between Window objects. Here we can use it to communicate between our plugin and the client's website. We can listen to postMessages via the window.onmessage event. Note that you can only send strings via this.

While creating Hyvor Talk, I created a simple class to wrap this communication. I recommend you to use that as it simplifies the process. Here's the code. I'll explain it next.

/**
 * Creates a messenger between two windows
 *  which have two different domains
 */
class CrossMessenger {

    /**
     * 
     * @param {object} otherWindow - window object of the other
     * @param {string} targetDomain - domain of the other window
     * @param {object} eventHandlers - all the event names and handlers
     */
    constructor(otherWindow, targetDomain, eventHandlers = {}) {
        this.otherWindow = otherWindow;
        this.targetDomain = targetDomain;
        this.eventHandlers = eventHandlers;

        window.addEventListener("message", (e) => this.receive.call(this, e));
    }

    post(event, data) {

        try {
            // data obj should have event name
            var json = JSON.stringify({
                event,
                data
            });
            this.otherWindow.postMessage(json, this.targetDomain);

        } catch (e) {}
    }

    receive(e) {
        var json;
        try {
            json = JSON.parse(e.data ? e.data : "{}");
        } catch (e) {
            return;
        }
        var eventName = json.event,
            data = json.data;

        if (e.origin !== this.targetDomain)
            return;

        if (typeof this.eventHandlers[eventName] === "function") 
            this.eventHandlers[eventName](data);
    }

}
Enter fullscreen mode Exit fullscreen mode

I used ES6 to write this class. If you are not using that, you can simply convert this to browser-supported Javascript using Babel.

Before an explanation of the class, let's use it in our script (iframe-loader.js).


// handle each event from the iframe
var clientEventsHandler = {
    resize: (data) => {
        // resize the iframe
    }
};

messenger
var clientMsger = new CrossMessenger(iframe.contentWindow, "https://pluginx.com", eventHandlers)
Enter fullscreen mode Exit fullscreen mode

While sending messages to the iframe, we will also need to receive messages and react upon data. The clientEventsHandler object will contain those functions. (keys with event name).

Let's go back to our class.

The constructor requires three parameters.

  • The first one is the iframe window on which we call the postMessage() function.
  • Then, the target domain which is your website's domain. This allows us to verify incoming messages using e.origin
  • Finally, the event handlers. I'll show you how to write some common event handlers like the resizing event later.

Now, we can send a message to the iframe using clientMsger.post(eventName, data). Here, data should be an object. It will be converted to JSON and sent to the iframe.

Receiving messages in Iframe

For now, we worked on the Javascript file added to the client's website. Now, we will start working on the iframe's script. We will need to use the same class in the iframe to receive messages.

Your iframe's content (HTML)

<html>
  ... html stuffs

  <script src="script.js"></script>
</html>
Enter fullscreen mode Exit fullscreen mode

script.js

var iframeEventHandlers = {}; // some event handlers as the clientEventHandlers
var clientDomain = ""; // How to find this???

var iframeMsger = new CrossMessenger(window.parent, clientDomain, iframeEventHandlers)
Enter fullscreen mode Exit fullscreen mode

We need to find the client's domain. How I did this was to set GET params when requesting the Iframe.

var domain = location.protocol + "//" + location.hostname;  
iframe.src = "https://pluginx.com/iframe?domain=" + domain;
Enter fullscreen mode Exit fullscreen mode

Then, you can receive it from your back-end language and set the clientDomain variable dynamically.

Ex: For PHP

var clientDomain = "<?= $_GET['domain'] ?>"; // make sure this is in the PHP page ;)
Enter fullscreen mode Exit fullscreen mode

Now, both of our messengers are completed. Let's consider some events that you may need.

Events

1. Resizing

Iframes are not auto-resizing by default. We have to explicitly set the height of the iframe. Otherwise, a part of the iframe won't be displayed. So, resizing is a crucial part of any iframe.

Resizing is quite complex than we think. You need to know the scrollHeight of the iframe document to be able to resize the iframe. This can only be canculated by the iframe.

Here are the important moments to resize the iframe.

  1. When the Iframe loads
  2. When the client's browser is resized
  3. When the height of the contents of the iframe changes. (Ex: New elements added)

No. 1 & 2 (On load and resize)

We can listen to iframe's onload event and the browser's onresize event from the script on client's website (iframe-loader.js).

(function() {

   // setting iframe and appending
   iframe.onload = function() {
       requestResize();
       // do other onload tasks
   }
   window.addEventListener("resize", requestHeight);
})();
Enter fullscreen mode Exit fullscreen mode

The requestHeight() function

This function only does one thing. Requests the height of the iframe from the iframe.

clientMsger.post("iframeHeightRequest");
Enter fullscreen mode Exit fullscreen mode

This sends a message to the iframe with the event name "iframeResizeRequest" and no data.

We have to listen to this event in the iframe. Add the event name and a handler to the iframeEventHandlers in the script.js.

var iframeEventHandlers = {
    iframeHeightRequest: () => {
        var docHeight = document.body.scrollHeight;
        iframeMsger.post("resize", { height: docHeight });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can receive the height from the client's script and resize the iframe.

var clientEventHandlers = {
    resize: () => {
        var height = data.height;

        var winScrollLeft = window.scrollX,
            windowScrollTop = window.scrollY;

            commentsIframe.style.visibility = "hidden";
            commentsIframe.style.height = 0;

            commentsIframe.style.visibility = "visible";
            commentsIframe.height = height + "px"; // + 10 to add little more height if needed

            // scroll to initial position
            window.scrollTo(winScrollLeft, windowScrollTop);
    }
}
Enter fullscreen mode Exit fullscreen mode

This process can be confusing. I had the same feeling in the beginning. But, you can adapt to it quickly. If you feel like "bewildered" take a short break. (I did that, as I remember). After the break, look at this picture.

Resizing Image

No 3. (Iframe height changed)

When you work inside the iframe, new elements can be added. This can cause to change the height of it. So, each time the height changes, we have to resize the iframe.

We have to listen to DOM element changes in the iframe. Here's a perfect function I found from the internet for this task.

var DOMObserver = (function(){
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    return function( obj, callback ){
        if( !obj || !obj.nodeType === 1 ) return; // validation

        if( MutationObserver ){
            // define a new observer
            var obs = new MutationObserver(function(mutations, observer){
                callback(mutations);
            })
            // have the observer observe foo for changes in children
            obs.observe( obj, { childList:true, subtree:true });
        }

        else if( window.addEventListener ){
            obj.addEventListener('DOMNodeInserted', callback, false);
            obj.addEventListener('DOMNodeRemoved', callback, false);
        }
    }
})();
Enter fullscreen mode Exit fullscreen mode

Add this to script.js and then call the function.

DOMObserver(document.body, iframeEventHandlers.iframeHeightRequest);
Enter fullscreen mode Exit fullscreen mode

Each time a node is added or removed, the iframeEventHandlers.iframeHeightRequest function will be called. Iframe will be resized!.

Besides resizing, you can add any event and convey messages between the iframe and the client.

If you are creating a plugin or anything that loads inside an iframe, here are some tips from me.

  • Keep all the data in your window. Only share the needed ones with the client website. Never share sensitive data like user's data.
  • Do all AJAX stuff in your iframe.
  • Never use CORS and give access to your website from others. Always use postMessage API.

I hope this article helped you to understand how to use Javascript with Iframes. I tried my best to explain what I learned while creating Hyvor Talk. If I missed something, I'll add it in the future. Also, If you like the article please share it and comment.

Thank you. :)

This post is originally posted in Web Developers Group in Hyvor Groups

Top comments (1)

Collapse
 
fenix profile image
Fenix

Thanks for sharing, ... Namaste ;.)