Form Validation – Part 4: Validating the MailChimp Subscribe Form

Avatar of Chris Ferdinandi
Chris Ferdinandi on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

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:

  1. Constraint Validation in HTML
  2. The Constraint Validation API in JavaScript
  3. A Validity State API Polyfill
  4. 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.

  1. We can remove the div#mc_embed_signup wrapper from around the form.
  2. Similarly, we can remove the div#mc_embed_signup_scroll wrapper around the fields inside the form.
  3. We can also remove the text informing visitors that “* indicates required.”
  4. Let’s remove the .mc-field-group classes from around our form fields, and the empty class attributes on the fields themselves.
  5. We should also remove the .required and .email classes from our email field, since they were only used as hooks for MailChimp validation script.
  6. I went ahead and removed the * from the email label. It’s totally up to you how you want to label required fields, though.
  7. We can delete the div#mce-responses container, which is only used by the MailChimp JavaScript file.
  8. We can also remove the .clear class from the div around the submit button.
  9. Let’s remove all of the empty value attributes.
  10. Finally, we should remove the novalidate attribute from the form 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.

Subscribe to our mailing list

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:

  1. Constraint Validation in HTML
  2. The Constraint Validation API in JavaScript
  3. A Validity State API Polyfill
  4. Validating the MailChimp Subscribe Form (You are here!)