Native Javascript Ninjutsu: DOMinating iframes

Ninja kanji

Sensei Says (dark ages, to dojos, to disciplines)

Javascript used to be a dark and ancient art, looked down upon by many web developers as a dishonorable – even malicious – ‘copy and paste’ language. Macromedia’s Shockwave – which later became Macromedia Flash, which even later became Adobe Flash – pushed audio, video, and interactive motion graphics onto the web in a cross-browser compatible format that all but decimated the need and appeal for Javascript. What little Javascript community there was began to seriously dwindle and die out.

And then the frameworks came to rise: Dojo, Yahoo! UI Library, Google Web Toolkit, jQuery, Prototype, MooTools, and many more. With these powerful armies by its side, the Javascript community quickly grew and regained its honor, competing heavily with the fluid animation and complex, real-time interactivity that Flash had delivered for years.

Javascript now seems to be a strong, healthy, and widely accepted language, frequently used and relied upon by web developers across the land. Yet how many of today’s programmers can write pure native Javascript without the aid of a framework? How many can perform AJAX requests without a framework? And most importantly, how many can craft fully cross-browser compatible code without a framework? In order to not become dependent on the frameworks – and thus risk sliding backwards into the dark ages – we must maintain a wide variety of practices: these are the native Javascript disciplines.

Discipline 6: Bajutsu (horsemanship)

I typically don’t front flip at the opportunity to use iframes in my native JS Ninjutsu practices, though I frequently find myself in the midst of a mission where I have no choice but to work with an iframe. While I may not need to approve of them, I certainly must make sure I know how to properly work with them in such cases. First and foremost, one must understand exactly what an iframe is, and I think it’s best to quote Wikipedia’s iframe article:

“An inline frame places another HTML document in a frame. Unlike an object element, an inline frame can be the [target] frame for links defined by other elements and it can be selected by the user agent as the focus for printing, viewing its source etc. The content of the element is used as alternative text to be displayed if the browser does not support iframes. First introduced by [MSIE] in 1997, standardised in HTML 4.0 Transitional, allowed in HTML 5.”

Now that we understand what an iframe is, let’s learn how to add one to the HTML DOM:

[js]
document.write( ‘</pre>
<iframe width="320" height="240"></iframe>
<pre>’ );
[/js]

That is the quickest and simplest way to get an iframe written to the DOM, though of course, if you want to be able to manipulate it further with JS it would make sense to give it an id:

[js]
document.write( ‘</pre>
<iframe id="the_iframe_id" width="320" height="240"></iframe>
<pre>’ );
[/js]

Alternatively you can give the iframe a name value and access it using the window.frames array (which I will demonstrate later in the article). The advantage to this approach is that it seems to be more backward compatible with older (legacy) web browsers:

[js]
document.write( ‘</pre>
<iframe name="the_iframe_name" width="320" height="240"></iframe>
<pre>’ );
[/js]

Despite it’s one-line appeal, the document.write method has a major downside: the JS itself must be located in the exact place in the DOM that you want the iframe to be inserted. For this reason I typically use the document.createElement and parentNode.insertBefore approach:

[js]
var the_iframe = document.createElement( ‘iframe’ );
the_iframe.id = ‘the_iframe_id’;

var another_element = document.getElementById( ‘another_element_id’ );
another_element.parentNode.insertBefore( the_iframe, another_elment.nextSibling );
[/js]

The above code will create the iframe, give it an id value, locate another specific element on the page (which you want to insert the iframe after), and then add the new iframe to the DOM at the given location.

Adding the iframe won’t do much though, as we need to give it a src value before any content will appear inside of it:

[js]
var the_iframe = document.createElement( ‘iframe’ );
the_iframe.id = the_’iframe_id’;
the_iframe.src = ‘http://domain.com/directory/filename.html’;

var another_element = document.getElementById( ‘another_element_id’ );
another_element.parentNode.insertBefore( the_iframe, another_elment.nextSibling );
[/js]

Changing the content of the iframe is as easy as changing the value of the src attribute:

[js]
document.getElementById( ‘the_iframe_id’ ).src = ‘http://new-domain.com/new-directory/new-filename.html’;
[/js]

As I mentioned above, you may use the legacy approach that relies on the iframe having a name value:

[js]
window.frames[ 'the_iframe_name' ].location = ‘http://new-domain.com/new-directory/new-filename.html’;
[/js]

Although it’s nice to understand legacy and alternative approaches, this one in particular does not seem to be all that useful. In my cross-browser tests the document.getElementById().src method works in all eighteen (18) web browsers that I routinely check (IE9, IE8, IE7, IE6, FF4, FF3.6, FF3.5, FF3, FF2, GC7, GC6, GC5, GC4, Safari 5, Safari 4, Safari 3, Opera 10, and Opera 9). In my opinion this would mean that it’s a cross-browser compatible solution that is safe to use for all purposes.

Now that we’ve gone over some of the basics about using iframes it makes sense to discuss the relationship between the iframe and its parent page. When both your pages come from the same domain you can do a lot of simple communication between the iframe and the parent page. There are three different ways you can reference the iframe‘s content:

  1. using the document.getElementById( 'the_iframe_id' ).contentDocument method,
  2. using the document.getElementById( 'the_iframe_id' ).contentWindow.document method,
  3. or using the window.frames[ 'the_iframe_name' ].document method.

I’ve done some extensive cross-browser testing and it seems that out of my usual eighteen (18) web browsers (listed above) the only two that don’t support the contentDocument method are IE6 and IE7. For this reason I would to one of the following:

  • Stick to using only contentWindow

[js]
var the_iframe = document.getElementById( ‘the_iframe_id’ );
var some_element = the_iframe.contentWindow.document.getElementById( ‘some_element_id’ );
[/js]

  • or check to see if contentDocument is available before using it)

[js]
var the_iframe = document.getElementById( ‘the_iframe_id’ );

if ( the_iframe.contentDocument )
{
var some_element = the_iframe.contentDocument.getElementById( ‘some_element_id’ );
}
else if ( the_iframe.contentWindow )
{
var some_element = the_iframe.contentWindow.document.getElementById( ‘some_element_id’ );
}
[/js]

There is one more alternative method to both of the above, and that is to rely on the name attribute of the iframe and access its elements using the window.frames array:

[js]
var the_iframe = window.frames[ 'the_iframe_name' ];
var some_element = the_iframe.document.getElementById( ‘some_element_id’ );
[/js]

This last method is (again) more suitable for legacy web browsers, though it does appear to work in all the modern web browsers that I typically test for cross-browser compatibility. Let’s put this all together in the following example which shows you how to communicate down (from the parent page to the iframe):

[html]

</pre>
<iframe id="the_iframe_id" src="the_iframe_content.html" width="320" height="240"></iframe>
<pre>

<script type="text/javascript">// <![CDATA[
function removeSomeElement()
{
var the_iframe = document.getElementById( 'the_iframe_id' );

if ( the_iframe.contentDocument )
{
var some_element = the_iframe.contentDocument.getElementById( 'some_element_id' );
}
else if ( the_iframe.contentWindow )
{
var some_element = the_iframe.contentWindow.document.getElementById( 'some_element_id' );
}

some_element.parentNode.removeChild( some_element );
};

// ]]></script>

[/html]

And here is the source code of the_iframe_content.html:

[html]

This element should not be visible

This element should be visible

[/html]

As you can see above I use the onload attribute/event to make sure the iframe has fully loaded it’s source before allowing the removeSomeElement() function to be executed. If I didn’t do this it’s very likely that the_iframe variable would end up being set as null or undefined and then the following code wouldn’t be able to properly locate the target element.

This whole process is referred to as ‘referencing down’ (or ‘communicating down’) because you are going from the parent page down into its child iframe. Similarly, ‘referencing up’ is when you reference the iframe‘s parent (and its DOM elements) by using the parent.document method:

[js]
var some_parent_element = parent.document.getElementById( ‘some_parent_element_id’ );
[/js]

Here’s how to add this method into our previous example:

[html]

This element should not be visible

This element should be visible

</pre>
<iframe id="the_iframe_id" src="the_iframe_content.html" width="320" height="240"></iframe>
<pre>

<script type="text/javascript">// <![CDATA[
function removeSomeElement()
{
var the_iframe = document.getElementById( 'the_iframe_id' );

if ( the_iframe.contentDocument )
{
var some_element = the_iframe.contentDocument.getElementById( 'some_element_id' );
}
else if ( the_iframe.contentWindow )
{
var some_element = the_iframe.contentWindow.document.getElementById( 'some_element_id' );
}

some_element.parentNode.removeChild( some_element );
};

// ]]></script>

[/html]

And here is the source code of the_iframe_content.html:

[html]

This element should not be visible

This element should be visible

<script type="text/javascript">// <![CDATA[
var the_parent_element = parent.document.getElementById( 'some_parent_element_id' );

the_parent_element.parentNode.removeChild( the_parent_element );

// ]]></script>

[/html]

If everything works right each targeted element should be removed from its respective parent page’s DOM. The cool part of this feature is that you can easily use the same methods to deal with nested iframes. You can reference an iframe element two levels down by doing the following:

[js]
var the_first_iframe = document.getElementById( ‘the_first_iframe_id’ );
var the_second_iframe = the_iframe.contentWindow.document.getElementById( ‘the_second_iframe_id’ );
var some_element = the_second_iframe.contentWindow.document.getElementById( ‘some_element_id’ );
[/js]

Similarly, to reference an element in the top level parent of an iframe you can do the following:

[js]
var the_top_parent_element = parent.parent.document.getElementById( ‘the_top_parent_element_id’ );
[/js]

In theory you should be able to repeat this infinitely, though I’m sure there are iframe limits imposed by web browsers as a basic safety measure (I have not needed to test this before but maybe I will as it sounds interesting enough).

So far so good, but as soon as you begin playing with cross-domain src values in your iframe you’ll notice that communication between the parent and the iframe becomes extremely restricted. If the parent page and the iframe src are from two separate domains then it will be impossible to directly access the DOM elements of one from the JS of the other. However there is a technique (more suitably referred to as a ‘hack’) that uses fragment identifiers to pass information cross-domain between an iframe and it’s parent. A fragment identifier is the part of the URL that follows the hash / anchor symbol (#). If you change (or add) this value it will not reload or refresh the page, but it does modify the window.location.hash value. The trick is to constantly measure the window.location.hash every second (or so) to see if the value has changed. This technique is called ‘polling’. Here’s a working example of the fragment (hash) identifier and polling techniques used to reference down in a cross-domain environment:

[html]

</pre>
<form id="the_form_id"><input id="the_input_id" type="text" value="black" />
<button id="the_submit_id" type="submit">reference down</button></form>
<pre>
</pre>
<iframe id="the_iframe_id" src="http://someotherdomain.com/the_iframe_content.html" width="320" height="240"></iframe>
<pre>

<script type="text/javascript">// <![CDATA[
function setupTheForm()
{
var the_iframe = document.getElementById( 'the_iframe_id' );
var the_form = document.getElementById( 'the_form_id' );
var the_input = document.getElementById( 'the_input_id' );
var the_src = 'http://someotherdomain.com/the_iframe_content.html#';

the_form.onsubmit = function ()
{
var the_value = the_input.value;
the_iframe.src = the_src + the_value;

return false;
}
};

// ]]></script>

[/html]

And here is the source code of the_iframe_content.html (which should be located on a separate domain):

[html]

<script type="text/javascript">// <![CDATA[
var poll;

function startPolling ()
{
var the_hash_value = window.location.hash.substr( 1 );
document.body.style.backgroundColor = the_hash_value;
poll = setTimeout( 'startPolling()', 1000 );
}

// ]]></script>

[/html]

Most of the above code should be pretty self-explanatory, but I’ll go into a bit of detail for those readers who are less familiar with native JS. First we apply an onload event to the body of our parent page and set this event to trigger the setupTheForm() function. This function stores the iframe element, the form element, the input element, and the iframe src value into their own variables. Notice that the_src variable includes the original iframe content URL with a hash symbol added to the end. This is necessary to make sure the fragment identifier is parsed correctly and also doesn’t refresh or reload the content within the iframe. Next we add a custom function to the onsubmit event of the form which stores the value of the input element, combines it with the_src variable, and uses the result to update the iframe src attribute. The function will also return false in order to prevent the default HTML form submit event from being triggered (in case there is one).

The iframe content also has an onload event that triggers the startPolling() function. This function grabs the fragment identifier by using the window.location.hash method. We also apply the substr() method to remove the first character of the hash string (which will always be the hash symbol). In this example we’re assuming that the user will always provide a value that is equal to one of the HTML colors (such as white, black, blue, green, yellow, orange, red, purple, etc) and we use this value to set the document.body.style.backgroundColor. Lastly, the function references the global poll variable, storing setTimeout() to it, which waits for the given time in milliseconds (the second parameter) before calling the provided function (the first parameter). Since this setTimeout always calls it’s parent function it will repeat the function infinitely, thus providing us with a working polling mechanism.

Now that you’ve seen how to reference down in a cross-domain environment, let’s complete the test by incorporating the code needed to reference up as well:

[html]

</pre>
<form id="the_form_id"><input id="the_input_id" type="text" value="black" />
<button id="the_submit_id" type="submit">reference down</button></form>
<pre>
</pre>
<iframe id="the_iframe_id" src="http://someotherdomain.com/the_iframe_content.html" width="320" height="240"></iframe>
<pre>

<script type="text/javascript">// <![CDATA[
var poll;

function startPolling ()
{
var the_hash_value = window.location.hash.substr( 1 );
document.body.style.backgroundColor = the_hash_value;
poll = setTimeout( 'startPolling()', 1000 );
}

function setupTheForm()
{
var the_iframe = document.getElementById( 'the_iframe_id' );
var the_form = document.getElementById( 'the_form_id' );
var the_input = document.getElementById( 'the_input_id' );
var the_src = 'http://someotherdomain.com/the_iframe_content.html#';

the_form.onsubmit = function ()
{
var the_value = the_input.value;
the_iframe.src = the_src + the_value;

return false;
}

startPolling();
};

// ]]></script>

[/html]

And here is the source code of the_iframe_content.html (which should be located on a separate domain):

[html]

</pre>
<form id="the_form_id"><input id="the_input_id" type="text" value="black" />
<button id="the_submit_id" type="submit">reference up</button></form>
<pre>
<script type="text/javascript">// <![CDATA[
var poll;

function startPolling ()
{
var the_hash_value = window.location.hash.substr( 1 );
document.body.style.backgroundColor = the_hash_value;
poll = setTimeout( 'startPolling()', 1000 );
}

function setupTheForm ()
{
var the_form = document.getElementById( 'the_form_id' );
var the_input = document.getElementById( 'the_input_id' );

the_form.onsubmit = function ()
{
var the_value = the_input.value;
parent.window.location.hash = the_value;

return false;
}
}

// ]]></script>

[/html]

For this example you’ll notice that the parent page content and the iframe content are almost identical. The main difference is that the iframe will not need to store values for the_iframe and the_src variables, and that when the form onsubmit event is triggered the JS will store the_value into the parent page’s fragment identifier by using the parent.window.location.hash method. While the first example (referencing down) was fully cross-browser compatible (at least concerning the eighteen web browsers I’ve mentioned earlier in the article), the second example appears to only work in Firefox. This has to do with the various browser limitations imposed on setting the parent.window.location.hash value. It appears that this line of code can be replaced with parent.location though you’ll have to add the whole URL with the hash value and this means the parent page could possibly be refreshed/reloaded (I did not do extensive testing to verify whether the parent page is actually reloaded or not in every browser). Also, it’s important to remember that you’ll be restricted by any size restraints which apply to the URL itself. For this reason the fragment identifier method is obviously limited to passing smaller bits of information between the parent page and its children.

This article is far from covering all the aspects of the iframe and how it works – especially in cross-domain environments – but I hope it has been a helpful tutorial, providing you with a way to use native JS to work with iframes. Stay tuned for next week’s discipline: Document object methods & properties.

Previous Disciplines
Discipline 1: AJAX with XHR
Discipline 2: Dynamic JS with PHP
Discipline 3: Include External JS
Discipline 4: Cookies and Variables
Discipline 5: removeNode vs. removeChild

Resources
Dynamic Web Coding’s iframe scripting tutorial
Software As She’s Developed’s Cross Domain iframe article
Tagneto’s Cross Domain Frame Communication with Fragment Identifiers
AJAXPatterns’ Cross-Domain Comms via IFrame Demo

Are you smart? Innovative? Driven? If you’re interested in working on challenging projects in one of the world’s most fast-paced industries, why not check out the openings on our Careers page?

Back to news overview