Monday, August 4, 2014

Talk to and iFrame and back across hosting at two different domain names!

This isn't as easy as I make it seem here. Let's say I have this web page hosted at http://foo/index.html:

<!DOCTYPE html>
<html>
   <body>
      <script context="text/javascript">
         var theOtherWorld;
         function findTheOtherWorld(world) {
            theOtherWorld = world;
         };
         function reachIntoTheOtherWorld() {
            alert(theOtherWorld.cat.about());
         };
      </script>
      <p>This page holds an iframe at: "foo"</p>
      <iframe name="eye" src="http://bar/index.html"
            onload="findTheOtherWorld(window.eye);">
      </iframe>
      <div>
         <button onClick="reachIntoTheOtherWorld();">reach into the iFrame</button>
      </div>
   </body>
</html>

 
 

...and let's also say that at http://bar/index.html lives the page nested inside of the iFrame at http://foo/index.html which looks like this:

<!DOCTYPE html>
<html>
   <body>   
      <script context="text/javascript">
         var cat = {
            name: "Patches",
            color: "Calico",
            about: function() {
               return this.name + " is " + this.color;
            }
         };
      </script>
      <p>This page sits inside an iframe at: "bar"</p>
   </body>
</html>

 
 

Well, then when I press the button it's going to throw up an alert like so, correct?

Wrong! That's not what is going to happen. If I were not trying to reach across from the hosting at one domain name to hosting at another the code above would work great, but alas that's not our circumstance and I'm going to get an error in the console like so:

Uncaught SecurityError: Blocked a frame with origin "http://foo" from accessing a frame with origin "http://bar". Protocols, domains, and ports must match.

 
 

I found this online which gives us a solution to this problem. To accomodate the fix, I can refactor my web page with an iFrame to be like so:

<!DOCTYPE html>
<html>
   <body>
      <script context="text/javascript">
         var theOtherWorld;
         function findTheOtherWorld(world) {
            theOtherWorld = world;
         };
         function reachIntoTheOtherWorld() {
            theOtherWorld.postMessage("letmein", "http://bar");
         };
      </script>
      <p>This page holds an iframe at: "foo"</p>
      <iframe name="eye" src="http://bar/index.html"
            onload="findTheOtherWorld(window.eye);">
      </iframe>
      <div>
         <button onClick="reachIntoTheOtherWorld();">reach into the iFrame</button>
      </div>
   </body>
</html>

 
 

The web page which sits inside of the iFrame ends up looking like this:

<!DOCTYPE html>
<html>
   <body>   
      <script context="text/javascript">
         var cat = {
            name: "Patches",
            color: "Calico",
            about: function() {
               return this.name + " is " + this.color;
            }
         };
         function receiveMessage(event) {
            if (event.data == "letmein") {
               if (event.origin == "http://foo") {
                  alert(cat.about());
               }
            }
         };
         window.addEventListener("message", receiveMessage, false);
      </script>
      <p>This page sits inside an iframe at: "bar"</p>
   </body>
</html>

 
 

Hmmm... now we are able to get back info about Patches, our calico cat, from the page within the iFrame, but we are deferring to it to inform us instead of throwing the alert in the page actually holding the iFrame as we had before. If we are going to throw an alert with the info about Patches from the first web page we will need yet another refactoring. How may we round trip data from first outside of the iFrame, then into it, and finally back again? That looks like this:

<!DOCTYPE html>
<html>
   <body>
      <script context="text/javascript">
         var theOtherWorld;
         function findTheOtherWorld(world) {
            theOtherWorld = world;
         };
         function reachIntoTheOtherWorld() {
            theOtherWorld.postMessage("letmein", "http://bar");
         };
         function receiveMessage(event) {
            if (event.origin == "http://bar") {
               alert(event.data);
            }
         };
         window.addEventListener("message", receiveMessage, false);
      </script>
      <p>This page holds an iframe at: "foo"</p>
      <iframe name="eye" src="http://bar/index.html"
            onload="findTheOtherWorld(window.eye);">
      </iframe>
      <div>
         <button onClick="reachIntoTheOtherWorld();">reach into the iFrame</button>
      </div>
   </body>
</html>

 
 

...and it also looks like what is below. I may have been able to use event.source below instead of top. event.source was suggested in the blog posting at the link I offer above, but top also worked for me. Whatever.

<!DOCTYPE html>
<html>
   <body>   
      <script context="text/javascript">
         var cat = {
            name: "Patches",
            color: "Calico",
            about: function() {
               return this.name + " is " + this.color;
            }
         };
         function receiveMessage(event) {
            if (event.data == "letmein") {
               if (event.origin == "http://foo") {
                  top.postMessage(cat.about(), "http://foo");
               }
            }
         };
         window.addEventListener("message", receiveMessage, false);
      </script>
      <p>This page sits inside an iframe at: "bar"</p>
   </body>
</html>

 
 

You will notice that I do not just hand back the cat variable. If I did that I would get this error in the console:

Uncaught DataCloneError: Failed to execute 'postMessage' on 'Window': An object could not be cloned.

 
 

The biggest time hole that I fought today in getting this stuff working lay in this error. It stems from the fact that there is a method on cat. If cat only had name and color and not about then it might be transfered as a JSON object to become event.data at the page holding the iFrame, but then, naturally, we would have to create our "Patches is Calico" message another way. There is no way to bring the about method across the wire. I noticed today that JSON.stringify just drops methods off of objects that it serializes to strings. I guess JavaScript methods just do not travel through the eyes of needles very well all and all.

No comments:

Post a Comment