Node Relationships And GTM
There’s a much easier, native-to-GTM way to do this now: the Matches CSS Selector.
Behind this tragically boring title is a simple solution to many problems with Google Tag Manager’s auto-event tracking. The common denominator to these problems is poor website markup. Selectors are used sparingly, and element hierarchy is messy. This disregard for proper node relationships means you have to resort to Data Layer Variable Macros which look like
gtm.element.parentElement.parentElement.id
to identify the element that was the target of the event. In other words, there’s no robust, unique identifier with which you could identify the targeted element, so you have to go either up or down the Document Object Model to find something you can latch on to.
This is not a stable solution.
The more steps there are in your selector chain, the more chances there are of you either miscalculating the required steps, or some code on some page template producing a hierarchy which you didn’t account for, resulting in a loss of hits.
In this guide I introduce a pretty simple solution. It explores the concept of node relationships by using a simple DOM function to do so. The whole thing is still a workaround, and you should definitely try to talk with your developers about how to mark up your elements with proper identifiers.
(By the way, for markup ideas, here are two excellent articles about using data attributes as identifiers:
Tracking Clicks Using Custom Data Attributes - Bounteous
Google Tag Manager Events Using HTML5 Data Attributes - SwellPath)
Anyway, this guide will demonstrate a pretty simple DOM (Document Object Model) function with which to check whether a given element (e.g. {{element}}) is an ancestor of some other element (e.g. a top-level menu wrapper). The rule will look something like this:
As usual, there’s the easy method and then there’s the advanced method (which isn’t really that advanced at all).
XThe Simmer Newsletter
Subscribe to the Simmer newsletter to get the latest news and content from Simo Ahava into your email inbox!
The easy method
The easy method is really just about checking the node relationship between a given element, such as {{element}}, and a specific parent element. This is useful if you only have one or two cases where this is an issue.
The DOM method we’ll be using is Node.contains.
Here’s the Custom JavaScript Macro, which is the motor of the solution, in all its simplicity:
{{node contains element}}
function() {
var controlElement = document.querySelector('#parentId');
return controlElement && controlElement !== {{element}} ? controlElement.contains({{element}}) : false;
}
And then your firing rule will be something like:
{{node contains element}} equals true
{{event}} equals gtm.linkClick
Feel free to use any other auto-event tracking type you wish.
This will only fire the tag if a click occurs on a link which is a direct ancestor of an element with ID parentId. A cousin or a long-lost blood brother will not do; the nodes must have a direct ancestral relationship.
So that was easy, right? What about when you have multiple ancestor-descendant-relationships you want to explore? You could create a rule and macro for each, but a far more robust way is to use a Lookup Table or a Custom JavaScript macro.
The advanced method
The idea with the advanced method is to vary the the node against which you control the event element, depending on some parameter. In this particular example I use {{url path}} to determine which control node to choose, and a Custom JavaScript macro to return the new query selector.
So let’s say I have three page templates:
-
Home page (url path === ‘/'), where I want to track clicks on links in the main navigation (id === ‘#mainNav’)
-
Product pages (url path === ‘/products/*'), where I want to track clicks in any of the three Call-to-action page elements (class === ‘call-to-action’)
-
All other pages, where I want to track clicks on links in the footer (tag name === ‘footer’)
Now, to complement the Custom JavaScript macro above, I want the query selector to be dynamic, depending on which of the above conditions is true. So let’s modify the macro just a little bit:
function() {
var controlElement = document.querySelector({{get query selector}});
return controlElement && controlElement !== {{element}} ? controlElement.contains({{element}}) : false;
}
So it’s the same, but the document.querySelector()
fetches the query string from a macro called {{get query selector}}.
And what does this macro look like? Well, to satisfy the three requirements I listed above, this is what you’ll get:
function() {
var homeRegex = /^\/$/,
productRegex = /^\/products\//;
if(homeRegex.test({{url path}})) {
return '#mainNav';
} else if (productRegex.test({{url path}})) {
return '.call-to-action';
}
return 'footer';
}
This returns a different string every time, depending on which condition is satisfied by the URL path of the document.
You can now create a unique tag for each of these three variations, still referencing the same macro in the rule, because it will return true/false depending on which condition is satisfied. If you want a more verbose check than just Boolean true/false, you can modify the original macro to return the query selector itself, after which you can modify the rule accordingly:
function() {
var controlElement = document.querySelector({{get query selector}});
return controlElement && controlElement !== {{element}} && controlElement.contains({{element}}) ? {{get query selector}} : false;
}
This piece of code returns “#mainNav” if the click occurred on the home page navigation, ‘.call-to-action’ if the click occurred on any of the three call-to-action elements on the home page, or ‘footer’ for all other cases. Then, if you have a tag which should only fire on clicks on the product page calls-to-action, the rule would look like this:
{{event equals gtm.linkClick}}
{{node contains element}} equals .call-to-action
And that’s it for the advanced method. Remember that one of the huge perks of using a tag management solution is the opportunity to consolidate your tags and make the whole setup so much leaner. Using Custom JavaScript macros for advanced lookups or the Lookup Table macro for simple value retrievals is usually the key to reducing the weight of your implementation.
Conclusions
This was a very basic guide on a sweet little method of the DOM Node object not many seem to be aware of. The point here is that instead of building vast selector chains, where each link is weaker than the one before it, you can just use an all-encompassing ancestor lookup, which reduces the chance of error.
If you want to get real flashy, there’s always the Node.compareDocumentPosition() method, which returns a bitmask representing the relationship between two nodes. It doesn’t take just ancestry into account, it also looks for descendant relationships, which might be useful in some cases. However, it’s not in the scope of this guide, and I’m pretty sure that most of the problems can be solved with the ancestor lookup.
JavaScript is fun! Google Tag Manager is fun-ner!