For a recent project, we needed to the ability to allow users to embed portions of the site in their own websites and blogs by copy-pasting a little bit of code. This blog post explains how we arrived at a solution for this.

Warning! This blog post gets pretty techy and is intended for other nerds who may find this technique useful in their own work.

More detailed requirements

  • As we don’t know what browsers our users’ sites are targetting, we want embedding to work on a range of browsers, including older versions of Internet Explorer (we aimed to support IE6+).
  • The code snippet should have no prerequisites (other than JavaScript being enabled) and be relatively short.
  • In case our service is being temporarily slow to respond for any reason, we don’t want to block the rest of the target page from loading.
  • We shouldn’t interfere with any other events on the target page.
  • We can’t rely on any external JavaScript libraries, and we shouldn’t pollute the global namespace of the target page.

Our embed script

Here’s an example of the code snippet for embedding a widget from the DCLG’s Impact Indicator Dashboard.

<script type="text/javascript" id="dclg-impact-indicators-embedder-5b8cf3a4-b0f0-13e9-3737-d5a159268ae6" class="dclg-impact-indicators-async-script-loader">
  (function() {
    function async_load(){
      var s = document.createElement('script');
      s.type = 'text/javascript';
      s.async = true;
      var theUrl = 'http://opendatacommunities.org/impact_indicators/indicators/housing-starts/dashboard_widget';
      s.src = theUrl + ( theUrl.indexOf("?") >= 0 ? "&" : "?") + 'ref=' + encodeURIComponent(window.location.href);
      var embedder = document.getElementById('dclg-impact-indicators-embedder-5b8cf3a4-b0f0-13e9-3737-d5a159268ae6');
      embedder.parentNode.insertBefore(s, embedder);
    }
    if (window.attachEvent)
      window.attachEvent('onload', async_load);
    else
      window.addEventListener('load', async_load, false);
  })();
</script>

Let’s break down what’s going on here…

A simple solution for modern browsers: HTML5 async attribute.

Much of the complexity above is there to support older browsers. If we only wanted to support modern browsers, then we could have achieved most of the desired functionality in one line (I’ll get onto what the remote script actually does later on).

<script async src="http://opendatacommunities.org/impact_indicators/indicators/housing-starts/dashboard_widget"></script>

The key thing to note here is the use of the async attribute. This tells the browser to execute the script asynchronously. The problem is, though, the async attribute is not supported on old browsers.

Without support for the async attribute, any content after the script tag is blocked from loading until its src has loaded and executed. This isn’t very polite at the best of times, and if our service was being slow or temporarily down, then it would be very annoying for users of the target page.

Creating a script tag with JavaScript.

To get around lack of support for async, our snippet creates a script tag with JavaScript and appends it to the document immediately before our snippet’s script tag. Note: We’ve used a guid in the id of the script tag to make sure we find the right one, if multiple widgets have been embedded.

<script type="text/javascript" id="dclg-impact-indicators-embedder-5b8cf3a4-b0f0-13e9-3737-d5a159268ae6" class="dclg-impact-indicators-async-script-loader">
  var s = document.createElement('script');
  s.type = 'text/javascript';
  s.async = true;
  s.src = 'http://opendatacommunities.org/impact_indicators/indicators/housing-starts/dashboard_widget';
  var embedder = document.getElementById('dclg-impact-indicators-embedder-5b8cf3a4-b0f0-13e9-3737-d5a159268ae6');
  embedder.parentNode.insertBefore(s, embedder);
</script>

We’re getting there, but the code above would block onload from triggering on some browsers until the script has been loaded and run, which isn’t really acceptable if we’re aiming to be a polite little widget.

Waiting until after onload

To prevent us from blocking onload, we wrap everything in a function, and call that once the page has finished loading (in a cross-browser compatible way). Note that we don’t just use window.onload to avoid stomping on any existing onload events on the target page.

Wrap the code in a closure, and add a referer parameter onto the url (so we can track who’s embedding our widgets), and we have our final solution. Scroll back up for a reminder of what it looks like.

The remote script

So far we’ve talked a lot about how to call a remote script without blocking the target page from loading, but what does the remote script actually do?…

In our case, the src url points to a Rails application route which responds with some JavaScript. The response creates a div in the target page, and injects our content into it. We also add a style tag into the head of the target page so that we can style our widgets.

Here’s a (slightly simplified) example of the response for a widget:

(function (global) {
  // add array index of for old browsers (IE<9)
  if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj, start) {
      var i, j;
      i = start || 0;
      j = this.length;
      while (i < j) {
        if (this[i] === obj) {
          return i;
        }
        i++;
      }
      return -1;
    };
  }

  // make a global object to store stuff in
  if(!global.OpenDataCommunities) { global.OpenDataCommunities = {}; };
  var OpenDataCommunities = global.OpenDataCommunities;

  // To keep track of which embeds we have already processed
  if(!OpenDataCommunities.processedScripts) { OpenDataCommunities.processedScripts = []; };
  var processedScripts = OpenDataCommunities.processedScripts;

  if(!OpenDataCommunities.styleTags) { OpenDataCommunities.styleTags = []; };
  var styleTags = OpenDataCommunities.styleTags;

  var scriptTags = document.getElementsByTagName('script');
  var thisRequestUrl = '<%= raw(request.url) %>';

  for(var i = 0; i < scriptTags.length; i++) {
    var scriptTag = scriptTags[i];

    // src matches the url of this request, and not processed it yet.
    if (scriptTag.src == thisRequestUrl && processedScripts.indexOf(scriptTag) < 0) {

      processedScripts.push(scriptTag);

      // add the style tag into the head (once only)
      if(styleTags.length == 0) {
        // add a style tag to the head
        var styleTag = document.createElement("link");
        styleTag.rel = "stylesheet";
        styleTag.type = "text/css";
        styleTag.href =  "http://opendatacommunities.org/assets/impact_indicators_embed.css";
        styleTag.media = "all";
        document.getElementsByTagName('head')[0].appendChild(styleTag);
        styleTags.push(styleTag);
      }

      // Create a div
      var div = document.createElement('div');
      div.id = '<%= div_id %>';

      // add the cleanslate classs for extreme-CSS reset.
      div.className = 'dclg-impact-indicators-embeddable .dashboard_widget cleanslate';

      scriptTag.parentNode.insertBefore(div, scriptTag);

      div.innerHTML = '<%= j(render(partial_name)) %>';

    }
  }
})(this);

Again, let’s break this down a bit…

Support for Array.indexOf

First, to make our lives easier later on, we add an indexOf function to Array for browsers that don’t already have it:

// add array index of for old browsers (IE<9)
if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function(obj, start) {
    var i, j;
    i = start || 0;
    j = this.length;
    while (i < j) {
      if (this[i] === obj) {
        return i;
      }
      i++;
    }
    return -1;
  };
}

Namespaced objects for tracking progress

Next, we make some objects (under an OpenDataCommunities namespace in our case) to track:

  • Which embed scripts have already been processed (in case there are multiple embeds on the target page)
  • How many style tags have been injected into the head.

Find all our embed scripts on the target page, and loop over them.

For the scripts we haven’t processed yet, and where the request url matches the src of the script tag, we’ll do our magic. (Again, this caters for when people have embedded multiple widgets).

var scriptTags = document.getElementsByTagName('script');
var thisRequestUrl = '<%= raw(request.url) %>';

for(var i = 0; i < scriptTags.length; i++) {
  var scriptTag = scriptTags[i];

  // src matches the url of this request, and not processed it yet.
  if (scriptTag.src == thisRequestUrl && processedScripts.indexOf(scriptTag) < 0) {

    processedScripts.push(scriptTag);
    // do our magic here!
  }
}

Note: for those not familiar with Rails, the content inside <% %> tags are little snippets of embedded ruby (ERb) that are run inline before the response is returned from the server.

Add a style tag into the head of the target page.

Nothing much to say here, except that in our case, we included the cleanslate extreme CSS reset in our CSS, so that we can be sure that no existing styles in the target page bleed into our widget.

// add the style tag into the head (once only)
if(styleTags.length == 0) {
  // add a style tag to the head
  var styleTag = document.createElement("link");
  styleTag.rel = "stylesheet";
  styleTag.type = "text/css";
  styleTag.href =  "http://opendatacommunities.org/assets/impact_indicators_embed.css";
  styleTag.media = "all";
  document.getElementsByTagName('head')[0].appendChild(styleTag);
  styleTags.push(styleTag);
}

Create the div and inject its content

// Create a div
var div = document.createElement('div');
div.id = '<%= div_id %>';

// Add the cleanslate class for extreme-CSS reset.
div.className = 'dclg-impact-indicators-embeddable .dashboard_widget cleanslate';

scriptTag.parentNode.insertBefore(div, scriptTag);

div.innerHTML = '<%= j(render(partial_name)) %>';

We use Erb again here to allow us to provide a unique div id, and render the contents of the widget into the innerHTML of that div.


Resources

I found thse pages useful while figuring this stuff out:

Keep up to date with our news by signing up to our newsletter.
Thanks for reading all the way to the end!
We'd love it if you shared this article.