“We’re seeing duplicate GTM Data Layer updates for a single Marketo form fill,” read the trouble ticket. “Sometimes even 3 or 4 updates.”

 

My mind went to adding multiple onSuccess listeners. Sure enough, they had a setup like this:

function gtmAfterSubmit(submittedValues){
  dataLayer.push({
    event: "marketoFormSubmit",
    form: submittedValues.formid
  });
});

MktoForms2.whenRendered(function(mktoForm){
  mktoForm.onSuccess(gtmAfterSubmit);
});  

 

Spot the problem?

 

It’s the use of whenRendered, which fires every time form elements are injected by Visibility Rules. They had some VRs in place (Country ⮕ State dependency, business vs. personal fieldsets) so the form rendered more than once. Each render meant another gtmAfterSubmit() pushed onto the onSuccess stack. Once the form submitted successfully, they all ran.

 

The fix is to use whenReady. To be clear, whenRendered is key to a lot of custom form behaviors — we more often see people use whenReady when they should be using whenRendered than the reverse! But here you indeed want whenReady.

 

Why weren’t the listeners deduped?

Problem solved. But they were still curious: “Shouldn’t the listener only be added once, since it’s a reference to the same function?”

 

“Shouldn’t” is subjective, of course. But I saw where they were coming from. With JavaScript’s native addEventListener, if you already added a listener function or object[1], it won’t be added again.

 

So in this code, where native XMLHttpRequest is used to submit a custom form, gtmAfterSubmit() will only run once, even though it’s seemingly added 2 times:

function gtmAfterSubmit(e){
  dataLayer.push({
    event: "formSubmit",
    form: form.id
  });
}

const ajaxSubmitter = new XMLHttpRequest;
ajaxSubmitter.addEventListener("load", gtmAfterSubmit);

/* … other stuff… */

ajaxSubmitter.addEventListener("load", gtmAfterSubmit);

 

The Marketo Forms 2.0 API doesn’t work that way. Every time you call onSuccess, the listener function is added, regardless of whether it was already there. In essence, Marketo uses an Array for its stack, while native JS uses a Set.

 

offSuccess prevents duplicates

Though the fix here is to switch to whenReady, you might encounter another situation where you aren’t sure if a listener was already added, e.g. when whenReady itself is used by multiple authors.

 

In that case, call offSuccess right before onSuccess. offSuccess removes the listener if it was already there, and otherwise does nothing. So with this code, gtmAfterSubmit() will only run once because we preemptively remove it:

function gtmAfterSubmit(submittedValues){
  dataLayer.push({
    event: "marketoFormSubmit",
    form: submittedValues.formid
  });
});

MktoForms2.whenReady(function(mktoForm){
  mktoForm.offSuccess(gtmAfterSubmit);
  mktoForm.onSuccess(gtmAfterSubmit);
});  

/* … other stuff… */

MktoForms2.whenReady(function(mktoForm){
  mktoForm.offSuccess(gtmAfterSubmit);
  mktoForm.onSuccess(gtmAfterSubmit);
});

 

And in case you’re wondering: nope, it’s not possible for anything to happen in-between the removing and adding because of JS’s run to completion guarantee.

 

Notes

[1] You also have to pass also the same options, like useCapture.