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. Theancestor()
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 eachparentNode
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
Theancestor()
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 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.