Beware the πšπš˜πš›πš–.πšœπšžπš‹πš–πš’πšπšπšŠπš‹πš•πšŽ(πšπš›πšžπšŽ) footgun

In programming slang, a footgun is a prominent feature that’s so easy to misuse β€” to shoot yourself in the foot β€” that everyone should be wary of it, even if it has its uses.

Perhaps a bit strong to describe the form.submittable(true) method of the Forms 2.0 JS API. Yet that method is used incorrectly the majority of the time (including by yours truly!) so let’s see how to use it right.

You typically see form.submittable(true) used like so:

MktoForms2.whenReady(function(readyForm){
   readyForm.onValidate(function(isNativeValid){
      if(!isNativeValid) return;
      
      const currentValues = readyForm.getValues();

      readyForm.submittable(false);
      if( currentValues.FirstName == "Santa" ) {
         readyForm.submittable(true);
      } else {
         readyForm.showErrorMessage("Must Be Santa");      
      }
   });
});

That’s a basic custom validation function:

  • if(!isNativeValid) makes sure the native validation rules succeeded (i.e. Form Editor rules like fields marked Required and string/number patterns)
    • if not, exit because you want those rules to pass before running custom stuff
    • if so, continue
  • store the current field values: form.getValues()
  • set the form to not be submittable: form.submittable(false)
  • check a custom validation rule
    • if validation succeeds, set the form back to submittable with form.submittable(true)
    • if validation fails, show an error message

Looks right, right? Indeed, if that’s the only custom onValidate function, anywhere on your page or in external scripts, you’re fine. But in reality, extended validation gets added in different places. There’s a remote <script src> that does email address validation, or you add one <script> in GTM and another <script> right on the LP, etc.

To be clear: separating validation tasks into different functions like this is best practice. But you have to write each function so it knows it might be in a chain of validation functions. People tend to not do that.

Look at the following onValidate chain, which adds another custom validation rule, making sure a Checkboxes field has at least 2 boxes checked:

<script>
MktoForms2.whenReady(function(readyForm){
   readyForm.onValidate(function(isNativeValid){
      if(!isNativeValid) return;
      
      const currentValues = readyForm.getValues();

      readyForm.submittable(false);
      if( currentValues.FirstName == "Santa" ) {
         readyForm.submittable(true);
      } else {
         readyForm.showErrorMessage("Must Be Santa");      
      }
   });
});
</script>
<script>
MktoForms2.whenReady(function(readyForm){
   readyForm.onValidate(function(isNativeValid){
      if(!isNativeValid) return;
      
      const currentValues = readyForm.getValues();

      readyForm.submittable(false);
      if( [].concat(currentValues.MeetingAvailability).length >= 2 ) {
         readyForm.submittable(true);
      } else {
         readyForm.showErrorMessage("Must select at least 2 meeting times")      
      }
   });
});
</script>

(I’ve shown the 2 <script> tags right next to each other for simplicity, but in reality expect them to be in entirely different places.)

Can you spot the bug? Take a minute to think through possible outcomes. I’ll wait.

😴

😴

😴

OK. The bug is that if the person’s First Name isn’t β€œSanta” but they did check 2 meeting times, the form will submit. While if the person’s First Name is β€œSanta” and they didn’t check 2 meeting times, the form won’t submit. That’s not the way it’s supposed to work: the validation rules are supposed to both succeed!

The cause of the bug is assuming that when each onValidate listener runs, form.submittable() starts out as true, so you restore it to true. That’s a bad assumption! It may start out as false, in which case you want to leave it that way β€” and don’t even bother running other validation logic.[1]

At the same time, each new pass through the onValidate chain (e.g. after the person changes form values and re-submits) must be independent of the last pass, which might have left submittable set to false.

So you need two complementary measures:

(1) Add a first listener that that only sets form.submittable(true), nothing else.

(2) In all subsequent listeners, check if form.submittable() is still true at the top of the function.

For (1), you might wonder how to ensure you’re adding the very first listener. Truth is, there’s no one-size-fits-all way β€” depends on your overall setup[2] β€” but putting it directly after the form embed[3] suffices in most cases:

<script src="//pages.example.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_123">
<script>MktoForms2.loadForm("pages.example.com", "123-ABC-456", 9999);</script>
<script>
MktoForms2.whenReady(function(readyForm){  
    readyForm.onValidate(function(isNativeValid){
      if(!isNativeValid) return;
     
      // this is the known-first listener, so set to `true`
      readyForm.submittable(true);      
    });
});
</script>

For (2), check submittability alongside native validity:

<script>
MktoForms2.whenReady(function(readyForm){
   readyForm.onValidate(function(isNativeValid){
      if( !isNativeValid || !readyForm.submittable() ) return;
      
      const currentValues = readyForm.getValues();      
      
      readyForm.submittable(false);      
      if( currentValues.FirstName == "Santa" ) {
         readyForm.submittable(true);
      } else {
         readyForm.showErrorMessage("Must Be Santa");
      }
   });
});
</script>
<script>
MktoForms2.whenReady(function(readyForm){
   readyForm.onValidate(function(isNativeValid){
      if( !isNativeValid || !readyForm.submittable() ) return;
      
      const currentValues = readyForm.getValues();

      readyForm.submittable(false);
      if( [].concat(currentValues.MeetingAvailability).length >= 2 ) {
         readyForm.submittable(true);
      } else {
         readyForm.showErrorMessage("Must select at least 2 meeting times")
      }
   });
});
</script>

That’s it!

Notes

[1] Some form designs require checking all validation rules, regardless of whether form.submittable() was already false. That lets you add a red border around all invalid fields at once, for example. But you still must make sure you don’t set form.submittable(true) unless every rule has passed. (The code for always-check-all-rules is more complex but you can ask about it in the comments.)

[2] Complex load sequences, like using my onMktoForms2Ready pattern, make adding a known-first listener pretty tricky.

[3] On a Marketo Guided LP Template, this means directly after the <div class="mktoForm"> element (the embed code is automatically injected inside that <div>).