Using modern JavaScript in GTM Custom HTML tags

Google Tag Manager has a JavaScript problem. Yep, that’s a strong claim.


Isn’t GTM designed to inject so-called “tags” — HTML fragments including <script> elements and perhaps related markup like <form> elements and <div> containers[1] — into your pages? How could it have a JS problem if it’s pretty much about JS?

OK, let me be fair: it only has a problem if you’re trying use recent JS. “Recent” as in syntax and keywords that were ubiquitous in, say, 2013. If you don’t mind coding like it’s 2008, you’re good.😛

See, the Custom HTML type tag supports local <script> blocks, but they’re only allowed to use ECMAScript 5 features.[2] For example, an attempt to use block-scoped variables — let or const — makes your workspace unpublishable:

I’m no longer religious about Internet Explorer support (through mid-2021, my published JS snippets were compatible with IE 11). But even IE 11 supports let and const, so I typically start a snippet like this:

<script>
(function(){
  const specialBehaviorsConfig = {
     "fieldName" : "Email"
  };
    
  MktoForms2.whenReady(function(readyForm){
    let formEl = readyForm.getFormElem()[0];
    // etc.

But you can’t just cut that snippet out of a Landing Page and paste it into GTM. I’m no great fan of GTM anyway because of the timing issues it creates, but this sucks extra hard.

The consensus seems to be to give up and rewrite everything to use the truly ancient var  just on the off chance it might be pasted into GTM. I couldn’t accept that.

Here’s the trick.

When validating a workspace, GTM looks for executable <script> tags, runs them through the Closure Compiler, and then puts the resulting code inside a <script type="text/gtmscript"> tag. That tag is then injected into the page based on your rules & triggers.

When first injected, since it doesn’t have one of the known JavaScript MIME types, it’s merely an inert data block — a block of “code-like text” but not code per se. The GTM JS library then uses your rules to determine which data blocks to reinject as runnable scripts. For those, it copies the inner text from the text/gtmscript into a new <script>, which runs immediately.[3]

You’re probably on the way to figuring this out: to inline modern JS, simply put it in a text/gtmscript block yourself:

Then the GTM back end won’t pass it through the Closure Compiler (since it’s just a data block and wouldn’t be expected to compile) but the GTM front end will still reinject and run it. Presto!

Notes

[1] On occasion, GTM injects true pixels as in <img> tags. But since JS is necessary to run GTM in the first place, you wouldn’t use normally a noscript option.

[2] GTM runs code through Google Closure Compiler to minimize it (and ostensibly to “optimize” it, but there’s no real difference with tiny inline scripts). The compiler supports a bunch of language levels: ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT, ECMASCRIPT_2015, ECMASCRIPT_2016, ECMASCRIPT_2017, ECMASCRIPT_2018, ECMASCRIPT_2019, ECMASCRIPT_2020,ECMASCRIPT_2021, STABLE, ECMASCRIPT_NEXT. For whatever reason, it chooses ECMASCRIPT5 under the hood and that can’t be changed.

[3] As is required when setting innerHTML that includes <script> tags. Otherwise they’re left in the confusing half-state of “scripts in the DOM that never ran.”