Finding an Ancestor DOM Node

Share this article

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.

Frequently Asked Questions (FAQs) about Finding An Ancestor Node

What is an ancestor node in JavaScript?

In JavaScript, an ancestor node refers to any node that is located directly above another node in the Document Object Model (DOM) tree. This includes parent nodes, grandparent nodes, and so on. Each node in the DOM tree has a parent node except for the root (or document) node. Ancestor nodes are important in JavaScript as they allow us to traverse up the DOM tree and manipulate elements as needed.

How can I find the closest ancestor node that has a specific class in JavaScript?

JavaScript provides a method called closest() that can be used to find the nearest ancestor that matches a specified selector. The closest() method traverses up the DOM tree from the current element and returns the closest ancestor that matches the specified CSS selector. If no such element exists, it returns null. Here’s an example:

let element = document.querySelector('.myElement');
let closestAncestor = element.closest('.myAncestor');

In this example, closest() will start at the element with the class ‘myElement’ and traverse up the DOM until it finds an element with the class ‘myAncestor’.

What is the difference between the closest() and parents() methods in JavaScript?

Both closest() and parents() methods are used to traverse up the DOM tree. The closest() method returns the first ancestor that matches the specified selector, starting at the current element. On the other hand, the parents() method returns all ancestor elements of the selected element, up to but not including the document root.

Can I use the closest() method in all browsers?

The closest() method is not supported in Internet Explorer. However, you can use a polyfill to add support for this method in unsupported browsers. A polyfill is a piece of code that provides the technology that you expect the browser to provide natively.

What is a DOM node in JavaScript?

In JavaScript, a DOM (Document Object Model) node is a single point in the DOM tree. It can be an element node, an attribute node, a text node, or any other of the node types that are defined in the Node interface. Each node can have a parent node, child nodes, and sibling nodes.

How can I select a DOM node in JavaScript?

JavaScript provides several methods to select DOM nodes, including getElementById(), getElementsByClassName(), getElementsByTagName(), and querySelector(). The querySelector() method is very powerful as it allows you to select elements using CSS selectors.

What is the Node interface in JavaScript?

The Node interface in JavaScript is a primary data type for the entire Document Object Model. It represents a single node in the document tree and can have various types depending on their role. The Node interface includes properties and methods that are common to all types of nodes.

How can I check if a node is a descendant of another node in JavaScript?

You can use the contains() method to check if a node is a descendant of another node. The contains() method returns a Boolean value indicating whether a node is a descendant of a given node or not.

What is the difference between a node and an element in JavaScript?

In JavaScript, an element is a specific type of node, one that can be directly specified in the HTML with tags and can have attributes. All elements are nodes, but not all nodes are elements. For example, text inside an element is stored in a text node.

How can I create a new node in JavaScript?

You can create a new node using the createElement() method. This method creates a new element node with the specified name. After the node is created, you can use the appendChild() or insertBefore() method to insert it into the document.

James EdwardsJames Edwards
View Author

James is a freelance web developer based in the UK, specialising in JavaScript application development and building accessible websites. With more than a decade's professional experience, he is a published author, a frequent blogger and speaker, and an outspoken advocate of standards-based development.

functionsRaw JavaScript
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form