Javascript and Iframes
Tags: html, javascript, tutorial
Published: 2019-11-05 19:26:11

From the title you know I'm going to write about [Javascript](https://en.wikipedia.org/wiki/JavaScript) and [Iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe). 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.

```html
<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>
``` 

That's what I called "straightforward".

I knew this and started developing a commenting plugin, [Hyvor Talk](https://talk.hyvor.com) 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 (<iframe>) 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
<html>
<head>
   ...
</head>
<body>
   ...
   <div id="pluginx-iframe-container"></div>
   <script src="https://pluginx.com/iframe-loader.js"></script>
</body>
```

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](https://www.w3schools.com/js/js_scope.asp)**. This can be done by wrapping the code inside a [self-invoking function](https://scriptverse.academy/tutorials/js-self-invoking-functions.html). This prevents the collision between your JS variables and the client's website's JS variables. If you use a bundler like [webpack](https://webpack.js.org/), this is not required as webpack makes all the variables local.

```js
(function() {

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

})(); 
``` 

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.

```js
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
```

Now, we can append the `<iframe>`.

```js
var container = document.getElementById("pluginx-iframe-container");
container.appendChild(iframe);
```

# 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](https://developer.mozilla.org/en-US/docs/Web/Security/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](https://stackoverflow.com/questions/3076414/ways-to-circumvent-the-same-origin-policy) 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](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) 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.

```js
/**
 * 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);
    }

}
```

I used ES6 to write this class. If you are not using that, you can simply convert this to browser-supported Javascript using [Babel](https://babeljs.io/).

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

```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)
```

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>
  ... html stuffs

  <script src="script.js"></script>
</html>
```

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

var iframeMsger = new CrossMessenger(window.parent, clientDomain, iframeEventHandlers)
```

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

```js
var domain = location.protocol + "//" + location.hostname;  
iframe.src = "https://pluginx.com/iframe?domain=" + domain;
```
Then, you can receive it from your back-end language and set the `clientDomain` variable dynamically.

Ex: For PHP
```js
var clientDomain = "<?= $_GET['domain'] ?>"; // make sure this is in the PHP page ;)
```

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). 

```js
(function() {

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

**The `requestHeight()` function**

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

```js
clientMsger.post("iframeHeightRequest");
```

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`.

```js
var iframeEventHandlers = {
    iframeHeightRequest: () => {
        var docHeight = document.body.scrollHeight;
        iframeMsger.post("resize", { height: docHeight });
    }
}
```

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

```js
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);
    }
}
```

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](https://sketch.io/render/sk-ad5c441f974e33f87a45827996feb7c1.jpeg)

### 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.

```js
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);
        }
    }
})();
```

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

```js
DOMObserver(document.body, iframeEventHandlers.iframeHeightRequest);
```

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](https://developer.mozilla.org/en-US/docs/Web/HTTP/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](https://groups.hyvor.com/WebDevelopment/171/javascript-and-iframes) in Hyvor Groups_