GTM Solutions Corner #5: Tracking changes in pre-populated HTML form fields

This is part 5 (and my first contribution, so bear with me) to this “semi-regular” series, dedicated to reusable GTM solutions for scenarios or issues we’ve encountered more than once. Last time, we looked at how to capture existing classic Google Analytics calls with GTM, and this time we’ll be exploring how to track changes in pre-populated HTML form fields.

Why I did it

I was recently approached with a request from a client who wanted to know which fields were updated in a form that was part of an account renewal process. These fields would include primary/secondary email/postal addresses. Since this work would involve interactions with individual HTML form input elements, I drew inspiration from Simo Ahava’s blog post on Form Abandonment tracking and incorporated it as part of this solution. Let’s take a closer look at how it works, shall we?

Overview

The solution will be contained within a Custom HTML Tag that will be deployed using Google Tag Manager. The contents of this tag can be found here. You can choose to deploy the tag across the entire site, or specify a trigger to only fire the code on certain pages that require forms to be tracked.

Quick-start

We start by first defining the CSS selectors and form attributes we need to capture the necessary elements of the HTML form.

var formSelector = 'form';

The first variable ‘formSelector’ targets the entire form element – modify this to be more specific if you have more than one on the page (by default it will select the first form on the page).

var attribute = 'name';

Secondly, the ‘attribute’ variable is the name of the element attribute that is shared across all the or elements from within the form. In most forms the ‘name’ attribute is usually what is stored here, but this can be changed.

And that’s it. As long as the HTML forms are not set up in an overly-elaborate or inconsistent manner then this should be all that is required to be initialised to get this tag working.

The nitty gritty

For those interested, I’ll attempt to describe how the process works. So here goes. We create a new function and declare some variables (including the two mentioned in the previous section):

function formFieldUpdates() {
    var formSelector = 'form';
    var attribute = 'name';
    var inputSelector = formSelector + ' input[' + attribute + '], ' + formSelector + ' select[' + attribute + ']';
    var inputs = [];
    var initObj = {};
    var eventAction, i;

When the function begins…functioning… it grabs all of the input fields and pushes them into an array:

window.document.querySelectorAll(inputSelector).forEach(function (input) {
        inputs.push(input);
    });

Then for each input, we store current values of those elements in an object as key-value pairs e.g. ‘firstName’: ‘Victor’, ‘lastName’: ‘Sarker’:

for (var input of inputs) {
        initObj[input.getAttribute(attribute)] = input.value;
    }

Next, we create a fresh copy of that object to store the values of any changes to those input field values that may happen on the page:

var newObj = JSON.parse(JSON.stringify(initObj));

Now that we have our ‘initial’ and ‘new’ objects, let’s define a function that will compare the two and determine which keys have been changed:

// keep track of the form update history
    var updateHistory = [];

    // check the new object for updated property values
    var getUpdatedProps = function (newObj, initObj) {
        var updates = [];
        for (var key in newObj) {
            if (newObj[key] !== initObj[key]) {
                if (initObj[key] === '') { // addition
                    updates.push(key + ' added');
                } else if (newObj[key] === '') {
                    updates.push(key + ' removed'); // deletion
                } else {
                    updates.push(key + ' updated'); // updated
                }
            }
        }
        return updates.join(', ')
    }

This method will iterate over each key in the new object (each of our selected input form fields) and check against the initial object to see if the value has changed. If so, then it will check whether the field was empty in either the initial state or new state. This will determine whether an entry has been added or removed. If the values were not empty in both then the field has simply been updated. We keep track of each check by pushing the key and how it was changed into an array. The method then returns a list by joining these array elements together by forming a comma-separated list.

With everything in place, all that needs to happen is add the event listeners. There is one that will listen out for changes in the form fields:

window.document.querySelector(formSelector).addEventListener('change', function (e) {
        updateHistory.push(e.target.getAttribute(attribute));
        newObj[e.target.getAttribute(attribute)] = e.target.value;
    });

There is one that will push a data layer event when the form is submitted. The event will only be pushed if there has been at least one change in the fields:

window.document.querySelector(formSelector).addEventListener('submit', function () {
        eventAction = getUpdatedProps(newObj, initObj);
        if (updateHistory.length && (newObj !== initObj)) {
            window.dataLayer.push({
                event: 'formFieldsUpdated',
                eventCategory: 'Form Fields Updated',
                eventAction: eventAction
            })
        }
    });

Finally, since it would be a shame not to include it, we add the ‘beforeunload’ listener used in Simo’s form abandonment post so that if the form is not completed, an event is pushed to the data layer that describes which fields were interacted with.

var i;
var checkSubmit = function () {
        i = window.dataLayer.length - 1;
        while (i > -1) {
            if (window.dataLayer[i].event === 'gtm.formSubmit') {
                return true;
            }
            i--;
        }
    };

    window.addEventListener('beforeunload', function () {
        if (!checkSubmit()) {
            window.dataLayer.push({
                event: 'formAbandonment',
                eventCategory: 'Form Abandonment',
                eventAction: updateHistory.join(' > ')
            });
        }
    });

And there you have it. Enjoy.

Share:
Written by

Subscribe to our newsletter: