Resize a Crossdomain iFrame

When it comes to crossdomain quirks with frames and Ajax, there’s not usually a “good” solution — there’s just one that works. Something I was working on the other week had a “well it works” moment, although the solution was far from ideal.

The Problem

The page in question is the Sponsored These Tweeters page at SponsoredTweets. It’s a pretty basic Wordpress page with an iFrame that contains the list of Tweeters. Real easy way of adding a dynamic touch to a Wordpress site. The issue is that the iframe contains a dynamic amount of content, and could potentially grow or shrink based on the length of amount of tweeters being shown. The kicker? The page with the list of tweeters is at a subdomain, and under https.

Monitoring with Jquery

My first idea with anything like this is “Can I just listen for a jquery event on the element?”. Well, sure, but the height of the iframe won’t change. You’ll only be able to get the height/width of the iframe itself, not the content within it from the page itself, so watching for a resize or load event on the iframe won’t work. What you’re really wanting to listen for is a change in the height of the content

Works? No.

Reaching into the frame with contentWindow

If your frame and the page containing it both exist on the same domain, you can reach into DOM of the frame and get the height that way. At anytime you can do something like this to get the height of the page:

document.getElementById("iframeid")
         .contentWindow.document.body.scrollHeight

This bit of code will return an integer — the height of the content of the frame in pixels. Even if the iframe has a height of (for example) 300px, the content itself could be smaller or larger. Unfortunately you can’t set a jQuery event watcher on document.getElementById("iframeid").contentWindow.document.body either (at least from what I’ve seen), so I can’t see a way to watch for a change in height.

The “not-so-nice” fix for this is to make a really fast function that repeatedly checks a change in the height, and resizes the iframe accordingly. Here’s some sample code for this using jQuery.

var $iframe = $("#iframeid");
function resize_iframe() {
  var current_height = $iframe.css("height");
  if(current_height != $iframe[0].contentWindow.document.body.scrollHeight) {
    $iframe.css("height", $iframe[0].contentWindow.document.body.scrollHeight);
  }
}
setInterval("resize_iframe();", 100);

Everytime I use setTimeout() or setInterval() in Javascript, it’s immediately a red flag. These (along with the evil-eval) can be an obvious code smell, but also a trigger that the code itself isn’t designed in the best way.

The problem with this though is that it won’t quite be realtime. Also the contentWindow.document object is not available cross-domain. Even doing things like contentWindow.document.location.href to get the current location of the frame doesn’t work if the calling page and the frame are on different domains/subdomains. This might work for some cases, but not if your iframe is on a different domain.

Works? Yes.
Works on the same domain? Yes.
Works on different subdomains on the same domain? No.

The document.domain hack

If both your frame and your calling page exist on the same parent domain, there’s a little hack you can do to get this to work. Each page has a document.domain variable that contains the domain from which ajax calls can be made to. If you rails application is up at https://app.sponsoredtweets.com, then your document domain will be set to app.sponsoredtweets.com. Of course your main website sitting at http://sponsoredtweets.com will have a document.domain ot sponsoredtweets.com. Because of this they won’t be able to talk to each other. What you can do is manually change the document.domain value.

Surprisingly enough, you can tweak this value as long as you only get broader in your domain. For instance, if document.domain is app.sponsoredtweets.com, then you’ll be able to manually set this using something like this: document.domain = "sponsoredtweets.com" From that point on, that page will be treated as those requests were coming/going to that domain. Using this, you could set your iframe and the page containing the iframe to be on the same root domain, then reach into it’s contentHeight as described above. After you’ve set this to “sponsoredtweets.com” though, you won’t be able to change it again, as that’s the most general a document.domain can get.

Works on the same domain? Yep.
Works on different subdomains on the same domain? Yes.
Works crossdomain? No.
Works across https? No.

Frame within a Frame within a Frame

Unfortunately, none of this works across domain, of from http connections to https. Here’s how it works for SponsoredTweets:

The main page is http://sponsoredtweets.com/tweeters/sponsor-these-tweeters/, aka, the parent page.
Which has a frame to https://app.sponsoredtweets.com/tweeters, aka, the content page.
Which has a frame to http://sponsoredtweets.com/iframe.html, aka, the placeholder page.

The middle frame is the one that will change in height of course. For the same reasons as above, the middle frame cannot reach into it’s parent frame and call methods there either. It can, however, control it’s own iframe in a very limited sense. For instance, take this line from the

$("#parent_domain")[0].contentWindow.location = 'http://sponsoredtweets.com/iframe.html#' + height;

Whenever the height of the content page changes, it updates the location of it’s frame to include a hash tag followed by the current height of the content. By itself this won’t do anything, but if that iframe.html page is watching for changes to it’s location, we can start from there.

The placeholder page is what makes this all possible. All it has to do is monitor for changes to it’s hash, and when it sees them, send them up to the parent page (that is, the top level page). Now, although the content page can’t talk to it’s parent page, the placeholder page can talk to it since they are both on the same domain. Is it a hack? Oh yeah, and of the worst kind.

<html>
<script type="text/javascript">
<!--
  function sendHash() {
    var location = window.location.href.split("#")
    if(location.length != 2) { return; }
    parent.parent.receiveHash(location[1]);
  }
  setInterval("sendHash();", 100);
-->
</script>
<body></body>
</html>

parent.parent…? Yeah, thanks HTML. All the receiveHash() function on the parent page does is take a number and set it as the height of it’s iframe. Real simple implementation, it’s just the craziness that is cross-domain security that makes it difficult to grasp.

Works across https? Yes!

Got a Better Solution?

I can’t believe this would be the ideal way to do something as simple as resize an iframe. Do you know of a better way of doing this given that the pages exist on separate domains? I’d love to hear about it.


 
 
 

7 Responses to “Resize a Crossdomain iFrame”

  1. Hey Adam,

    Can you post the HTML that you would use for this example? I have a similar issue I’m trying to tackle. The iframe domain is totally separate from my domain. I know just enough jquery to sort of follow your code, but I would like to see the HTML markup to help me get a better grasp of it.

    My setup is basically this (fake domains of course):
    My domain is xyz.com, I need to load an external source from acme.com into an iframe. The source page has a submission button that generates results and the content dynamically growns.

    Will your solution work for this type scenario? I see that your example uses sub domains, so that’s the only reason I’m a bit skeptical in my example. However, you seem like a pretty knowledgeable guy. I would appreciate if you could send me some type of sample HTML markup or .js file that would help me out.

    Thanks!

  2. Hey Dallas,

    You should be able to view source on the 3 pages above to get the full code of these pages, but I’ll try to include the relevant bits in my response as well.

    I think the “Frame within a Frame within a Frame” technique would work in your case since you’re on completely separate domains.

    Basically I see it working like this:
    xyz.com has an iframe to acme.com.
    acme.com has some submission button that makes the page change in size of the content. (this change could be via ajax, or from a full out page reload).
    Somewhere on acme.com you’d need to have an iframe that went back to a different page on xyz.com.

    On acme.com you’d need something like this (using jquery as an example):

    $(function() {
      var height = $("#container_div").height();
      try {
        $("#third_level_iframe")[0].contentWindow.location = 'http://acme.com/iframe.html#' + height;
      } catch(e) { }
    });

    So now when xyz.com loads, it’ll calculate the height of it’s main container div (this should be the entire contents of the page). This xyz.com page has an iframe that goes back to acme.com, and it sets the location of that iframe to be, for example “http://acme.com/iframe.html#645″, where 645 is the height of the content you want to show.

    I included the source above for the “http://acme.com/iframe.html” page — it’s the little html snippet with the sendHash() function. This will send the height from http://acme.com/iframe.html -> http://acme.com (or whatever page has the initial iframe).

    Acme.com then must have a global function called receiveHash() that takes an integer (in this case, it’ll be the height). In the case of SponsoredTweets, you can see that this function is really simple, and could probably be expanded on.

    function receiveHash(height) {
      document.getElementById('tweeters_frame').height = parseInt(height)+60;
    }

    Real straightforward here — just get the iframe that contained xyz.com, and change it’s height (adding a bit of padding).

    Hope it helps!

  3. I only have the link to the external source. I do not have access to acme.com/iframe.html, only the link to it. I cannot add anything to that particular page. So, I don’t see how I will be able to create an iframe on acme.com.

    Hopefully it’s not too confusing. It would be like trying to display google.com content after displaying search results. You don’t have access to the google domain to add another iframe to communicate back to xyz.com.

    That would be my scenario. Maybe I’m missing something.

  4. Dallas,

    Hmm yeah that’ll be a problem ;/ This solution requires you to have control over the code of both the page containing the frame and the frame itself.

    So you’d need to have control over:
    http://acme.com (your page)
    http://xyz.com (the content of the iframe)
    http://acme.com/iframe.html (your page that brings it all together)

    I don’t think there’s a way to do this if you don’t have control over both domains unfortunately. Or at the very none that I know of. :/

  5. Ok. Either way, your solution rocks and does what I’ve seen others not be able to do. I’ll keep it filed away in my goodie bag for future use. Thanks for the help, Adam.

  6. Interesting article but… what a fraud! Title looks like the promised land, article begins well… only to end up finding no solution to the matter stated in the title!

  7. How about cross domain communication. Here is a page that gives a solution:

    http://geekswithblogs.net/rashid/archive/2007/01/13/103518.aspx

    I haven’t tried this, but it seems to look like it would work.