What’s the difference between a single-form loadForm() callback and the all-forms whenReady()?

Let’s say you want to immediately switch to lightbox mode (using the Forms 2.0 native lightbox() feature) when a specific form is ready.

You’d think there’d be zero difference between Approach #1:

 MktoForms2.whenReady(function(mktoForm){
   if(mktoForm.getId() == 1234)
      MktoForms2.lightbox(mktoForm).show();
   }
 });
 MktoForms2.loadForm("//pages.example.com", "410-ABC-673", 1234);

And Approach #2:

MktoForms2.loadForm("//pages.example.com", "410-ABC-673", 1234, function(mktoForm){
  MktoForms2.lightbox(mktoForm).show();
});

Both snippets load the form and fire a callback when it’s ready.

In #1, the callback function technically runs when any form becomes ready, but the lightbox is conditioned on the form ID. (For a page with only one form, that condition wouldn’t be necessary.)

In #2, the callback runs only when form 1234 is ready, since it’s part of that form’s  loadForm() call.

All other things being equal, #1 would be better, since you can manage browser behaviors (like lightbox()-ing) separately from loading the form from the server.

But they’re not equal! Check out the momentary flicker of the form at the upper left in Approach #1:

No such flicker for Approach #2, where the form only shows in lightbox mode:

Note the behavior of #1 is the same whether whenReady() comes before or after loadForm(). I just put it before so you wouldn’t be tempted to think that was the cure.

Why does this happen?

It’s a subtle implementation difference that has to do with when a browser repaints.

The way an onSingleFormReady callback works — it doesn’t have that official name, it’s really just “the 4th argument to loadForm” — is that immediately after the form is rendered, the callback runs. “Immediately” meaning on the very next line, in the same execution context:

form.render();
callback();

In contrast, the whenReady callback might be also be thought of as “immediate,” but it isn’t, let’s say, as immediate. Instead, whenReady listeners fire asynchronously. There are actually 2 different async modes[1], but they’re both equivalent (for today’s purposes) to:

form.render()
setTimeout(callback, 0);

setTimeout with a delay of 0, as you may already know, doesn’t really mean a delay of zero milliseconds. It means 4ms + whatever additional time the browser needs to schedule a new task. So it might be just 4ms or a few millis more, maybe even a few seconds if other tasks are going crazy in the browser at that moment.

But even if it takes the absolute minimum of 4ms, the browser still schedules a new task, “turning” the JavaScript event loop. And since the initial task has completed (the one that ends with the call to setTimeout) the browser takes the opportunity to repaint the window.

So that’s why you see the flicker of the form in its original position before the ever-so-slightly-delayed function runs and moves the <form> into the lightbox.[2]

You might not ever notice this difference, of course. 99.999% of form behaviors (which you should be adding using whenReady, by the way!) don’t need to repaint the screen. The lightbox is a notable exception because the <form> element is relocated in the DOM.

Solving the flicker even with whenReady

Luckily, you can avoid the flicker in all cases with a little CSS. When creating the shell <form> element, add a special attribute:

<form data-mktomodal="true" id="mktoForm_1234"></form>

Then hide forms with that attribute by default, only showing them after they’re moved inside the modal container (i.e. lightbox):

.mktoForm[data-mktomodal="true"] {
   visibility: hidden;
   position: absolute;
}
.mktoModal .mktoForm[data-mktomodal="true"] {
   visibility: visible;
   position: static;
}

Why visibility and position instead of display? Try the alternative and see. :)

Notes

[1] One is a literal setTimeout. The other is an EventEmitter event, which is also asynchronous.

[2] You can prove this easily by using the onSingleFormReady but forcing a setTimeout inside it:

MktoForms2.loadForm("//pages.example.com", "410-ABC-673", 1234, function(mktoForm){
  setTimeout(function(){
      MktoForms2.lightbox(mktoForm).show();
  },0);
});

That’ll demonstrate the flicker due to repaint.