heyloura.com A basic framed* website... more experiments with disappearing iframes

Published

A basic framed* website… more experiments with disappearing iframes

Want to follow along? You are going to need some basic familiarity with JavaScript, html, css, you will need a code editor and have the files served from a webserver. Due to iframe restrictions, this won’t work locally.

From my An experimental take on the retro website frames… disappearing iframes with vanilla-js, html, and css post we know have a way of using html pages as page fragments using iframes and then making those iframes disappear. Let’s tackle a small proof of concept website using this method. We want the website to have the navigation links and footer to load in from iframes.

So let’s start with a navigation fragment. Since my website is using the small classless css library Simple.css we’re going to structure the main navigation like they intended. Which is basically a nav tag in a header tag. Simple enough. Let’s create _nav.html and use our starting fragment template. Don’t know what’s needed in the fragmentRedirect.js file? Check out the previous post or code.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>

</body>
</html>

Next we’re going to add in the navigation and the links to the pages of the website.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/cats.html">Cats</a>
    <a href="/fish.html">Fish</a>
    <a href="/chickens.html">Chickens</a>
  </nav>
</body>
</html>

The next step is to load our navigation into our home page so let’s create the index.html file for our website including the disappearingFrame.js script we made previously. Note the aria-live and aria-busy tags I used to mark for screen-readers where the content will be changing. This time I used aria-live="polite" to notify the user, since navigational elements are pretty important.

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A framed* website</title>
  <link rel="stylesheet" href="./style.css" />
  <script src="disappearingFrame.js" type="text/javascript"></script>
</head>
<body>
  <main>
    <header aria-live="polite">
      <h1>A framed<sup>*</sup> website</h1>
      <iframe aria-busy="true" 
        src="/_navigation.html"
        title="main navigation links"
        onload="disappear(this)"></iframe>
    </header>
  </main>
</body>
</html>

This is what have so far, but if you’re following along with your own code you might have noticed something as it loaded. A flash of white and the size of the header shrinking and expanding as we showed the iframe and then removed it after adding in the elements. Let’s fix that.

a screenshot of a website featuring my favorite pets. It is empty except for the main navigation elements and the title

Luckily the white flash has been solved before and StackOverflow was quick to point me in the right direction. It actually does something similar to what we did with iframes. It creates a script tag to hide the iframe and then when everything is loaded it re-shows the iframe. Why do that in JavaScript and not use css? Because if a user has JavaScript disabled the iframe will remain visible, which is what we want. We also don’t need to make the iframes visible at the end, because we remove them when we are done copying the content. Let’s add that to our disappearingFrame.js file. Notice it’s using the IIFE pattern. Here’s what the script file looks like now:

// Hide the white flash of an iframe loading 
(function () {
    let div = document.createElement('div');
    let ref = document.getElementsByTagName('base')[0] || 
              document.getElementsByTagName('script')[0];

    div.innerHTML = '<style> iframe { visibility: hidden; } </style>';
    ref.parentNode.insertBefore(div, ref);
})();  

// Load an iframes content into the DOM
function disappear(frame) {
  let body = (frame.contentDocument.body || frame.contentDocument);
  let children = [...body.children];
  for(let child of children) {
    frame.before(child);
  }
  frame.remove();
}

That got rid of the flash… but what about the height issue? Since the height is variable based on what you’re inserting the best way to deal with it is to set the height of the iframe to the height of the content or the parent container with some css. Here I set the height of the header with a style tag. I added a little dummy content and this is what we have for index.html.

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A framed* website</title>
  <link rel="stylesheet" href="./style.css" />
  <script src="disappearingFrame.js" type="text/javascript"></script>
  <style> body > header { height: 229px } </style>
</head>
<body>
  <header aria-live="polite">
    <h2>A framed<sup>*</sup> website</h2>
    <iframe aria-busy="true" 
      src="/_navigation.html"
      title="main navigation links"
      onload="disappear(this)"></iframe>
  </header>
  <main>
    <header> 
      <h1>My favorite pets</h1>
      <p>This website is dedicated to some of the wonderful pets
      I've had over the years.</p>
    </header>
  </main>
</body>
</html>

Next we’ll add the footer, which is also a fragment. This one is easy.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>
  <p>Built with πŸ’– by heyloura</p>
</body>
</html>

So the final index.html file is this

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A framed* website</title>
  <link rel="stylesheet" href="./style.css" />
  <script src="disappearingFrame.js" type="text/javascript"></script>
  <style> body > header { height: 229px } </style>
</head>
<body>
  <header aria-live="polite">
    <h2>A framed<sup>*</sup> website</h2>
    <iframe aria-busy="true" 
      src="/_navigation.html"
      title="main navigation links"
      onload="disappear(this)"></iframe>
  </header>
  <main>
    <header> 
      <h1>My favorite pets</h1>
      <p>This website is dedicated to some of the wonderful pets
      I've had over the years.</p>
      <figure>
        <img alt="Orange tabby cat in the center of a lit Christmas tree" 
             src="/photos/christmas_cat.jpg" />
        <figcaption>
	        This is the cat who tried his best to knock down my Christmas tree.
        </figcaption>
      </figure>
    </header>
  </main>
  <footer aria-live="off">
    <iframe aria-busy="true" 
      src="/_footer.html"
      title="the footer credits of the website"
      onload="disappear(this)"></iframe>
  </footer>
</body>
</html>

Which results in this:

Screenshot of the website we are building with navigation, footer, and a cat stuck in a Christmas tree for content.

Update website navigation to highlight the current page.

We should probably indicate what page the user is on. But how can we do that if the navigation is in an iframe? Good question! Turns out you can, assuming you can use a little JavaScript, but we’ll need to adjust a few things first.

Let’s test if we can pull a script tag and have it run when the content is pulled in from the iframe. Let’s adjust the _navigation.html file and put a script at the bottom. Also let’s make sure we don’t run it in the iframe, only outside of it by checking if window.top === window.self that way we can test if it works.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/cats.html">Cats</a>
    <a href="/fish.html">Fish</a>
    <a href="/chickens.html">Chickens</a>
  </nav>
  <script>
    <!-- Don't run code when in the iframe -->
    if(window.top === window.self) {
      alert('Hello World!');
    }
  </script>
</body>
</html>

Uh oh, that didn’t work. Turns out just inserting a script tag doesn’t work because of security concerns. Maybe that’s something the parent page can take care of when it’s pulling in the elements? Let’s give it a shot.

What we need to do is detect if the element we are going to insert is a script tag. Luckily we have a property that tells us that, element.tagName and then we can do the trick we used to suppress the white flash of an iframe loading.

  1. Create a script element.
  2. Take the contents from the iframes script tag and append it to that script.
  3. Append it to the head of the document, which causes the script to run.
  4. Then we can remove it (or not).

So here are the change to the disappearingFrame.js file:

// Hide the white flash of an iframe loading
(function () {
    let div = document.createElement('div');
    let ref = document.getElementsByTagName('base')[0] || 
              document.getElementsByTagName('script')[0];

    div.innerHTML = '<style> iframe { visibility: hidden; } </style>';
    ref.parentNode.insertBefore(div, ref);
})();  

// Load an iframes content into the DOM
function disappear(frame) {
  let body = (frame.contentDocument.body || frame.contentDocument);
  let children = [...body.children];
  for(let child of children) {
    if(child.tagName === 'SCRIPT'){
      let script = document.createElement("script");
      script.text = child.innerHTML;
      document.head.appendChild( script ).parentNode.removeChild( script );
    } else {
      frame.before(child);
    }
  }
  frame.remove();
}

Sweet! We got the alert that time. This means we can include scripts in our fragments and then execute them. This opens up possibilities I’ll explore in the next post.

So to mark our page as the current page in the navigation we can piggyback on the accessibility tag to mark the current page aria-current="page" attribute. Plus our stylesheet already styles anchor elements that have that attribute. But now we have a choice add the attribute during the iframe load or once it’s in our parent page. To keep things simple and scoped, I’m going to set the attribute from within the iframe. We’ll just need to swap around the condition of the iframe. Note: when you pull in the script tag to the parent document, the document is the parent. So you’ll need to be careful with using selectors.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/cats.html">Cats</a>
    <a href="/fish.html">Fish</a>
    <a href="/chickens.html">Chickens</a>
  </nav>
  <script>
    // check that we are in the iframe.
    if(window.top != window.self) {
      let url = document.referrer.substring(document.referrer.lastIndexOf('/') + 1);
      let navs = document.querySelectorAll('a');
      for (let nav of navs) {
        if(nav.getAttribute('href') == `/${url}` || 
             (nav.getAttribute('href') == '/' && url == 'index.html')) {
          nav.setAttribute('aria-current','page')
        }
      }
    }
  </script>
</body>
</html>

What we are doing, is that if we are in the iframe, grab the anchor tags and loop through them. Check to see if the end part of the url matches the anchor tag href. We can get the url string from the document.referrer but only if the iframe source is on the same primary domain as the parent page. You can click between the pages to see the highlighted navigation anchor change. Pretty sweet!

Don’t forget about the no-JS crowd

Now that we got something built, let’s see how it does if we turn JavaScript off. Everything looks fine, though the iframe pieces are not styled the same. This can be taken care of by adding a bit of styling to the head section of the iframe and in the parent setting an appropriate width and height. But what happens if you click to change a page in the navigation? Disaster!

A screenshot of the website we are building showing the nested navigation if you click when JavaScript is turned off.

Turns out, the solution to this is pretty easy. Remember how we’re checking if the iframe is the top window in our JavaScript? You can tell anchor tags to open in the top window, by giving them the right target target="_top" so let’s make those changes.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex, nofollow" />
  <script src="fragmentRedirect.js" type="text/javascript"></script>
</head>
<body>
  <nav>
    <!-- For navigation elements make sure you add target="_top" to the anchors -->
    <a href="/" target="_top">Home</a>
    <a href="/cats.html" target="_top">Cats</a>
    <a href="/fish.html" target="_top">Fish</a>
    <a href="/chickens.html" target="_top">Chickens</a>
  </nav>
  <script>
    if(window.top != window.self) {
      let url = document.referrer.substring(document.referrer.lastIndexOf('/') + 1);
      let navs = document.querySelectorAll('a');
      for (let nav of navs) {
        if(nav.getAttribute('href') == `/${url}` || 
             (nav.getAttribute('href') == '/' && url == 'index.html')) {
          nav.setAttribute('aria-current','page')
        }
      }
    }
  </script>
</body>
</html>

And done!

TL;DR

Using our disappearing iframe trick we can build a basic website where the navigation and footer are in separate html pages. I’ve also shown how you can pull in Javascript from these iframes and execute it on the parent page. You can check out the result here: code - preview.

Next Steps

You can use this technique to build a basic website and split frequently repeated portions into their own fragments. Things like navigation, footers, page headers or other repeated content. The method is pretty simple and removes some of the major drawbacks the retro frame system people used in the early web. But it’s also a little more powerful than that, we now have a system that we can create a “component” with… styles, html, and css all in one place. No need for a component framework, or the overhead, of something like Svelte, Vue or React and no need for anything server-side either. To challenge how far this can hold up I’m going to build a basic productivity app with this method. So stay tuned.

  1. Let’s code something experimental… an introduction
  2. An experimental take on the retro website frames… disappearing iframes with vanilla-js, html, and css
  3. A basic framed website… more experiments with disappearing iframes (you are here)
  4. Coding a framed* productivity app - building out the calendar

Conversation

Don't feel like chatting? How about a simple