Inverting the submitted value of a Forms 2.0 Checkbox

The Forms 2.0 Checkbox type works straightforwardly: if it’s checked, it sends the string “yes” to Marketo, and if it’s unchecked, it sends the string “no”.

If the corresponding Marketo field is a Boolean, the “yes” is saved as true, and the “no” is saved as false.[1]

This behavior is desired almost all the time. You might have wanted it 100% of the time you’ve worked in Marketo. But working across a range of clients, I get to see site-specific cases, and sometimes... for pretty good reasons... you want the opposite.

That is, sometimes you want a ’box to send “no” if it’s checked and “yes” if it’s unchecked.

This is typically due to the way the question (i.e. label) is phrased — when it’s deliberately avoiding the negative/discouraging/technical tone of the underlying logic.

Take, for example, the Boolean field isMinorDependent labeled as “I’m 18 or over” on the form:

If the checkbox is unchecked, you want to set isMinorDependent to true. Which means the unchecked box needs to send “yes” and vice versa.

Another example is when an opt-out type of Boolean, whether it be the native Unsubscribed or Marketing Suspended or a custom field, is exposed as an opt-in type of checkbox:

Here you want the checked box to set Marketing Suspended to false. Which means the checked state needs to send “no” — so you need to invert the value before submitting.

What not to do

I’m not suggesting that you call setValues() in an onSubmit function to switch the value to its inverse. That would magically check or uncheck the checkbox while the person is looking at the form, which is strange-looking and (IMO) obviously unacceptable.

We’re also not going to hide the checkbox completely before calling setValues() to patch over the problem with the above. That’s also too strange for me.

And we’re also not going to replace the checkbox with a custom image or widget. (Though it feels like a good moment to feature my CSS-only Soft Checkboxes mockup from a few years ago, which has never seen production.☺)

And, finally, we’re not using a proxy field/hidden field combo.

In sum, the checkbox is going look exactly as it does by default, but act different, putting a different value on the wire to Marketo.

What to do

The high-level requirement:

Keep the visual checked/unchecked status the way the user left it, but make the Forms 2.0 library believe the checked/unchecked status is the opposite.

Or to put it more technically:

Intercept the forms library’s call to get the status and return a programmatically-controlled status.

What you probably don’t realize is there are multiple ways to get and/or set the checked-ness of a checkbox from JavaScript, and they may all give different results at the same point in time, depending on how the form is assembled and managed:

  1. whether the HTMLInputElement has the JS checked property set to Boolean true: input.checked
  2. whether the element matches the CSS pseudo-class :checked: input.matches(":checked")
  3. whether the related HTML <input> tag has the checked attribute: input.hasAttribute("checked")[2]

So you have to know which method is being used, or you’ll intercept the wrong one, having no effect.

The Marketo Forms library uses method (2) —  matches() — but calls it via the jQuery wrapper method is().

The use of jQuery is a good thing in this case, because it makes it easier to intercept the call.[3] I wrote code to add the necessary events to jQuery and documented it here:

Extending jQuery’s “is” with “before is” and “after is” event listeners

We’ll be dropping in the code from that post as-is. And that’s really all we need: the rest is just using the custom fn.is.after event inside the standard Marketo onSubmit event.

Here’s the final code:

/*
 * jQuery :: beforeIs/afterIs events
 * @author Sanford Whiteman
 * @version v1.0.0 2021-08-05
 * @copyright © 2021 Sanford Whiteman
 * @license Hippocratic 2.1: This license must appear with all reproductions of this software.
 *
 */
function installCustomIsHandlers(jQ) {
   let originalIs = jQ.fn.is;

   jQ.fn.is = function (initialSelector) {
      let selector = this.triggerHandler("fn.is.before", { selector: initialSelector}) || initialSelector;
      
      let is = originalIs.call(this, selector);
          
      let extendedIs = this.triggerHandler("fn.is.after", { selector: selector, was: is });

      if (typeof extendedIs == "boolean") {
         return extendedIs;
      } else {
         return is;
      }
   };
}

// enrich Marketo's jQuery with the new events
installCustomIsHandlers(MktoForms2.$);

MktoForms2.whenReady(function (mktoForm) {
   
   // put the Checkbox fields you want to invert in this array
   let invertibleCheckboxNames = ["Unsubscribed"]; 

   mktoForm.onSubmit(function (mktoForm) {
      invertibleCheckboxNames.forEach(function (fieldName) {
         let fieldSelector = "input[type='checkbox'][name='" + fieldName + "']",
             fieldJq = mktoForm.getFormElem().find(fieldSelector);
         
         // flip the `is` result 
         fieldJq.on("fn.is.after", function (event, context) {
            if (context.selector == ":checked") {
               return !context.was;
            }
         });
      });
   });
});

Notes

[1] Assuming field updates aren’t blocked in Admin.
[2] Not reliable unless the original HTML had <input type="checked"> — that is, that the form wasn’t assembled using JS. Avoid this one at all costs.
[3] You could hyper-locally override input.matches just for that element, which is very safe — but won’t help if you have a framework that call()s the “master” document.documentElement.matches instead.