“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
.