Marketo’s onValidate and friends stack like DOM event listeners: they run in the order they were added. But there are still major differences:
- In Marketo, the listener stack isn’t deduped. If you add the same listener function 3 times, it’ll run 3 times. In contrast, DOM
addEventListener(event,callback)checks if thecallbackwas already added and if so, doesn’t add it again. - In Marketo, all listeners always run. There’s no short-circuit method like
stopImmediatePropagation(). - In Marketo, to stop submission you call
submittable(). There’s no event-levelpreventDefault(). (NoteonValidate— but onlyonValidate— respects the callback’s return value, kind of like DOM inline event handlers.)
Adding (1) is an easy tweak that makes it a lot easier to manage advanced form behaviors with lots of coordinated components.
Adding (2) is quite tricky[1] and I’ll cover that another day. Might even throw in (3).😏
We’re adding idempotence to the Forms JS API
Idempotence is a show-offy CS term for an action that produces the same state, no matter how many times it runs.[2] DOM addEventListener is a great example. You can pass the same callback to addEventListener 1000x and the stack will be the same as if you only passed it once.
Here’s one built-in example of idempotence in Marketo Engage: you can run Change Program Status even if a lead already has that status: Marketo won’t stop you from running the action, it’ll just log the skip.
Another example: loading the Forms 2.0 or Munchkin library multiple times. Only the first load will create the global MktoForms2 or Munchkin while subsequent loads will be ignored.[3]
Anyway, wherever it’s feasible, idempotence makes code harder to break + easier to test.
Choosing our approach
First, note there’s no public access to the listener stack. (Same goes for DOM listeners, of course — you can’t access the stack of click or focus listeners from JS.)
That means we need to intercept calls to the on* and off* functions to maintain our own mirror of the current stack. In every on* call, check the mirror to see if if a callback is already present and ignore the call if so. In every off* call, delete the callback from our mirror and then process the call normally.
Get the code
If nothing else, this serves as a showcase for two JavaScript features, Set and Proxy. Those being built-in keeps our code pretty short and sweet:
/**
* Enable DOM-like idempotent event stack for Marketo forms
* @author Sanford Whiteman
* @version v1.0.0 2026-02-03
* @copyright © 2026 Sanford Whiteman
* @license MIT: This license must appear with all reproductions of this software.
*
*/
function enableIdempotentListeners(readyForm){
const events = ["Validate", "Submit", "Success"];
const handlerPairs = events.map( event => ["on" + event, "off" + event] );
handlerPairs.forEach( ([onHandler, offHandler]) => {
const idempotentStack = new Set();
readyForm[onHandler] = new Proxy(readyForm[onHandler], {
apply(originalOnHandler, thisArg, [callback]){
if( !idempotentStack.has(callback) ) {
Reflect.apply(originalOnHandler, thisArg, [callback]);
idempotentStack.add(callback);
}
return readyForm;
}
});
readyForm[offHandler] = new Proxy(readyForm[offHandler], {
apply(originalOffHandler, thisArg, [callback]){
Reflect.apply(originalOffHandler, thisArg, [callback]);
idempotentStack.delete(callback);
return readyForm;
}
});
});
}In the distant past, we would’ve done this with Array indexOf() and storing the original handlers in a closure, but it’s fun to use later-model stuff.
Run enableIdempotentListeners on form load
Before enableIdempotentListeners starts intercepting, onValidate/onSubmit/onSuccess will work as they usually do. So ideally you start intercepting immediately by using onReady, i.e. the 4th argument to loadForm():
MktoForms2.loadForm("//123-ABC-456.mktoweb.com", "123-ABC-456", 999, enableIdempotentListeners);If you absolutely can’t use onReady, you can use whenReady() right after loading the forms library:
MktoForms2.whenReady(enableIdempotentListeners);Enjoy your newfound idempotence
Now, even if you add the same callback under multiple conditions, it’ll only run once:
function validateSpecial(nativeValid){
if( !nativeValid ) return;
console.log("something’s gonna be done for form", mktoForm.getId());
}
MktoForms2.whenReady(function(readyForm){
if( someCondition ) {
readyForm.onValidate(validateSpecial);
}
/* ... do lots and lots of other stuff... */
if( someOtherCondition ) {
// thanks to idempotence, this won’t add the callback again
readyForm.onValidate(validateSpecial);
}
});Notes
[1] You might think, “Just use a variable in a shared scope,” e.g. check if skipFurtherValidation === true at the top of each listener function. That’ll work the first time through the stack, sure! But remember, there’s no event object passed to Marketo listeners.
(By contrast, with DOM submit events, a new SubmitEvent is created on each attempt and passed to each submit listener. One click ⇒ one distinct SubmitEvent.)
So consider someone trying to submit a Marketo form after correcting an initial error. How do you know if this is a new submit attempt, so skipFurtherValidation should be reset, rather than reading its last value? 😜
[2] Some people misdefine an idempotent action as an action that throws an error if it’s already run. But that’s not it. Idempotence means you can re-run without an error yet without further changes.
Others misdefine it as an action that never makes changes, like a database query. But that’s not it, either. A read-only query is implicitly idempotent, yes. But an update or insert can also be idempotent.
[3] When loading forms from different Marketo instances, this optimization becomes suboptimal and I’ve written before about how to disable it.