🤯 50% Off! 700+ courses, assessments, and books

Finding an Ancestor DOM Node

James Edwards
Share

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.