Resizing the Dang iframe

Rob Simmons
June 2018

The JS in the iFrame below (setup.html) dynamically attaches CSS files and the "Box 2" div, and also makes sure the <textarea> is tall enough to hold its text. (This mimics a situation where we may not know which content CSS files we're serving when we serve the base page.) There are two sources of delay: the page's JS waits to load the file, and the server waits a second before serving the file. Both style files are simple: bigstyle-1.css makes Box 1 big and red, and bigstyle-2.css makes Box 2 big and blue. We end up with a nice Dutch national flag.

There are two problems:

  1. This only looked okay because I knew ahead of time how tall the box would be.
  2. If you make that text box bigger, everything looks bad.

Resizing an iFrame

Resizing an iFrame isn't hard if you're not working with HTML files on different domains. (If you are working with HTML files on different domains, none of this will work at all except for the iframe-resizer option I talk about at the very end.) Here's a function that lets code inside the iFrame reach out and resize itself.

function resize() {
  if (window.frameElement && window.frameElement.tagName === "IFRAME") {
    window.frameElement.height = `${document.body.offsetHeight}px`;
  };
}

The hard problem, from my perspective, is making sure this function gets called at the right time(s). (Also, per @himmxt on Twitter, you may want to think about using getClientBoundingRect() instead of offsetHeight if your frames are doing anything remotely fancy.)

(You'll notice that the pages I link to will use document.body.offsetHeight + 2, which is a hack because I always gave the iFrames on this page a 1 pixel border. Don't do that yourself.)

If you're not dynamically adding content to the DOM by inserting <link> elements (or using require.js or whatever), then just call the function above on the load event. As you can see from pageload.html below, that doesn't work for us at all. There are two reasons.

  1. The load event fires seconds before the CSS is requested, much less loaded.
  2. Even if that didn't happen, the page's size could change if stuff gets written into the <textarea>.

This writeup is about how to resize correctly when you're dynamically loading content and when the content size might change, in mid-2018.

ResizeObserver: the best

If all the platforms you care about implement ResizeObserver (only Chrome in mid-2018), then I hate you a little bit, but you're having a good day. See resizeobserver.html.

MutationObserver: it tried, ok?

MutationObserver fixes one of our two problems: whenever we muck about with the DOM, it allows us to check the height and reset it if it's wrong. This means that the iframe initially looks terrible, but when we interact with it it pops into shape. See mutationobserver.html.

A lot of existing advice on the internet seems to be MutationObserver + screwing around with setTimeout to get the initial resize to happen after the page has settled down but before the user notices. Ugh.

(Warning: the only reason that clicking in the textarea fixes the height is that the textarea's automatic resizing does DOM manipulations on every click and change. Without those manipulations, just clicking or writing in the textarea wouldn't trigger the MutationObserver.)

Grabbing the CSS load event

We fix the OTHER problem in onload.html by attaching an onload event to the dynamically-added CSS element. This is pretty sub-optimal, because we have to successfully track down all the places in our code where we're adding <link> elements and add events to them. As far as I know, this is the only way to correctly trigger on events from these dynamically added stylesheets.

With our powers combined?

Now that we have a solution that fixes both problems, can we combine them to get a cross-platform solution for resizing the dang iframe? (solution1.html)

Well, no.

From the top: iframe-resize

(This is an update to the original post.)

We've spent the whole document acting like the whole solution has to fit within the framed page exclusively. Except for the very first iFrame, where we set the height correctly in advance, the iFrames on this page have been included like this:

<iframe src="/whatever.html"></iframe>

If we have to work between domains, then this is absolutely never going to work. For security reasons, when all the code and HTML isn't coming from the same domain, the parent frame has to give permission for the iframe to resize. (If you look at that original resize() function I wrote, window.frameElement will be null if the domains don't match.

David Bradshaw's iframe-resizer makes this tricky cross-domain thing work: it basically helps the parent page and the framed page cooperate. For the browers I'm testing with, this solution doesn't help me. In my specific I don't have much control over the iFrames my code is being included in, even though they come from the same domain, and Bradshaw's library is about the same as using MutationObserver (in fact, that's how the library works).

To demonstrate this solution here, I add one line to this page's <head> section.

<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/3.6.1/iframeResizer.min.js"></script>

Additionally, the actual iframe creation looks more complicated:

<iframe src="/iframe-resizer.html" id="iframe-resize-example"></iframe>
<script>iFrameResize({warningTimeout: 0}, "#iframe-resize-example")</script>

I also need to include a companion script, iframeResizer.contentWindow.min.js, in iframe-resizer.html. Furthermore, if just using the default options, iframe-resizer has the same problem that my MutationObserver solution has. In order for the iframe below to look right, I also needed to add an explicit resize check call when the CSS finished loading.