back to series

Implement a getElementsBy function

getElementsBy(*) are some common DOM API to find elements matching a certain selector. They are often used to navigate/find elements on a DOM tree. In this blog, we’ll learn how to possible recreate them using recursive DOM transversal.

We’ll consider the following HTML document for all the below problems for simplicity:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root" class="root">
<span data-name="test-node" class="red yellow">1</span>
<span data-name="test-node" class="red">2</span>
<button id="btn">Click Me</button>
<div class="div1" id="div1">
<p><span class="red green">Hello</span>World</p>
<div class="div2" id="div2">
<b><span data-name="3" class="purple red 123">Bold text</span></b>
</div>
</div>
</div>
</body>
</html>
Variation: By Tag Name

1. By tagName implementation

Implement a getElementsByTagName function which takes a rootElement and a tagSelector, and finds and returns all elements matching the tagName. You can ignore the root element.

./src/getElementsByTagName.js
function getElementsByTagName(rootEl, tagName) {
const result = [];
const targetTagName = tagName.toLowerCase();
if (!rootEl) {
return result;
}
function traverseEl(element) {
const elTagName = element.tagName.toLowerCase();
// each child is checked for matching tagName
if (elTagName === targetTagName) {
result.push(element);
}
// if it has further nesting, we check on those children
for (const childEl of element.children) {
traverseEl(childEl);
}
}
// we iterate over all children of root and check if we have a match
for (const child of rootEl.children) {
traverseEl(child);
}
return result;
}
// Usage
const output = getElementsByTagName(document.getElementById('root'), 'span');
// Yields the following HTML elements:
// [span.red.yellow, span.red, span.red.green, span.purple.red.123]
console.log(output);
Variation: By ClassName

2. By className implementation

Implement a getElementsByClassName function which takes a rootElement and classNames, and finds and returns all elements matching the classNames (can be more than one). You can ignore the root element.

./src/getElementsByClassName
import getFormattedClassNames from './src/utils.js';
function getElementsByClassName(rootEl, classNames) {
const result = [];
// The getFormattedClassNames utility function is covered later on.
const targetClassNames = getFormattedClassNames(classNames);
if (!rootEl) {
return result;
}
function traverseEl(element) {
const elClassNames = getFormattedClassNames(element.className);
// check if all classNames to match exist on element
const isClassNameMatch = targetClassNames.every((cls) => elClassNames.includes(cls));
if (isClassNameMatch) {
result.push(element);
}
// if it has further nesting, we check on those children
for (const childEl of element.children) {
traverseEl(childEl);
}
}
for (const child of rootEl.children) {
traverseEl(child);
}
return result;
}
// Usage
const output = getElementsByClassName(document.getElementById('root'), 'red');
// Yields the following HTML elements:
// [span.red.yellow, span.red, span.red.green, span.purple.red.123]
console.log(output);
// Can also handle more than one classNames
// const output = getElementsByClassName(document.getElementById('root'), 'red purple');

In this implementation, the utility function getFormattedClassNames, may or may not be needed based on how className field is passed. In general there might be few cases you need to handle:

  1. Whether only you need to match one or more classNames?
  2. Whethere the classNames passed contains duplicates, whitespaces etc?

My implementation covers the above cases this way, but there maybe be other cases which we might need to cover depending upon the interview. One example can be, lowercase and uppercase classnames 😅

./src/utils.js
function getFormattedClassNames(classNames) {
// split by space: "red yellow red" --> ["red","yellow","", "red"]
const classNamesArray = classNames.split(' ');
// remove whitespaces: ["red","yellow","", "red"] --> ["red","yellow", "red"]
const filteredClassNamesArray = classNamesArray.filter((cls) => cls.trim().length > 0);
// remove duplicate entries: ["red","yellow", "red"] --> ["red","yellow"]
const uniqueClassNames = Array.from(new Set(filteredClassNamesArray));
return uniqueClassNames;
}
Variation: By Custom Data Attribute

3. By customDataAttribute implementation

Implement a getElementsByCustomDataAttribute function which takes a rootElement, a custom attribute name and value, and finds and returns all elements contain the custom attribute name and value. You can ignore the root element.

./src/getElementsByCustomDataAttribute.js
function getElementsByCustomDataAttribute(rootEl, { attributeName, attributeValue }) {
const result = [];
if (!rootEl) {
return result;
}
function traverseEl(element) {
// get the custom attribute
const elAttributeValue = element.getAttribute(`data-${attributeName}`);
if (attributeValue === elAttributeValue) {
result.push(element);
}
// if it has further nesting, we check on those children
for (const childEl of element.children) {
traverseEl(childEl);
}
}
for (const child of rootEl.children) {
traverseEl(child);
}
return result;
}
// Usage
const output = getElementsByCustomDataAttribute(document.getElementById('root'), {
attributeName: 'name',
attributeValue: 'test-node'
});
// Yields the following HTML elements:
// [span.red.yellow, span.red]
console.log(output);

There may be further variations of this to check for a list of data attributes, a particular style attribute like color, fontSize etc.

The possibilities here are endless, but the core transversal remains same with only the filter/matching logic changes.

Example, if we were to check for a particular style attribute like color, we could simply do:

/**
* ......
* ..... before
*/
function traverseEl(element) {
// get any style attribute using window.getComputedStyle API
const elColor = window.getComputedStyle(element).color;
// if we were to check a direct match
if (elColor === targetColor) {
result.push(element);
}
// if we were to check for inclusion
if (targetColor.includes(elColor)) {
result.push(element);
}
/**
* ......
* ..... and so on
*/
}
/**
* ......
* ..... after
*/
Variation: By class Name Hierarchy

4. By classNameHierarchy implementation

In this implementation, we need to find elements which follow a given classname hierarchy on DOM, i.e, fruits>apple>ripe Hierarchy means finding all elements will classname ripe which are direct children of elements with classname apple and so on..

./src/getElementsByClassNameHierarchy
function getElementsByClassNameHierarchy(root, classHierarchy) {
const classNames = classHierarchy.split('>');
const result = [];
function traverse(el, index = 0) {
// current className which were are interested in
const currentClassName = classNames[index];
// does current el have that class
const elHasCurrentClassName = el.classList.contains(currentClassName);
// if it has it and it's also last className, then search is over
if (elHasCurrentClassName && index === classNames.length - 1) {
result.push(el);
return;
}
if (elHasCurrentClassName) {
// if it did have, we traverse child and look for second className
for (const childEl of el.children) {
traverse(childEl, index + 1);
}
} else {
// if it did not have it, we traverse child and restart the search
for (const childEl of el.children) {
traverse(childEl, 0);
}
}
}
// we start from root directly since we want to include root as well
traverse(root);
return result;
}
const root = document.getElementById('root');
const result = getElementsByClassNameHierarchy(root, 'root>div1>div2');
/**
* Yields: [div#div2.div2]
*
*/
console.log(result);

Further Reading

I strongly encourage you to explore and tackle additional questions in my Recursion Questions for Frontend Interviews blog series.

By doing so, you can enhance your understanding and proficiency with recursion, preparing you to excel in your upcoming frontend interviews.

Wishing you best. Happy Interviewing 🫡.