2021-09-16-00_46_25-CodePen---MktoForms2-__-The-Trouble-With-Rich-Text-Behaviors---Mozilla-Firefox

JavaScript on the page, Never fear / JavaScript in a Rich Text, Hold my beer

SanfordWhiteman
Level 10 - Community Moderator
Level 10 - Community Moderator

Just because you can serve Forms 2.0 JavaScript enhancements from a Rich Text area on the form — I showed how to do that in an earlier post — doesn’t mean you should!

 

If you use this method, you need to be sure — much more sure than usual — that your code doesn’t bug out. If you tend to code optimistically, instead of defensively, you shouldn’t do it in a Rich Text.

 

Let’s say this Forms 2.0 JS had been running on the page itself:

<script>
MktoForms2.whenReady(function(mktoForm){
   document.querySelector("#gem-details").open = true;
});
</script>

 

This particular code finds the <details> element with a known id, then reveals it as the form header. But the code could be doing anything — I just took a simple example from a recent Community thread!

 

The expected final output is like this:

2021-09-16-00_46_25-CodePen---MktoForms2-__-The-Trouble-With-Rich-Text-Behaviors---Mozilla-Firefox

 

But even if there isn’t a<details>with thatidon the page, the form still renders and works, just without its header text (“Fill out the form below.”) The script errors out, since querySelector() returns null (which you’d see easiy in the F12 console) but there aren’t any other ill effects.

 

Now you think to yourself, “It’s just a couple of lines, that’s annoying to remember to put on the page. Besides, it depends on the form being there, so why I don’t I put it in the form itself?”

 

So you move that same code[1] into a Rich Text area on the form:

2021-09-16-00_38_30-Lab---Broken-JS-in-RTA

 

(You don’t have to listen for whenReady anymore, because the form must be present if you’re, uh, inside the form at the time.)

 

So that seems simpler. And yep, it works fine, if the <details> is there.But suppose somebody on the web team changed the id of the <details> without considering the consequences:

<details id="jewel-details"><summary />Fill out the form below</details>

 

Now your form is gonna look like this:

2021-09-16-00_46_51-CodePen---MktoForms2-__-The-Trouble-With-Rich-Text-Behaviors---Mozilla-Firefox

 

To put it mildly, that form ain’t showing up. Let’s trace why.

 

First, check the F12 Console and you’ll see the direct cause. document.querySelector() returns null, so you obviously can’t set the open property on it:

2021-09-18-03_13_19-CodePen---A-Pen-by-Sanford-Whiteman---Mozilla-Firefox

 

Wait a minute, though. Didn’t I say in my last post that independent <script> tags never affect each other? Shouldn’t the <script> inside the Rich Text have its own Execution Context, so even if it errors out, it doesn’t cause catastrophic side effects?

 

Hold that thought. ☺

 

Surprises herein

Let’s inspect the HTML:

2021-09-16-00_47_59-CodePen---MktoForms2-__-The-Trouble-With-Rich-Text-Behaviors---Mozilla-Firefox-1

 

OK, that’s kind of strange, right? We see the <script> in a Rich Text container (mktoHtmlText)*, but it’s inside another <form> element at the base of the page. Not the <form> element we placed in the body.

 

And that secondary <form> is actually thrown offscreen (via position: absolute, top, and left styles), while the standard <form> remains empty.

 

But here’s the thing: that other <form> is always there, even on a good day! You just didn’t notice it unless you needed to do deep DOM inspection. Here’s how the form(s) look normally:

2021-09-15-19_36_36-CodePen---A-Pen-by-Sanford-Whiteman---Mozilla-Firefox

 

The secondary <form> is for dynamically calculating static dimensions (if that’s not too confusing)

In normal operation, the main <form>, obviously, is where the visible elements are rendered.

 

The secondary <form> is empty 99.9999% of the time, because it’s only used to measure the dimensions of prospective elements before injecting them into the visible area.

 

As you probably know (but may not have thought deeply about) Forms 2.0 mixes static height and width values with responsive CSS dimensions. In order to calculate the former, it needs to render elements offscreen and see what they’ll look like. Then it moves them into the main <form> when all the calculations are done. (This approach makes sense, by the way, if you need to know things like “What’s the widest child element?” without unbearable flicker.)

 

So now, at least, you know why there’s another form. But why does the measuring<form> stop in its tracks just because there’s a TypeError thrown by a standalone <script> that appears in one Rich Text area? Why doesn’t the JS error get logged to the console in the background, while the elements themselves continue to be measured & moved?

 

Another factoid about <script> tags

It comes down to another thing you probably haven’t had a chance to learn (unlike us old-timers) about the <script> element.

 

Let’s see if I can keep this simple: code is not executed if you just insert an inline <script>, as HTML, using JavaScript.

 

In other words, this will insert a <script> tag, yes:

document.querySelector("#someWrapper").innerHTML = "<script>alert('Hi!');</script>";

 

But it’ll be a <script> in every way but the one that counts!

 

You can see it in F12 Dev Tools, where it looks like any other <script>. You can find it in the DOM using querySelector. It’s a member of the document.scripts collection, and it’s an instance of HTMLScriptElement. But the code doesn’t run!

 

Weird, eh? Well, such is browser life.

 

Executing the inexecutable

So there are 2 ways around this, which JS frameworks have been choosing between since time immemorial:

  • 1. Read the inner code of the <script> as a string and immediately  eval() it.
  • 2. Create a new script using document.createElement("script"), set its text property to the code string, and inject it into the DOM.

 

Marketo’s Forms 2.0 library chooses the former, which is marginally more compatible (the latter doesn’t work in IE 8, which absolutely no one cares about today but which was still a concern when Marketo first debuted).

 

Other than compatibility, the 2 methods are generally considered equivalent. Method 1 is a little faster, not that you’d notice as we’re talking nanoseconds.

 

But there’s something else, a major difference that’s usually ignored: with Method 1, the first error from eval() stops execution, while with Method 2, scripts can’t crash each other.

 

And that, friends, is why having bad code inside a Rich Text is far more fatal than you expect. Rather than injecting independent <script> tags, the code is eval’d inside the same loop that builds the form. So any error is a global error.

 

What to do?

An obvious solution is to pull your code out of the Rich Text and put it back on the page, which is to my mind more manageable and debuggable anyway (really, who wants to debug eval’d code?). You’ll still need to find errors, but they may not be fatal to the core functionality/visibility of the form.

 

Or you can make your code more resilient, even if you otherwise consider it non-critical. In this case, check if the element exists:

<script>
MktoForms2.whenReady(function(mktoForm){
   let formHeader = document.querySelector("#gem-details");
   if (formHeader) {
     formHeader.open = true;
   }
});
</script>

 

Another option, but hardly easy: don’t modify your code. But do get your change management and QA testing procedures in line. Good luck!

 

NOTES

[1] Note the <![CDATA[ wrapper is not related to the problem, that’s just something Marketo does for backward compatibility.

322
0