Randomly shuffling the order of checkboxes to shake off bias

SanfordWhiteman
Level 10 - Community Moderator
Level 10 - Community Moderator

Interesting thread over in the Products discussion area, where user DN asked:

 

I am trying to create an unsubscribe form that has various values for the reason someone is unsubscribing.
For example: “I no longer want to receive these emails,” “The emails are too frequent,” “I never signed up for this mailing list,” and “The emails are inappropriate.”
Is there any way I can randomize the order the above values appear for each new user so that people do not just choose the first option? Thanks!

 

Here’s the form in question:

2022-06-16-17_01_45-Leverage-Data-Across-the-Entire-Asset-Lifecycle---Waterfox[1].png

 

Note it also has an “Other” option, which (as I suspected) DN wants to keep at the bottom, not shuffle. When “Other” is checked, there’s a a Visibility Rule that reveals a textbox for Unsubscribed Other Reason, so we need to make sure that functionality is intact.

 

 

The code, Take 1

It’s easy to randomly shuffle DOM child elements, as long as you know how the markup is structured (which we definitely do with Marketo forms). You merely create an array of the elements, shuffle the array, and then reinsert the shuffled array from top to bottom.

 

The only quirks here are:

  1. 1. pairing each <input> element with its related <label> element (the label is always the input’s next element sibling in the DOM)
  2. 2. making sure the “Other” input + label are fixed at the end
  3.  

So here you go:

(function () {
   /**
    * Shuffle Marketo Checkbox/Radio sets
    * @author Sanford Whiteman
    * @version v1 2022-06-20
    * @copyright © 2022 Sanford Whiteman
    * @license Hippocratic 3.0: This license must appear with all reproductions of this software.
    *
    */
   const shuffleField = "UnsubscribedReason",
         fixedAtBottom = ["Other"];

   /* NO NEED TO TOUCH BELOW THIS LINE */
   
   const arrayify = getSelection.call.bind([].slice);

   MktoForms2.whenReady(function (mktoForm) {
      let formEl = mktoForm.getFormElem()[0];

      let shuffleContainer = formEl.querySelector("[name='" + shuffleField + "']").parentElement;

      let originalOptions = arrayify(shuffleContainer.querySelectorAll("input"))
      .map(function (input) {
        let label = input.nextElementSibling,
            value = input.value;
        
        return {
          value: value,
          input: input,
          label: label
        };
      });

      let fixedOptions = [],
          shuffleOptions = [];      
      originalOptions
      .forEach(function (desc) {
        return fixedAtBottom.indexOf(desc.value) == -1 
           ? shuffleOptions.push(desc)
           : fixedOptions.push(desc)
      });
            
      let finalOptions = makeShuffled(shuffleOptions).concat(fixedOptions);
      
      finalOptions
      .forEach(function (desc) {
        shuffleContainer.appendChild(desc.input), shuffleContainer.appendChild(desc.label);
      });     
      
   });
   
   /**
    * Fisher-Yates shuffle {@link https://javascript.info/task/shuffle}
    * @author Kantor Ilya Aleksandrovich
    * @license [CC-BY-NC-SA]{@link https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode}
    *
    */   
   function makeShuffled(array) {
      let shuffled = [].concat(array);
      for (let i = shuffled.length - 1; i > 0; i--) {
         let j = Math.floor(Math.random() * (i + 1));
         let t = shuffled[i];
         shuffled[i] = shuffled[j];
         shuffled[j] = t;
      }
      return shuffled;
   }   
   
})();

 

That’ll randomly alternate between

2022-06-16-17_02_37-Leverage-Data-Across-the-Entire-Asset-Lifecycle---Waterfox[1].png

 

and

 

2022-06-16-17_03_44-Leverage-Data-Across-the-Entire-Asset-Lifecycle---Waterfox[1].png

 

 

and so on. All 24 permutations without the agony of building a 24-way A/B test!

 

Just swap in your target field for the shuffleField and put your fixed fields, if any, in the fixedAtBottom array. Remember to use SOAP field names.

 

Not quite there

There’s still a problem, though.

 

You shuffled the order in hopes that people would seek out their real reason instead of clicking the first one. But how can you be sure they still didn’t just click the top one, which is now going to hold a different value for different people?

 

For that, you’d need to also reflect the order — the specific order they saw at runtime — in a hidden field.

 

The code, Take 2

The expanded v2 below saves the indexes (1-based for friendliness, rather than 0-based) of all selected items to a hidden String field, the indexesField.

 

For example, it’ll store “2” or “1;3;5”. Set indexesField to undefined to skip this feature.

(function () {
   /**
    * Shuffle Marketo Checkbox/Radio sets
    * @author Sanford Whiteman
    * @version v2 2022-06-20
    * @copyright © 2022 Sanford Whiteman
    * @license Hippocratic 3.0: This license must appear with all reproductions of this software.
    *
    */
   const shuffleField = "UnsubscribedReason",
         fixedAtBottom = ["Other"],
         indexesField = "UnsubscribedReasonIndexes";

   /* NO NEED TO TOUCH BELOW THIS LINE */
   
   const arrayify = getSelection.call.bind([].slice);

   MktoForms2.whenReady(function (mktoForm) {
      let formEl = mktoForm.getFormElem()[0];

      let shuffleContainer = formEl.querySelector("[name='" + shuffleField + "']").parentElement;

      let originalOptions = arrayify(shuffleContainer.querySelectorAll("input"))
      .map(function (input) {
        let label = input.nextElementSibling,
            value = input.value;
        
        return {
          value: value,
          input: input,
          label: label
        };
      });

      let fixedOptions = [],
          shuffleOptions = [];      
      originalOptions
      .forEach(function (desc) {
        return fixedAtBottom.indexOf(desc.value) == -1 
           ? shuffleOptions.push(desc)
           : fixedOptions.push(desc)
      });
            
      let finalOptions = makeShuffled(shuffleOptions).concat(fixedOptions);
      
      finalOptions
      .forEach(function (desc) {
        shuffleContainer.appendChild(desc.input), shuffleContainer.appendChild(desc.label);
      });
      
      mktoForm.onSubmit(function(mktoForm){
         if(!indexesField) return;
                   
         let currentValues = mktoForm.getValues()[shuffleField];         
         currentValues = Array.isArray(currentValues) ? currentValues : [];
         
         let currentIndexes = [];         
         finalOptions
         .forEach(function(desc,idx){
           if(currentValues.indexOf(desc.value) != -1){
             currentIndexes.push(++idx);
           }
         });            
         
         let mktoFieldsObj = {};
         mktoFieldsObj[indexesField] = currentIndexes.join(";");
         mktoForm.addHiddenFields(mktoFieldsObj);
         
      });
      
   });
   
   /**
    * Fisher-Yates shuffle {@link https://javascript.info/task/shuffle}
    * @author Kantor Ilya Aleksandrovich
    * @license [CC-BY-NC-SA]{@link https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode}
    *
    */   
   function makeShuffled(array) {
      let shuffled = [].concat(array);
      for (let i = shuffled.length - 1; i > 0; i--) {
         let j = Math.floor(Math.random() * (i + 1));
         let t = shuffled[i];
         shuffled[i] = shuffled[j];
         shuffled[j] = t;
      }
      return shuffled;
   }   
   
})();
630
0