De-creepifying ZoomInfo FormComplete on Marketo forms

ZoomInfo’s FormComplete plugin is... potentially pretty cool. (I’m not here to endorse any product... though I didn’t say anything mean, which is a good sign!)

FormComplete delivers DiscoverOrg enrichment data directly into form fields, so starting from just an email address, you can get most — or even all — of the form fields pre-filled, in B2B at least.

(Note: this is separate from (a) pre-filling form data from Marketo for known leads and from (b) the web browser’s ability to autofill from previously stored data.)

The problem, as one of my clients put it well, is that it can be kind of creepy. Seeing your personal data evidently loading from a mysterious service, especially if you know you don’t have that data saved in the browser, ain’t so good:

animated-ss

(The inverse relationship between creepiness and friction can extend to activity tracking as well. But I digress.)

Much better would be having fields that are populated by FormComplete/DiscoverOrg not show up at all. So all fields other than Email are hidden to start, and only those that remain empty after the lookup, if any, are unhidden:

animated-ss

So I set out to reverse-engineer the ZoomInfo logic (since it’s sparsely documented, to put it kindly) and hook it into the Marketo Forms 2.0 JS API and came up with something pretty good.[1]

I’ll leave the ZoomInfo side to you

The setup within ZoomInfo — you have to be at least a trial customer — involves getting a unique formId for their embed code (that ZoomInfo form ID is not related to the Marketo form ID) and telling them which fields you want to return.

Their pricing, I believe, is based on how much firmographic/technographic info you want, so in this case we’ll start with the most basic tier: First, Last, Phone, Title, and Company.

Start with a regular ol’ Marketo form

No need for anything too fancy in Form Editor. Just sure all the possible ZoomInfo fields are on the form (they won’t be dynamically injected, so they need to be in Form Editor explicitly).

The fields may or may not be marked Required, and potentially some could be Hidden. The assumption in the code is that any non-Hidden field that’s set up in ZI, but for which ZI doesn’t have data, will be shown to the end user.

Ergo, if Company is a Text field on the form, and ZI doesn’t know the person’s Company, then Company is revealed so they can fill it in.

If you want to send ZI’s info to Marketo whether it’s empty or not, and never show it to the end user, then you’d set the field as Hidden in Form Editor, not Text.

Take control of field visibility in JS/CSS

A prerequisite for any such project is pinpoint, client-side control over the visibility of fields that are set to always-on, always-visible in Form Editor — that is, not in the Progressive Profiling block, and not of type Hidden.

And the way to get that control is the FormsPlus-Tag JS library, available from this link: teknkl-formsplus-tag-0.2.3.js. Download the file to your server — please don’t try to serve it from my CDN, it’s not guaranteed to be cross-domain-linkable — and include it as a remote <script> anywhere after the Forms 2.0 embed code and it’ll make you very happy.

FormsPlus-Tag adds a data-wrapper-for attribute to the rows and columns of the form DOM. The value is a space-delimited list of all the fields that are inside the element. (In the example below, it’s a single-column form, but it also works with multi-column forms, where the value would be like data-wrapper-for="Email FirstName LastName", just like a class attribute.)

2020-08-04-23_41_00-CodePen---Inference-MktoForms2-__-ZoomInfo-hook-v1.0.1--RESTRICTED-

This simple addition to the markup allows you to hide rows and columns using CSS styles. For a simple, admittedly off-topic example: imagine you want to dynamically hide the Company if some other conditions on the page (not just on the form!) are met.

You could add a class no-company to the <form> element before you load FormsPlus-Tag, and have this CSS:

.mktoForm {
   visibility: hidden;
   position: absolute;
}
.mktoForm[data-initial-wrapper-tagging-complete="true"] {
   visibility: visible;
   position: static;
}
.mktoForm.no-company > .mktoFormRow[data-wrapper-for~=Company] {
   display: none;
}

Note the .mktoForm as a whole is hidden until FormsPlus-Tag has added its special “I’m done tagging” flag, data-initial-wrapper-tagging-complete="true", to the form element. Otherwise, people would see Company for a moment before it disappears. You don’t want that!

FormsPlus-Tag also does a lot more than this, but I don’t want to get bogged down today. The important thing is we do have a way to selectively unhide fields. Now over to ZoomInfo.

Take control of the ZoomInfo field-filling logic

Turned out there were 2 different things to hack away at on the ZoomInfo side:

  1. Firing a JS callback when ZoomInfo server returns a set of found values.
  2. Stopping ZoomInfo from also filling <input>s with the values, even though it had fired the callback and seemingly passed control over to JS.

I’ll just dip a bit into each area.

Figuring out the callback logic was pretty easy: it’s not publicly documented, but it is built-in. So I didn’t need to hack the Ajax call or anything. You can add a callbacks.onMatch function to the global window._zi object, before injecting the ZoomInfo script:

window._zi = { 
  callbacks: {
    onMatch: function(ziFieldsObj){
      // do something with the fields in ziFieldsObj (a JS Object)
    }
  }
};

The harder part, surprisingly, was stopping ZI from filling fields. Normally, you assume a callback function would let you return false; or set dontDoYourDefaultThing = true; or something like that to stop the out-of-the-box behavior from happening. But there’s no such function here, it just assumes you want it to fill the fields anyway!

A little more peering at their code showed a pretty easy workaround. If you set the attribute data-hasusertyped to "true" on an element, ZoomInfo thinks the person has overridden the ZoomInfo-supplied data with their own. So you can just add that attribute to all fields to start out. That gives you complete control from JS.

Putting it together

You’ll need the teknkl-formsplus-tag-0.2.3.js file provided above, and these 2 ZoomInfo-specific files:

  1. teknkl-zoominfo-smartreveal-1.0.1.css
  2. teknkl-zoominfo-smartreveal-1.0.1.js

As with the FormsPlus-Tag file, please download and re-upload to your own server.

The CSS is very simple. I wanted a smooth reveal of the empty fields instead of just blasting them into view, so it transition-s the height. You’ll be able to tweak/remove that effect if you know CSS.

The JS is quite precise and I don’t recommend modifying it on your own. If something doesn’t make sense or seems to be missing just ask me. It also has a number of advanced, optional behaviors that aren’t likely to be necessary. (I’d thought during development that the UX might be weird in some edge cases. Turned out those cases were covered but I left the advanced code in there anyway.)

To deploy the solution, include the 3 remote files in the following order, after a li’l config block:

<script>
window.smartZIRevealUserConfig = {
  ziManagedFields : [
     "Email",
     "FirstName",
     "LastName",
     "Phone",
     "Title",
     "Company"
  ],
  ziLookupField : "Email",
  ziFormId : "s8YcuR7L0CVxStvxZ2wH"
};
</script>
<link id="teknkl-ZoomInfo-CSS-1.0.1" rel="stylesheet" href="https://yourserver.example.com/teknkl-zoominfo-smartreveal-1.0.1.css">
<script id="teknklFormsPlus-Tag-0.2.3" src="https://yourserver.example.com/teknkl-formsplus-tag-0.2.3.js"></script>
<script id="teknklZoomInfo-JS-1.0.1" src="https://yourserver.example.com/teknkl-zoominfo-smartreveal-1.0.1.js"></script>

(The above completely replaces the default ZoomInfo-provided embed code.)

The config block is simple. ziManagedFields are all the same fields you set up in ZoomInfo’s app. ziLookupField is almost certainly going to be capital-E Email, but hey, there might be some weird exception, so it’s configurable. ziformId is the ZoomInfo-assigned form ID (remember, nothing to do with the Marketo form ID).

There’s also a special “developer-only” mode (not my creation — it’s a ZoomInfo feature, but the code lets you enable it easily). Enter dev mode by adding the special hash #dev to your page URL. In this mode, you can use 1 of 3 test addresses:

full-match@zoominfo.com
partial-match@zoominfo.com
no-match@zoominfo.com

Each of those addresses sends back a selection of hard-coded fields, so you can see what happens when/some/none of the fields are filled. Dev mode doesn't count any API calls/rate limits against your ZoomInfo subscription, which is great!


Notes

[1] If I didn’t spend so much of my time reverse-engineering other people’s apps, this would’ve been a terrifying prospect. But luckily, there are only so many ways it could work, and with a wide array of interception/detouring/metaprogramming tools available in JavaScript, I felt sure the default behavior could be overridden in a reliable way.