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>
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.
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;}
// Usageconst 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);
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.
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;}
// Usageconst 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:
- Whether only you need to match one or more classNames?
- 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 😅
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;}
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.
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;}
// Usageconst 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 */
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..
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 🫡.