File under Things You Thought Already Worked.
For awhile, I too thought multiple Marketo forms on the same page worked as long as they had different form IDs (though I always knew there was a problem with 2 or more of the same form).
But no, even different forms don’t really work. The only multi-form scenario that works out-of-the-box is different forms that have zero visible fields in common. Since Email Address is on almost all forms, it’s almost certain that multiple forms won’t be fully functional without the fix below.↓
The problems with multiple forms all relate to the HTML ID attribute.
Background: “ID must be unique” doesn’t mean what you think
You’ve probably heard that id
attributes on HTML elements/tags must be unique across an entire document.
This is true, in that a page isn’t valid HTML5 if it has duplicate IDs:
The [
id
attribute value] must be unique amongst all the IDs
You are not generally required to add id
to an element, but if you do, it must be chosen carefully.
But that doesn’t tell the real-world story.
In reality, browsers won’t reject pages served up with duplicate IDs; they'll happily render elements whose IDs were already used; and they won't strip off non-unique ID values. A browser won't even log a console message if it detects duplicate IDs! Doesn't mean there aren't consequences (as we shall see), especially with cross-element events and selecting elements in the DOM. But as the browser doesn't signal you to expect problems, they can remain undiscovered.
You might say browsers exercise “prosecutorial discretion” with regard to web standards.[1] Like all musts in standards and RFCs, §3.2.5.1 is the rule for generating valid HTML5 markup. The must helps settle arguments, and it tells you whether a page's author was precise/experienced vs. lazy/junior. But, perhaps surprisingly, it doesn't predict whether an “HTML5 compliant” web browser will do anything about a violation.[2]
The HTML5 standard notes this combo of strict rules + loose enforcement in several places. Look under <label for>
(emphasis added):
If [the
for
attribute] is specified, the attribute’s value must be the ID of a labelable element in the same Document as the label element. If the attribute is specified and there is an element in the Document whose ID is equal to the value of the for attribute, and the first such element is a labelable element, then that element is the label element's labeled control.
Which is a bit like saying:
By law, all secret agents must be assigned a unique code name. Now call in the first agent assigned the code name VORPALBLADE.
It’s strange-sounding (why not say “the element” instead of “the first element”?) but that's because the standard simultaneously makes the rules and dictates gentle fallback behaviors when rules are broken.
How multiple forms get tripped up by the unique ID rule, Part 1…
So it's up to HTML authors (including third-party JavaScript authors whose code injects HTML elements on the fly, as the Forms 2.0 library does) to enforce/expect the uniqueness of id
attributes.
Forms 2.0’s loadForm()
and newForm()
— which run automatically for embedded forms and named LP forms, respectively — expect a single <form>
tag with id="mktoForm_{formid}"
. As long as you use different forms, this part isn't a problem:
Some stuff
<form id="mktoForm_1234"></form>
Some other stuff
<form id="mktoForm_5678"></form>
Yet other stuff
<form id="mktoForm_10022"></form>
That’s valid HTML (id
s are unique) and Marketo won't have any trouble putting the forms in the right containers.
However, with the same form ID, you might try doing this:
Some stuff
<form id="mktoForm_1234"></form>
Some other stuff
<form id="mktoForm_1234"></form>
Yet other stuff
<form id="mktoForm_1234"></form>
This emphatically won't work, but not solely because it isn't valid HTML. Even with invalid HTML, as in my rambling above, the browser is forgiving. It will not only draw all three <form>
tags, it'll even let you select each one separately — if you use the right method to select.
document.querySelectorAll('#mktoForm_1234')
would return a collection of all three, or document.querySelectorAll('[id=mktoForm_1234]')
if that suits your fancy. And old-school document.forms
will include all of 'em as well.
But what you can't do is document.getElementById ('mktoForm_1234')
or its equivalent document.querySelector('#mktoForm_1234')
. These selectors will only ever return the first <form>
with that ID, because that's how getElementById
is defined.
Since the Marketo Forms library uses getElementById
, it continuously finds the same <form>
tag, and that's why you'll see a “stack” of forms all inside the first container on the page, instead of the form drawn in different containers.
(If the library used a different method to select form containers, this problem wouldn't exist. For example, at its most primitive, it could expect that there might be duplicate id
s despite that being bad HTML. Even better, it could select a given class
or data-
attribute and skip elements that already contain form fields.)
… and Part 2
You see the clear problem with the same form multiple times. But there's also a more elusive problem, also ID-related, that crops up even with multiple forms. (This is important because people may think they've worked around the first problem by cloning their form.)
This problem relates to the <label>
element.
See, it was no coincidence that I quoted the HTML5 HTMLLabelElement standard above. I'm going to let you interact with the problem for yourself.
Here are a couple of forms generated by the following HTML:
<form>
<label for="subscriptions_1">
Click this label...
</label>
<input type="checkbox" name="subscriptions" id="subscriptions_1">
</form>
<form>
<label for="subscriptions_1">
...and click this one!
</label>
<input type="checkbox" name="subscriptions" id="subscriptions_1">
</form>
Click the two labels. See the problem? Of course you do!
The cause: a label
's for
attribute is defined as referring to the first element with the corresponding ID. No matter how many forms you add, if any have <label for="subscriptions_1">
, clicking the label will check/uncheck the checkbox in the first form on the page.
This can get confusing quickly, since interactions between form elements are part of the user experience. May not seem like a fatal flaw, but consider what happens if someone thinks their click on a Subscribe box had no effect, then they later submit the first form on the page… not good. (I mean, yes, it's not the worst thing ever, but it is a bug.)
Note if you happen to have full control over the form this behavior is work-around-able. Check out these 2 forms:
<form>
<label onclick="this.parentNode.querySelector('#subscriptions_1').click()">
Click this label...
</label>
<input type="checkbox" name="subscriptions" id="subscriptions_1">
</form>
<form>
<label onclick="this.parentNode.querySelector('#subscriptions_1').click()">
...and click this one!
</label>
<input type="checkbox" name="subscriptions" id="subscriptions_1">
</form>
This is another way to select elements with non-unique IDs: “root” the querySelector
call to the parent <form>
element, meaning it will only search for children of that <form>
element that match #subscriptions_1
.
Of course this workaround eschews the native behavior of for
, so it's far less than ideal.
Get the fix
The fix is in the CodePen below. Don't worry about the CSS pane (I just color-coded the forms to prove it's working). You just need to follow the HTML structure and the JS code.
In the HTML, you'll see the forms do not have the id="mktoForm_123"
that you usually see, instead using class="mktoForm"
. And set the data-formId
attribute to the form ID. This is setup is important: don't add an id
attribute, since JS manages the IDs (they're still used, but are not static values).
You can add as many different instances of as many forms as you like.
<form class="mktoForm" data-formId="787" data-formInstance="one"></form>
<form class="mktoForm" data-formId="341" data-formInstance="one"></form>
<form class="mktoForm" data-formId="787" data-formInstance="two"></form>
Then in the JS, the only part you need to modify is the top mktoFormConfig
object:
var mktoFormConfig = {
podId : "//app-sj01.marketo.com",
munchkinId : "410-XOR-673",
formIds : [341, 787]
};
podId
and munchkinId
are self-explanatory, and formIds
is an array of numeric IDs (in any order).
Include a form ID just once in formIds
, even if you're drawing it multiple times — note how data-formId="787"
is on two different <form>
tags, but there's only one 787
in formIds
.
Why does the fix work, though?
If you step through the CodePen JS but you're unfamiliar with object-oriented programming (or handles/references/pointers in general) you might not understand why the above fix doesn't end up breaking more stuff. After all, if you change IDs, doesn't Marketo lose track of the forms and inputs?
The answer is that the library creates a JS object to manage the underlying HTML <form>
and its children, and the object maintains a set of references to the <input>
tags it injects into the form.
A reference doesn't stop working just because properties of the referenced object change (that's kind of the main idea of a reference).
Take a couple of checkboxes like this:
<input type="checkbox" id="interestVacuumCleaners" name="productInterest">
<input type="checkbox" id="interestWashingMachines" name="productInterest">
And this code:
var vacuumCleanersCheckbox = document.getElementById('interestVacuumCleaners');
vacuumCleanersCheckbox.id = 'interestFlamethrowers';
vacuumCleanersCheckbox.checked = true;
You'll see that the code checks the first checkbox, even though we changed the ID. That's because the variable vacuumCleanersCheckbox
is a reference to the checkbox element, and we created the reference before changing the element's ID.
If you inspected the page in Dev Tools, you'd see <input id="interestFlamethrowers">
as that change took effect immediately, but it doesn't break the earlier reference.
This behavior underscores why you must not set Marketo Form values using native DOM CSS selector methods nor jQuery wrappers for those same methods but should only use the Forms 2.0 setValues
method. setValues
uses already-stored references to form elements.
Notes
[1] There's actually a nerdy term for this design approach, called Postel's Law. If you've got extra time, Trevor Jim raises some convincing objections to the concept. Nevertheless, it remains the norm in consumer-oriented internet software.
[2] Contrast this with XML tools. XML and HTML are similarly structured, and XML places the same MUST restriction on the id
attribute. Yet if you put duplicate IDs in XML, it's nearly certain that the receiving software will completely reject your doc. XML (and even the looser-limbed JSON) validation is rigidly enforced by reader apps.
In fairness, XML and JSON are expressly for transmitting data, where the slightest break is destructive by definition. HTML typically contains a mixture of data and merely cosmetic tags/attributes/text. But it's pretty optimistic to assume that ignoring validation can't be destructive. After all, if you look at the <label>
issue above, if someone un/selects a checkbox without realizing it, eventually that mistake will be propagated to a back-end database.