Finding an Ancestor DOM Node
For the eighth article in this small-and-sweet functions series, I’ll be looking at a function called ancestor()
. As the name suggests, this function gets a reference to a given node’s ancestor, according to a tag name and/or class match.
Here’s the ancestor()
function’s code:
function ancestor(node, match)
{
if(!node)
{
return null;
}
else if(!node.nodeType || typeof(match) != 'string')
{
return node;
}
if((match = match.split('.')).length === 1)
{
match.push(null);
}
else if(!match[0])
{
match[0] = null;
}
do
{
if
(
(
!match[0]
||
match[0].toLowerCase() == node.nodeName.toLowerCase())
&&
(
!match[1]
||
new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className)
)
)
{
break;
}
}
while(node = node.parentNode);
return node;
}
The first argument is a reference to the original node — which can be any kind of DOM node, but will usually be an element. The second argument is a string that identifies the ancestor — either as a simple tag-name like "ul"
, or a class-selector such as ".menu"
, or as a combination of the two, like "ul.menu"
. The function will iterate upwards from the original node, and return the first ancestor node that matches the string pattern, or null
if no such ancestor can be found.
What the Function is for
The most common use-case for this functionality is from within event-handling code — to identify a containing element from an event target, without necessarily knowing what other nodes are in-between; perhaps we don’t even know what type of element the ancestor is. The ancestor()
function handles this by iteratively checking parent nodes against whatever information we have.
For example, let’s say we’re binding focus
events to a group of menu links, with handler code that will need to get a reference to the containing list-item. Dynamic menus usually need to be very flexible in the kind of markup they support, accounting not just for simple items like this:
<li>
<a>...</a>
</li>
But also more complex items, with additional elements added for extra semantics or as styling hooks:
<li>
<h3>
<span>
<a>...</a>
</span>
</h3>
</li>
JavaScript would be added to handle the link focus
events (which have to be added individually, since focus events don’t bubble):
var links = menu.getElementsByTagName('a');
for(var len = links.length, i = 0; i < len; i ++)
{
links[i].addEventListener('focus', function(e)
{
var link = e.target;
}, false);
}
Then the ancestor()
function can handle the target conversion:
var item = ancestor(link, 'li');
The flexibility of the second argument allows for different information cases, for example, where we know the containing menu will have a class
of "menu"
, but we don’t know whether it will be a <ul>
or <ol>
element:
var menu = ancestor(link, '.menu');
Or, perhaps we have a more deeply-nested structure, where individual sub-menus are unordered lists (<ul class="menu">
), while the top-level navigation bar is an ordered-list with the same class
name (<ol class="menu">
). We can define both the tag name and class
in the match, to get the specific reference we want:
var navbar = ancestor(link, 'ol.menu');
In that case then, any number of other "menu"
elements would be ignored, with the ancestor only being returned if it matches both the tag name and the class
.
How the Function Works
The basic functionality is simply an upward iteration through the DOM. We start from the original node, then check each parentNode
until the specified ancestor is matched, or abandon iteration if we run out of nodes (i.e. if we reach the #document
without ever finding the desired node). However, we also have some testing code to make sure both the arguments are properly defined:
if(!node)
{
return null;
}
else if(!node.nodeType || typeof(match) != 'string')
{
return node;
}
If the input node
argument is undefined or null
, then the function returns null
; or if the input node
is not a node, or the input match
is not a string, then the function returns the original node. These are simply safety conditions, which make the function more robust by reducing the need to pre-test the data that’s sent to it.
Next, we process the match
argument to create an array of two values — the first is the specified tag-name (or null
if none was specified), while the second is the specified class-name (or null
for none):
if((match = match.split('.')).length === 1)
{
match.push(null);
}
else if(!match[0])
{
match[0] = null;
}
Finally, we can do the iterative checks, comparing the current reference node at each iteration with the criteria defined in the match
array. If match[0]
(the tag-name) is null
then any element will match, otherwise we only match an element with the specified tag name (converting both to lowercase so the match is case insensitive). Likewise, if match[1]
(the class name) is null
then anything is fine, otherwise the element must contain the specified class
:
do
{
if
(
(
!match[0]
||
match[0].toLowerCase() == node.nodeName.toLowerCase())
&&
(
!match[1]
||
new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className)
)
)
{
break;
}
}
while(node = node.parentNode);
If both conditions are matched, we break iteration, and the current reference node is returned; otherwise we continue to the next parentNode
. If we had allowed the code to get this far when both match
values are null
, the end result would be that we return the original node
, which is exactly what the safety condition at the start already does.
An interesting thing about the iteration itself, is the use of do...while
:
do
{
...
}
while(node = node.parentNode);
Inside the while
evaluation, we’re taking advantage of the ability to define an assignment inside of an evaluation. Each time that’s evaluated, the node
reference is converted to its parentNode
and reassigned. That assignment returns the assigned node
. The node
reference will be null
if the parent didn’t exist, therefore it won’t pass the while
condition, so iteration will stop and the function will return null
. However if the parent does exist, it will pass the while
condition, and so iteration will continue, since any node reference evaluates to true
, but null
evaluates to false
.
Since the number of nodes we have to test is unknown, we have to use a while
statement to iterate for as long as a parent exists. But, by using do...while
rather than simply while
, we evaluate the original node before converting to its parent (since the do
is evaluated before the first while
). Ultimately, this means that if the original node already passes the match condition, it will be returned right away, and this saves us from having to define a separate if
condition before the iteration.
Conclusion
The ancestor()
function won’t win any prizes for sophistication! But abstractions of simple functionality are the bricks and mortar of programming, providing reusable code that saves on repeatedly typing the same basic logic.