Identifying the current script element

[2014-05-04] guest, rodney, dev, javascript, clientjs
(Ad, please don’t block)

Guest blog post by Rodney Rehm

You may find yourself in a situation requiring you to know the DOM context of the JavaScript that is currently executing. The DOM context of a running JavaScript is the <script> element that caused the JavaScript to be executed in the first place. With HTML5 the WhatWG introduced document.currentScript, which this article will use to explain simple techniques to make use of DOM context.

What do I need the currentScript context for?  

The script you’re running may be in need of parameterization. It requires some sort of configuration, some data so it can do its job properly. Developers who’ve used RequireJS haven seen this before:

<script src="scripts/require.js" data-main="js/main"></script>

That’s a shorthand way of doing the following in JavaScript:

<script src="scripts/require.js"></script>
<script>
  require.config({
    baseUrl: 'js/',
    deps: ['main']
  });
</script>

We can see here that first the script scripts/require.js is downloaded and evaluated, then it is initialized with some basic configuration.

The first example pulled some configuration data out of the attribute data-main of the <script> element the JavaScript was loaded in. It used the <script> element to configure and initialize itself. No other script was required to achieve this. Another example where DOM context can come in handy is when you need to know the containing DOM element:

<div class="container-for-some-magic">
  <script src="magic.js"></script>
</div>

The script magic.js needs to know its parent element in order to initialize itself. Again, we could have achieved the same thing with the following, more verbose JavaScript:

<div class="container-for-some-magic"></div>
<script src="magic.js"></script>
<script>
  // find all .container-for-some-magic known to this point
  var elements = document.getElementsByClassName(
    'container-for-some-magic');
  // use the last element
  var targetElement = elements[elements.length - 1];
  // initialize magic.js
  magic.initialize(targetElement);
</script>

We can take things further by using the textContent of a remote <script> resource for configuration, in case a data-attribute is insufficient:

<script src="use-my-content.js">/*{
  "i am treated": "as text, you'll",
  "have to": "JSON.parse() me"
}*/</script>

If you think that the above is an error, you’re probably remembering the old rule to never put code inside an external script. After all, MDN says that »script elements with a src attribute specified should not have a script embedded within its tags« – but what exactly is "not a script"?

HTML5 mandates the text content of a <script> element that has a src attribute to either be empty – which is what we’re used to – or contain only script documentation. In other words, if you put something in there, make sure it’s a valid script comment. So you either have to make sure all lines are preceded by // to make them line-comments, or you wrap the content in /* and */ to make the entire content a block-comment. Et voila, we have content that is "not a script". In reality no browser (Blink, Gecko, Trident, WebKit) cares about this rule – at least not in a way that would prevent you from reading the script element’s textContent. Of course you also need to make sure that the character sequences <script> and </script> don’t show up in your content, as the general [restrictions for contents of script elements](script content restrictions) still applies.

Summing things up, DOM context can be a powerful tool for configuring JavaScript.

Obtaining the currentScript DOM context?  

The WhatWG HTML5 Specification defines the property document.currentScript. It contains a reference to the currently executed script. The good news is that Gecko (Firefox) and Blink (Chrome, Opera) support this property already. The bad news is, WebKit (Safari, Safari iOS, Android Stock Browser) and Trident (Internet Explorer) don’t yet know document.currentScript.

At first glance it looks as if document.scripts could fill the gap. And it does, unless you’re planning on working with <script> inside an <svg>. While Internet Explorer lists <script> within inline <svg> in document.scripts, every other browser does not. document.getElementsByTagName() does not show that limitation and returns <script> within <svg> just fine.

We can reliably access the current script’s DOM element with the following snippet:

var currentScript = document.currentScript || (function() {
  var scripts = document.getElementsByTagName('script');
  return scripts[scripts.length - 1];
})();

There are two situations where this snippet will fail, though. When a script is loaded asynchronously using <script async>, the order of script execution is uncertain. Therefore the document.getElementsByTagName() trick will not work, as it relies on the currently executed script being the last of that collection. The same holds true for <script>s that are not appended to the end of the document. In the future this might become less of a problem, as document.currentScript properly resolves async scripts.

With <script defer> we don’t see this limitation. While async scripts can be executed whenever they become ready, deferred scripts are collected and executed right after the browser finished parsing the document. That’s why it can keep track of the <script> element reference even through document.getElementsByTagName().

If a script is made selectable, by adding an id attribute, a certain class or, say, a data attribute, one could query the DOM with the appropriate selector instead. That’s the route RequireJS 2.1.11 currentScript detection went. This will work fine in any situation, except for when the given selector is used multiple times.

Conclusion  

Knowing which <script> the currently executed JavaScript belongs to can help simplify configuration. Modern browsers support document.currentScript, others can be helped along with document.getElementsByTagName() – but watch out for <svg> and <script async>.

Thanks go to @rachelnabors and @derSchepp for reviewing this article.


Rodney Rehm is a web developer, enjoying to create tools that not just work, but work well. He currently works for Deutsche Telekom on JavaScript applications of their Connected Home Platform Qivicon. Rod started out working the full web stack as a freelancer over a decade ago. In recent years he focused on the front end, particularly on JavaScript. With utilities like URI.js, articles like Designing Better JavaScript APIs and testing CSS Transitions he’s trying to make the web a better place.