`document.currentScript` is more useful than I thought.
After discovering and being unsure of its value, I just realized how useful `document.currentScript` can be in exposing configuration properties to `<script>` elements (and other things too).
Every so often, I stumble across a well-established JavaScript API in the browser that I probably should've known about years ago. Examples include the window.screen property and the CSS.supports() method. To my relief, I've realized I'm not always alone in my ignorance. I remember posting about window.screen and getting a surprising amount of feedback from people who also didn't know of it. That made me feel a little less dumb.
I think the awareness of an API has less to do with how long it's been around, and more to do with its applicability to the problems we're trying to solve. If there aren't a ton of cases in which window.screen is useful, people will tend to forget it exists.
But occasionally, there's the surprise opportunity to use one of these not-so-well-known features. I think I found (at least) one for document.currentScript, and I intend to savor it for a while.
One glance at the API reveals what provides: a reference to the <script> element currently executing the code in which it's invoked.
<script>console.log("tag name:",document.currentScript.tagName);console.log("script element?",document.currentScriptinstanceofHTMLScriptElement);// tag name: SCRIPT// script element? true</script>
Since you get back the element itself, you're able to access properties like you would any other DOM node.
<scriptdata-external-key="123urmom"defer>console.log("external key:",document.currentScript.dataset.externalKey);if(document.currentScript.defer){console.log("script is deferred!");}</script>
// external key: 123urmom
// script is deferred!
Pretty straightforward. And what should be obvious by now, browser support is of zero concern. It's been in all major browsers for over a decade. That’s enough web years to form natural diamonds.
A noteworthy tidbit about document.currentScript is that it's not available within modules. But oddly enough, you won't get undefined for attempting to access it. Instead, it'll be set to null:
This is a deliberate decision noted in the specification. As soon as the document is constructed, currentScript is initialized with null:
The currentScript attribute, on getting, must return the value to which it was most recently set. When the Document is created, the currentScript must be initialized to null.
And since it's switched back to that initial value after the script synchronously executes, you'll also get null when executing asynchronous code:
In light of that, there doesn't seem to be any way of accessing the current script tag from within <script type="module" />. The best you can do is know whether the script is running in a module, and that's best accomplished by checking for null (again, only if isn't performed inside any asynchronous activity).
Don't bother checking for import.meta, by the way, even if it's within a try/catch. By simply existing in a classic <script> tag, the browser will throw a SyntaxError. It doesn't even need to run. It's thrown as the browser first parses the script's contents.
<script>// Will throw a `SyntaxError` without eveing being invoked!functionisInModule(){try{return!!import.meta;}catch(e){returnfalse;}};// Also throws error:console.log(typeofimport?.meta);</script>
Since modules still lack this sort of feature, it'll be interesting to see how resolving it shakes out. A note in the specification suggests it's an ongoing discussion:
This API has fallen out of favor in the implementer and standards community, as it globally exposes script or SVG script elements. As such, it is not available in newer contexts, such as when running module scripts or when running scripts in a shadow tree. We are looking into creating a new solution for identifying the running script in such contexts, which does not make it globally available: see issue #1013.
That issue, by the way, is a long-lived one, stretching back to 2016 with a whole bunch of people contributing. Until it lands, I suppose querying for the element is your best bet:
<scripttype="module"id="moduleScript">const scriptTag =document.getElementById("moduleScript");// Do stuff.</script>
I'm using a Stripe pricing table on PicPerf's website, embeddable by means of a native web component. Load a script, drop the element into your HTML, and set a couple of attributes:
That's all well & good if you can pull a couple environment variables when your HTML's being rendered, but I wanted to embed the table in a Markdown file. Markdown supports raw HTML just fine, but accessing those property values isn't as simple as reaching for import.meta.env or process.env. Instead, I'd need to dynamically inject the values independently from the markup rendered to page.
Unfortunately, it's not possible to separate rendering of the table from accessing & setting its configuration properties. The values must be available when the element is initialized.
That left me with injecting the entire element (property values & all) from within a client-side script. I'd target a placeholder element in my Markdown, and plop in the finished table markup from there.
At this point, the only thing I'm missing is the attribute values themselves. Server-rendering them onto the window object was an option, but wouldn't leave a nice feeling in my tummy. It's not keen on shoveling things into the global scope like that.
To be very honest, I could've had this solved in about 14 seconds. PicPerf.io is built with Astro, which offers a define:vars directive. It's stupid-easy to make server-side variables available to client-side scripts:
---
const truth = "Taxation is theft.";
---
<style define:vars={{ truth }}>
console.log(truth);
// Taxation is theft.
</style>
But there is neither fun nor a blog post in a problem solved so quickly.
More than that, define:vars is a highly proprietary way to address a challenge shared by many other platforms and content management systems (I've worked with them).
It's very usual for a CMS's constraints to be very narrow by design. An editor may be able to configure bits and pieces of the markup, but very rarely the contents of <script> tags. That's for good reason. A lot of potential security vulnerabilities there.
On top of that, those scripts often point to external packages shared by other teams, but still require some configuration. In those cases, server-rendering the values within the script isn’t an option at all.
<!-- Shared library, but still requires configuration! --><scriptsrc="path/to/shared/signup-form.js"></script>
In similar situations, I've seen configuration values made available via server-rendered data attributes. You define the attribute values, and the script takes it from there. Modular single-page applications whose configuration is set on the root node are where I've most often seen it:
importReactfrom'react';importReactDOMfrom'react-dom/client';importAppfrom'./App';const appNode =document.getElementById('app');const root =ReactDOM.createRoot(appNode);
root.render(// Use value plucked from root element.<ApprecaptchaSiteKey={appNode.dataset.recaptchaSiteKey}/>);
It should be glaringly clear where I'm going with this. Data attributes are a tidy way to pass specific values from the server to the client. In the SPA example above, the only extraordinarily nit-picky downside is the need to first query for the element before being able to access its attributes.
But since my scenario used a <script /> tag instead of any other element, I could eliminate that complaint too. The document.currentScript property provides it for free:
That's feeling pretty nice. There's nothing magically proprietary going on, no values are being jammed into the global scope, and it enables me to gloat on X that I'm "using the platform." Wins all around.
Say you maintain a JavaScript library that's required to be loaded asynchronously. It's simple to give quick, clear feedback using document.currentScript:
<scriptdefersrc="./script.js"></script>
// script.jsif(!document.currentScript.async){thrownewError("This script must be loaded asynchronously!!!");}// The rest of the library...
You could even enforce a certain <script> position on the page. Maybe it has to be loaded just before the opening body tag:
const isFirstBodyChild =document.body.firstElementChild===document.currentScript;if(!isFirstBodyChild){thrownewError("This MUST be loaded immediately after the opening <body> tag.");}
There's not much ambiguity in an error like that:
In all, it provides some nice, guided reinforcement. A great counterpart to solid documentation.
This one was brought up by ShotgunPayDay on Reddit. The Locality of Behavior principle asserts that you should be able to understand a chunk of code by looking at only that chunk of code (Carson Gross has a helpful post on it). Any sort of framework with "single-file components" (SFC) might come to mind. Everything's just there, in one spot.
As it relates to document.currentScript, it means you can build portable experiences just by virtue of elements being placed next to each other. Here's an example submits any form asynchronously just by placing the script tag right after it. Your central script would know to target the element right before <script>:
There's something very rewarding about finally understanding the utility of some of these web-old, lesser known features. It gives me an appreciation for the API designers of the early and adolescent web, especially knowing how often they need to deal with the arrogant complaints of modern programmers. I'm eager to see what else I'll find. Maybe AGI's already in the HTML spec and we just haven't found it yet.
Thx! Here’s also a great (at least I think so 😁) use case on my website: https://whatislove.dev/articles/bem-modifiers-in-pure-css-nesting/. Open/close a details based on screen width on initial load. I hide/show a details element containing article content, depending on which viewport the user opens the page from. On "mobile" (up to 768px) it’s closed by default; on " "desktop" (anything larger), it’s open by default.
0
replies
No spam. Might be irregular. Unsubscribe whenever. Won't be hurt.