Google Tag Manager: DOM Listener

ShareShare on Google+19Tweet about this on Twitter10Share on LinkedIn6Share on Facebook13

In this post, I’ll walk you through a tutorial on how to create a Google Tag Manager extension. This extension will be a new listener, whose sole job is to listen for changes in the HTML and CSS markup on the page.

The reason I’m teaching you how to create a DOM listener is simple. Ever so often I come across people asking for help, but they are unable to touch on-page code. Usually the problem is magnified with form handlers, since the developers might have installed some custom jQuery form manager, for instance, which simply refuses to cooperate with GTM’s form listeners. That is why you might want to fire a GTM event when a certain message pops up on the screen, for example.

With a DOM listener, you can fire a GTM event every time something changes on the page. Well, not every time. Actually, in this example you’re restricted to element insertion and attribute changes. A working use case might be form validation: if you want to track invalid forms, maybe by sending the content of the validation error message with an event, you might just as well create a DOM listener. This listener will then trigger when an error message appears on the page.

DISCLAIMER: To be truthful, I feel quite strongly about using hacks such as this to fix faulty markup or an otherwise shoddy tag implementation. The main idea behind this post is to introduce a feature of JavaScript which can alleviate some of your tag management pains. However, if you find that you need hacks such as the DOM listener to circumvent development problems on your website, I would strongly suggest that you take this up with your developers and try to come up with a solution which works with GTM’s standard features.

The premise

To create the DOM listener, I will leverage an API known as MutationObserver. This little piece of magic will create an observer object, which triggers every time a mutation takes place. This mutation can come in different sizes and shapes, but for the purposes of this guide, I will listen for two kinds of mutations, for two kinds of use cases:

  1. Node insertion – when a new <span class=”error”> is injected in the DOM
  2. CSS style change – when a previously hidden <span class=”error”> is displayed

So the first use case is when your form injects a new SPAN element into the DOM upon an error. The script will check if the injected node is really the error message, and if it is, it pushes a dataLayer event with the content of the SPAN (the message itself).

The second use case is when an invalid form causes a hidden SPAN to appear, with the error message within.

Listening for node insertion is a bit different than listening to an attribute change. A node insertion listener can be primed on any node on the DOM, meaning you have much more to work with in terms of flexibility. Listening for attribute changes requires you to pinpoint exactly which node you want to observe, and the attribute change will be reported for that node only.

Preparations

Here are the ingredients:

  1. A page where a <span class=”error”> is either inserted or revealed with a CSS style change
  2. Custom HTML tag(s)

My test page code looks like this (I combined both use cases into one here):

<script>
function addHTML() {
  var myDiv = document.getElementById("contents");
  var newSpan = document.createElement("span");
  newSpan.className = "error";
  newSpan.innerHTML = "This form contained errors";
  myDiv.appendChild(newSpan);
}
function changeCSS() {
  var mySpan = document.getElementsByClassName("error")[0];
  mySpan.style.display = "block";
}
</script>
<a href="#" onClick="addHTML();">Add span with .innerHTML</a>
<a href="#" onClick="changeCSS();">Add span with CSS</a>
<div id="contents">
  <span class="error" style="display: none;">This form contained errors</span>
</div>

Let’s just quickly go over this page.

First, you have two functions. The function addHTML() will insert the following in the DIV#contents below:

<span class=”error”>This form contained errors</span>

The insertion is done upon clicking a link whose text is “Add span with .innerHTML”.

In the second function, the SPAN.error is already in the DOM, but it’s been hidden with a display: none CSS command. When the link labelled “Add span with CSS” is clicked, this style directive will be changed to display: block.
Simple test page
This is just my test page. Obviously you’ll need to navigate around your current implementation to make things work.

Finally, you’ll need your custom HTML magic. The next two chapters will go over the tags and the code you’ll need to write for them.

Case 1: node insertion

In this use case, a new node (<span class=”error”>) is inserted into a DIV on the page. The markup on the page looks something like this:

<script>
function addHTML() {
  var myDiv = document.getElementById("contents");
  var newSpan = document.createElement("span");
  newSpan.className = "error";
  newSpan.innerHTML = "This form contained errors";
  myDiv.appendChild(newSpan);
}
</script>
<a href="#" onClick="addHTML();">Add span with .innerHTML</a>
<div id="contents">
</div>

So as you can see, there’s an empty DIV (#contents), which is then appended with a new SPAN, created by the function addHTML() which, in turn, waits for a click event on the link on the page.

Now, the next step is to create the listener itself. You’ll need to use the MutationObserver API to listen for any node which is inserted into the observed target. The node itself can be on any level in the DOM hierarchy, but I chose the DIV#contents to keep things simple. When a node is inserted, a dataLayer push is done with a new GTM event and the text content of the SPAN.

Here’s what you Custom HTML Tag should look like:

<script>
  var contentDiv = document.querySelector("#contents");
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if(mutation.type==="childList" && mutation.addedNodes[0].className==="error") {
        dataLayer.push({'event': 'newErrorSpan', 'spanErrorMessage': mutation.addedNodes[0].innerHTML});
        observer.disconnect();
      }
    });    
  });
  var config = { attributes: true, childList: true, characterData: true }
  observer.observe(contentDiv, config);
</script>

OK, OK, lots of stuff going on here. Let’s go through the script and see what it does. First, you create a reference to the DIV you will be listening to. I’m using the querySelector() function, because it’s a nice way to combine CSS selectors and JavaScript.

Next, you create the MutationObserver itself by first tackling a known cross-browser issue with WebKit browsers. The observer listens for mutations of type childList (a new child node is inserted) and checks if the first added node has CSS class “error”. You’ll have to modify this code if the SPAN with the error is not the first node that your script inserts into the DOM.

If such a mutation is detected, a GTM event is pushed into dataLayer (newErrorSpan), and the error message contents are sent as a data layer variable as well. Note that I use innerHTML to get the contents of the SPAN. If your SPAN has HTML formatting within, you might want to use innerText instead.

The disconnection means that after this particular mutation takes place, no further mutations are listened for. So if someone keeps on pushing the “submit” button, the observer will shut down after the first error. You might want to remove this line if you want to track ALL the potential errors your visitor propagates on the form.

Lastly, I create a configuration for the MutationObserver and prime it on the DIV.

And that’s it for node insertion. If you set this new listener to fire on pages where the SPAN with class “error” is created in a DIV with ID “contents”, a dataLayer.push() will take place every time the SPAN is inserted into the DOM. Try it for yourself!
DataLayer error message

Case 2: CSS change

In this case, you have a hidden SPAN with the error message, which is then revealed when the form validation fails. Here’s what the HTML might look like:

function changeCSS() {
  var mySpan = document.querySelector(".error");
  mySpan.style.display = "block";
}
</script>
<a href="#" onClick="changeCSS();">Add span with CSS</a>
<div id="contents">
  <span class="error" style="display: none;">This form contained errors</span>
</div>

So I have a simple SPAN within a DIV with the error message. This is initially set to display: none, but when the link is clicked, the display status is changed to block.

As for your Custom HTML Tag, you’ll need something like this:

<script>
  var spanError = document.querySelector('.error');
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.type==="attributes" && mutation.target.style.display==="block") {
        dataLayer.push({'error': 'modErrorSpan', 'spanErrorMessage': mutation.target.innerHTML});
        observer.disconnect();
      }
    });    
  });
  var config = { attributes: true, childList: true, characterData: true }
  observer.observe(spanError, config);
</script>

It’s pretty similar to the previous one but with one or two small changes. First, you’re not listening to the DIV, you’re listening to the actual node you know will be the target of the style change. This is important, and it means that you have to know exactly what the target will be when creating this script.

Next, in the observer itself, you’ll need to specify just what the style change was. I used simply a change from display: none to display: block, but you might have something different in your code. So don’t forget to change the content of the if-clause to match what the new style is.

The benefit here is that you’re listening to just one single node, so there’s a performance boost. I’ve got the observer.disconnect(); again, but you might want to remove that if you want to send events perpetually for each invalid click of the submit button.

Don’t forget to test.
Span with error and dataLayer

Conclusions

This might seem like a cool hack to you. After all, you’re listening for changes on the page without actually any page refresh happening! Also, you’re extending GTM’s default listeners so you’re kind of like a Google engineer, right?

Well, remember what I said in the disclaimer of this text. This is a hack, a circumvention, a band-aid, designed to overcome problems with your markup or your JavaScript. Having a custom form handler which doesn’t propagate a proper form submit event (which is required by GTM’s default form submit listener) is a bit suspect and reeks of bad practices. So before you resort to this DOM listener, make sure you exhaust all other, more orthodox possibilities with your developers.

Then again, you might not need this to overcome any development problems, but rather to complement your current tag setup. In that case, go crazy! It’s an excellent way to add more flexibility to your tags. Do note the cross-browser support, however. Support is not comprehensive enough to warrant using this listener as a firing rule for some business-critical tag.

ShareShare on Google+19Tweet about this on Twitter10Share on LinkedIn6Share on Facebook13

Comments

  1. Bjorn says

    Great article again!

    Is it no way get an event when a css property change, like this one. Without adding code?

    I feel like when you need to add code, the purpose of Tag Manager disappears. Especially since it much simpler just add one line of old analytics code, compared to setup all rules and macros to catch variables and events.

    • says

      Hey Bjorn,

      Thanks. If you mean without adding code in GTM or on-page, then no, not at the moment. Because there is no page refresh or traditional user-initiated action (such as click, mousedown, etc.), you will need to observe changes using a Mutation Observer. And that requires code. Perhaps at some point Google will add a Mutation Observer as one of the default listeners, but considering how much of a hack it is, it’s not something I’m holding my breath for.

      If writing code is the deal-breaker for your TMS use then you have very high standards indeed :) In my opinion, a TMS isn’t just a plug-and-play marketers’ tool, but a full blown developer platform as well. At the same time, a TMS can’t be used as an excuse to have uncooperative markup or conflicting scripts on the page, and then use the TMS to circumvent these problems. Most of the stuff I write in my blog posts has to do with problems with page markup and the “hacks” required to make them work with Google Analytics. This post is a prime example of such a scenario. Personally, I’m a bit concerned every time I publish a post like this, thinking “am I encouraging people to use GTM to band-aid their on-page markup / JavaScript problems?”.

      The issues highlighted in this article are just that: markup which does not comply with what modern web traffic tracking would require. For tagging to be efficient, you’ll need DOM elements with clearly distinguishable attributes such as IDs and CLASSes, your form validators should not stop propagation of events (e.g. with a return false;), your AJAX navigation should make the appropriate browser state changes so that these can be listened to, etc.

      Depending on your process with your website developers, sure, inline tracking might be the best way to proceed for you. However, the TMS shines in its centralized approach to tagging. Having all your eggs in one basket is just so refreshing in terms of tag maintenance, upgrades, shared variables and so forth. And yes, using a TMS to hack your way through the obstacles of a crappy CMS or horrible website markup is one of the perks, and you can even use the TMS to proof-of-concept a website redesign (“See how easily this could be fixed with just a little JavaScript?”).

      If the markup has been created intelligently and with the requirements of good web traffic tracking in mind (as all sites should be), you might indeed get by without having to write a single line of Custom HTML or Custom JavaScript code. Also, I’m sure Google engineers do their best to make using Custom HTML and Custom JS as obsolete as possible, by constantly updating macro and tag templates.

      • Bjorn says

        Thanks for a very nice reply!

        Well, i am and running with GTM now. I tag our sit, and almost everything is done within GTM. With the exception I must introduce clear id’s or classes here and there. You can do remarkably much with the rules and macros tools you have.

        There was a nivoSlider, where i had to push an event in the code, to record clicks on the images. But thats the only case. I could have detected it by checking the CSS proper display, if GTM had support for that. I think they should build support for this in general.

        Overall, I like GTM by now. It was really hard understand what it was first, and this is because of Google s poor documentation. They really can’t write.

        Code is in two places somehow, but on the other hand, its code that really don’t belong to the website, but to analytics.

        We keep in touch! I will help a friend implement it now in his website :-)
        Bjorn

  2. says

    Hi Simo,
    I am amazed with all your posts! They have been helping me a lot!
    I am your fan! :)

    My question here is about this DOM Listener, There’s an overlay when donating that we need to track in order to get CTR of this element.

    I have implemented everything:
    *I create a custom HTML tag into GTM
    *I adapted the Javascript function to
    -listen when my element attribute changes to block
    -write the appropriate key-value pair to be sent to the dataLayer
    *Firing Rule I associate to it is: all pages

    The issue is that only loading the page it results on this ugly javascript error:

    “Uncaught NotFoundError: Failed to execute ‘observe’ on ‘MutationObserver': The provided node was null.”

    Should I have to load any JQuery library, plugin, whatever.. before?
    I would appreciate very much your help.
    Thanks and congrats for your great work!

    Sandra

    • says

      Hi Sandra!

      That error occurs when you try to listen to a node which doesn’t exist.

      From what I’m seeing, you’re trying to pass a DIV with a class name to the listener, when you should be passing a DIV with an ID.

      So instead of document.querySelector(‘.lb-overlay-donate’)… you need document.querySelector(‘#lb-overlay-donate’)

      Also, make sure that the listener fires only on pages with #lb-overlay-donate present in the template, or else you’ll get the error every single time. Or, you can have it fire on all pages, but then you’ll need to wrap the Custom HTML Tag code in a try…catch block to prevent errors from occurring.

      • says

        Hi Simo,

        Yes! of course!
        I mixed up class with id selectors.

        It’s already changed and working properly.

        Many thanks!

  3. says

    Hi Simo,

    In wordpress how can I record the label of the link in the menu?
    I would like to set the label: “top menu : about link”

    There is no way I can set a class on the A tag, only on the LI.

    Thank you,

    Sam

    • says

      If the link is a direct descendant of the LI (i.e. <li class=”menu”><a href=”/about-us/”>About</a></li>), then you can use the parent element to identify the clicked link. You’ll need to create a new Data Layer Variable Macro which refers to gtm.element.parentElement.className and use this as the firing rule, e.g.

      {{event}} equals gtm.linkClick
      {{element parent class}} contains top-menu

      This will fire the event when a link is clicked and the direct parent has a class “top-menu”. To get the label of the link, you can use the {{element text}} macro (if it doesn’t exist, create a new Auto-Event Variable macro of type Text).

      • says

        Thank you very much for your reply.

        One more question, if I have another tag that listen to a link click, should I block the other event by using a rule “parent node does not contain top-menu class”?
        This way I wont have a multiple event on a top-menu click.

        Thank you,

        Sam

      • says

        If the other tag listens to all link clicks then yes, you should block the parent class rule.

        You’re welcome!

  4. Nastasia says

    Hi Simo,

    I’m applying the 2nd case (CSS change) and I see that the tags fires successfully but 4 times instead of one. I’ve thought that it would be because there are more than one hidden divs showed. How can I restrict the listener to make the dataLayer.push only when a div with a specific ID/class is displayed?

    Thanks,
    Nastasia

    • says

      You specify it in the var spanError = document.querySelector('.error');. If you want to listen to an element with a specific ID, it would be var spanError = document.getElementById('error'); where error is the ID value of the node.

      If it’s still firing 4 times, then there’s something else wrong :)

  5. says

    Hey, anyone know a solution to track a Image Slider on a tablet by swiping the images? thanks a lot – Greetings from Hamburg (Germany) ;)

    • says

      You could set up a custom event listener for touchmove, or you could use a jQuery plugin which captures the touch + slide event.

      A custom event listener would be something like the following in a Custom HTML Tag:

      <script>
      var el = document.getElementById(‘slider’);
      if (el !== null) {
      el.addEventListener(‘touchmove’, function() { dataLayer.push({‘event’ : ‘slideEvent’}); });
      }
      </script>

      This pushes ‘event’ : ‘slideEvent’ into dataLayer whenever the touchmove event is fired. But a jQuery plugin would be better since it takes into account cross-browser and cross-device issues.

  6. Stefano says

    Hi Simo,
    I have to track a form submission with GTM but Form Submission Listener seems not to cooperate.
    I tried with Click Listener and it’s works fine but now i’m to set a rule to collect a GA event only when the form is successfully submitted.
    How can i use the thank you message that loads after the form submission? The page is: http://www.hotelvalentinoterni.it/offerte-e-promozioni.html.

    Thanks,
    Stefano

Leave a Reply

Your email address will not be published. Required fields are marked *

Please do not write HTML or other formatted code in your comments!