Over the last few articles in this series, we’ve learned how to use a handful of input types and validation attributes to natively validate forms.
We’ve learned how to use the Constraint Validation API to enhance the native browser validation process for a better overall user experience. And we wrote a polyfill to extend support all the way back to IE9 (and plug a few feature holes in some newer versions).
Now, let’s take what we’ve learned and apply it to a real example: the MailChimp signup form.
Article Series:
- Constraint Validation in HTML
- The Constraint Validation API in JavaScript
- A Validity State API Polyfill
- Validating the MailChimp Subscribe Form (You are here!)
A simple form with a large footprint
When you embed a MailChimp signup form on your site, it comes with a JavaScript validation script named `mc-validate.js`.
This file is 140kb (minified), and includes the entire jQuery library, two third-party plugins, and some custom MailChimp code. We can better!
Removing the bloat
First, let’s grab a MailChimp form without any of the bloat.
In MailChimp, where you get the code for your embeddable form, click on the tab labelled “Naked.” This version includes none of the MailChimp CSS or JavaScript.

<div id="mc_embed_signup">
<form action="//us1.list-manage.com/subscribe/post?u=12345abcdef&id=abc123" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<div id="mc_embed_signup_scroll">
<h2>Subscribe to our mailing list</h2>
<div class="indicates-required"><span class="asterisk">*</span> indicates required</div>
<div class="mc-field-group">
<label for="mce-FNAME">First Name </label>
<input type="text" value="" name="FNAME" class="" id="mce-FNAME">
</div>
<div class="mc-field-group">
<label for="mce-EMAIL">Email Address <span class="asterisk">*</span></label>
<input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL">
</div>
<div id="mce-responses" class="clear">
<div class="response" id="mce-error-response" style="display:none"></div>
<div class="response" id="mce-success-response" style="display:none"></div>
</div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_f2d244c0df42a0431bd08ddea_aeaa9dd034" tabindex="-1" value=""></div>
<div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
</div>
</form>
</div>
This is better, but it still includes some markup we don’t need. Let’s trim this down as much as possible.
- We can remove the
div#mc_embed_signup
wrapper from around the form. - Similarly, we can remove the
div#mc_embed_signup_scroll
wrapper around the fields inside the form. - We can also remove the text informing visitors that “* indicates required.”
- Let’s remove the
.mc-field-group
classes from around our form fields, and the emptyclass
attributes on the fields themselves. - We should also remove the
.required
and.email
classes from our email field, since they were only used as hooks for MailChimp validation script. - I went ahead and removed the
*
from the email label. It’s totally up to you how you want to label required fields, though. - We can delete the
div#mce-responses
container, which is only used by the MailChimp JavaScript file. - We can also remove the
.clear
class from thediv
around the submit button. - Let’s remove all of the empty
value
attributes. - Finally, we should remove the
novalidate
attribute from theform
element. We’ll let our script add that for us when it loads.
All of this leaves us with a much more clean and modest looking form. Since the MailChimp CSS is removed, it will inherit your site’s default form styles.
<form action="//us1.list-manage.com/subscribe/post?u=12345abcdef&id=abc123" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank">
<h2>Subscribe to our mailing list</h2>
<div>
<label for="mce-FNAME">First Name</label>
<input type="text" name="FNAME" id="mce-FNAME">
</div>
<div>
<label for="mce-EMAIL">Email Address</label>
<input type="email" name="EMAIL" id="mce-EMAIL">
</div>
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_f2d244c0df42a0431bd08ddea_aeaa9dd034" tabindex="-1" value=""></div>
<div><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
</form>
Adding Constraint Validation
Now, let’s add in a few input types and validation attributes so that the browser can natively validate the form for us.
The type
for the email field is already set to email
, which is great. Let’s also add the required
attribute, and a pattern
to force emails to include a TLD (the .com
part of an address). We should also include a title
letting people know they have to have a TLD.
Enhancing with the Constraint Validation API
This is a great starting point, but we can enhance the user experience by adding the form validation script we wrote earlier in this series.
Our validation script is just 6.7kb before minification, making it 20x smaller than the one MailChimp provides. If we want to ensure support back to IE9, though, we should include our Validity State polyfill and Eli Grey’s classList.js polyfill.
That brings our total file size up to 15.5kb unminified—still 9× smaller than the MailChimp validation script.
Submitting the form with Ajax
The mc-validate.js
script provided by MailChimp doesn’t just validate the form. It also submits it with Ajax and displays a status message.
When you click submit on our modified form, it redirects the visitor to the MailChimp site. That’s a totally valid way to do things.
But, we can also recreate MailChimp’s Ajax form submission without jQuery for a better user experience.
The first thing we want to do is prevent the form from submitting via a page reload like it normally would. In our submit
event listener, we’re calling event.preventDefault
if there are errors. Instead, let’s call it no matter what.
// Check all fields on submit
document.addEventListener('submit', function (event) {
// Only run on forms flagged for validation
if (!event.target.classList.contains('validate')) return;
// Prevent form from submitting
event.preventDefault();
...
}, false);
Using JSONP
The mc-validate.js
script uses JSONP to get around cross-domain security errors.
JSONP works by loading the returned data as a script element in the document
, which then passes that data into a callback function that does all of the heavy lifting.
Setting up our Submit URL
First, let’s set up a function we can run when our form is ready to be submitted, and call it in our submit
event listener.
// Submit the form
var submitMailChimpForm = function (form) {
// Code goes here...
};
// Check all fields on submit
document.addEventListener('submit', function (event) {
...
// Otherwise, let the form submit normally
// You could also bolt in an Ajax form submit process here
submitMailChimpForm(event.target);
}, false);
The first thing we need to do is get the URL from the form’s action
attribute.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
};
In the `mc-validate.js` script, the /post?u='
in the URL is replaced with /post-json?u=
. We can do that quite easily with the replace()
method.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
};
Serializing our form data
Next, we want to grab all of the form field data and create a query string of key/value pairs from it. For example, FNAME=Freddie%20Chimp&[email protected]
.
Let’s create another function to handle this for us.
// Serialize the form data into a query string
var serialize = function (form) {
// Code goes here...
};
Now, we want to loop through all of our form fields and create key/value pairs. I’ll be building off of the work done by Simon Steinberger for this.
First, we’ll create a serialized
variable set as an empty string.
// Serialize the form data into a query string
// Forked and modified from https://stackoverflow.com/a/30153391/1293256
var serialize = function (form) {
// Setup our serialized data
var serialized = '';
};
Now let’s grab all of the fields in our form using form.elements
and loop through them.
If the field doesn’t have a name, is a submit or button, is disabled, or a file or reset input, we’ll skip it.
If it’s not a checkbox
or radio
(a nice catchall for select
, textarea
, and the various input
types) or it is and it’s checked, we’ll convert it to a key/value pair, add an &
at the beginning, and append it to our serialized
string. We’ll also make sure to encode the key and value for use in a URL.
Finally, we’ll return the serialized string.
// Serialize the form data into a query string
// Forked and modified from https://stackoverflow.com/a/30153391/1293256
var serialize = function (form) {
// Setup our serialized data
var serialized = '';
// Loop through each field in the form
for (i = 0; i < form.elements.length; i++) {
var field = form.elements[i];
// Don't serialize fields without a name, submits, buttons, file and reset inputs, and disabled fields
if (!field.name || field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') continue;
// Convert field data to a query string
if ((field.type !== 'checkbox' && field.type !== 'radio') || field.checked) {
serialized += '&' + encodeURIComponent(field.name) + "=" + encodeURIComponent(field.value);
}
}
return serialized;
};
Now that we have our serialized form data, we can add it to our URL.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
url += serialize(form);
};
Adding a callback
A key part of how JSONP works is the callback.
Traditional Ajax requests return data back to you. JSONP instead passes data into a callback function. This function has to be global (as in, attached to the window
rather than inside of another function).
Let’s create a callback function, and log the returned data in the console so that we can see what MailChimp sends back.
// Display the form status
var displayMailChimpStatus = function (data) {
console.log(data);
};
Now we can add this callback to our URL. Most JSONP use callback
as the query string key for this, but MailChimp uses c
.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
url += serialize(form) + '&c=displayMailChimpStatus';
};
Injecting our script into the DOM
Now we’re ready to inject our script into the DOM. First, we’ll create a new script element and assign our URL as it’s src
.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
url += serialize(form) + '&c=displayMailChimpStatus';
// Create script with url and callback (if specified)
var script = window.document.createElement( 'script' );
script.src = url;
};
Next, we’ll grab the first <script>
element we find in the DOM, and inject our new one just before it using the insertBefore()
method.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
url += serialize(form) + '&c=displayMailChimpStatus';
// Create script with url and callback (if specified)
var script = window.document.createElement( 'script' );
script.src = url;
// Insert script tag into the DOM (append to <head>)
var ref = window.document.getElementsByTagName( 'script' )[ 0 ];
ref.parentNode.insertBefore( script, ref );
};
Finally, we’ll remove it from the DOM after our script loads successfully.
// Submit the form
var submitMailChimpForm = function (form) {
// Get the Submit URL
var url = form.getAttribute('action');
url = url.replace('/post?u=', '/post-json?u=');
url += serialize(form) + '&c=displayMailChimpStatus';
// Create script with url and callback (if specified)
var script = window.document.createElement( 'script' );
script.src = url;
// Insert script tag into the DOM (append to <head>)
var ref = window.document.getElementsByTagName( 'script' )[ 0 ];
ref.parentNode.insertBefore( script, ref );
// After the script is loaded (and executed), remove it
script.onload = function () {
this.remove();
};
};
Processing the submit response
Right now, our callback method is just logging whatever MailChimp responds with into the console.
// Display the form status
var displayMailChimpStatus = function (data) {
console.log(data);
};
If you look at the returned data, it’s a JSON object with two keys: result
and msg
. The result
value is either error
or success
, and the msg
value is a short string explaining the result
.
{
msg: '[email protected] is already subscribed to list Bananas Are Awesome. Click here to update your profile.'
result: 'error'
}
// Or...
{
msg: 'Almost finished... We need to confirm your email address. To complete the subscription process, please click the link in the email we just sent you.'
result: 'success'
}
We should check to make sure our returned data has both of these keys. Otherwise, we’ll throw a JavaScript error when we go to use them.
// Display the form status
var displayMailChimpStatus = function (data) {
// Make sure the data is in the right format
if (!data.result || !data.msg ) return;
};
Display a status message
Let’s add a <div>
to our form, just before the submit button, that we’ll use to add our error or success message. We’ll give it a class of .mc-status
.
<form action="//us1.list-manage.com/subscribe/post?u=12345abcdef&id=abc123" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank">
/* ... */
<div class="mc-status"></div>
<div><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
</form>
In our displayMailChimpStatus()
function, we want to find the .mc-status
container and add our msg
to it.
// Display the form status
var displayMailChimpStatus = function (data) {
// Get the status message content area
var mcStatus = document.querySelector('.mc-status');
if (!mcStatus) return;
// Update our status message
mcStatus.innerHTML = data.msg;
};
We can style the message differently depending on whether the submission was successful or not.
We already have some styles set up for our error messages with the .error-message
, so let’s reuse those. We’ll create a new class, .success-message
, for successful submissions.
.success-message {
color: green;
font-style: italic;
margin-bottom: 1em;
}
Now, we can conditionally add one of our classes (and remove the other) based on the result
.
// Display the form status
var displayMailChimpStatus = function (data) {
// Get the status message content area
var mcStatus = document.querySelector('.mc-status');
if (!mcStatus) return;
// Update our status message
mcStatus.innerHTML = data.msg;
// If error, add error class
if (data.result === 'error') {
mcStatus.classList.remove('success-message');
mcStatus.classList.add('error-message');
return;
}
// Otherwise, add success class
mcStatus.classList.remove('error-message');
mcStatus.classList.add('success-message');
};
An important accessibility improvement
While our message will be easily spotted by sighted users, people using assistive technology like screen readers may not inherently know a message has been added to the DOM.
We’ll use JavaScript to bring our message into focus. In order to do so, we’ll also need to add a tabindex
of -1
, as <div>
elements are not naturally focusable.
// Display the form status
var displayMailChimpStatus = function (data) {
// Get the status message content area
var mcStatus = document.querySelector('.mc-status');
if (!mcStatus) return;
// Update our status message
mcStatus.innerHTML = data.msg;
// Bring our status message into focus
mcStatus.setAttribute('tabindex', '-1');
mcStatus.focus();
// If error, add error class
if (data.result === 'error') {
mcStatus.classList.remove('success-message');
mcStatus.classList.add('error-message');
return;
}
// Otherwise, add success class
mcStatus.classList.remove('error-message');
mcStatus.classList.add('success-message');
};
There’s a good chance this will add a blue outline to our status message. This is a really important accessibility feature for links, buttons, and other naturally focusable content areas, but it’s not necessary for our message. We can remove it with a little CSS.
.mc-status:focus {
outline: none;
}
The end result
We now have a lightweight, dependency-free script that validates our MailChimp form and submits it asynchronously.
Our completed script weighs 19kb unminified. When minified, the script weighs just 9kb. That’s 15.5× smaller than the version MailChimp provides.
Not bad!
Article Series:
- Constraint Validation in HTML
- The Constraint Validation API in JavaScript
- A Validity State API Polyfill
- Validating the MailChimp Subscribe Form (You are here!)
You have a problem with your regular expression there, it passes email addresses that are longer than the email address format specification allows (64 characters for local part, and 255 in total including the domain).
Of course, that regex is nice an maintainable… or not. This is why regular expressions for things like this is not recommended. As Jamie Zawinski famously said:
‘Some people, when confronted with a problem, think
“I know, I’ll use regular expressions.” Now they have two problems.’
This will be useful for a lot of people who use mailchimp forms and don’t want the extra fluff. Thanks for writing this!
Thank you for this great blog. Very useful as I was about to star looking into optimisation of the Mailchimp form.
A question regarding the HTML code.
Shouldn’t the name of the hidden input field be the same as the combination of the parameters ‘u’ and ‘id’ from the post url?