Let's say I have the following:
<div className="container">
<div id="one" class="item">
<div></div>
<div></div>
<div>
<span>foo</span>
</div>
</div>
<div id="two" class="item">
<div></div>
<div></div>
<div>
<div>foo</div>
</div>
</div>
<div id="three" class="item">
<span>foo</span>
<div></div>
<div>
<span></span>
</div>
</div>
<div id="four" class="item">
<span></span>
<div></div>
<div>
<div>
<span>foo</span>
</div>
</div>
</div>
</div>
I want to return all items that contain a span with text content "foo" anywhere within them. In the above example I would expect 1, 2, and 4 to be returned.
I've written a recursive method but am stumbling somewhere, I think in how I'm handling the return. This currently returns all items.
const hasFoo = (el) => {
const isSpan = el.tagName?.toLowerCase() === "span";
const isMatch = el.innerText?.toLowerCase() === "foo";
if (isSpan && isMatch) {
return true;
} else if (el.children?.length) {
const nodes = [...el.children];
return nodes.forEach(node => hasFoo(node));
} else {
return false;
}
};
const container = document.getElementsByClassName("container")[0];
const nodes = [...container.children];
const filtered = nodes.filter(node => hasFoo(node));
A couple issues.
className is not a valid alternate for the class attribute. Because of that, document.getElementsByClassName("container") returns an empty array.
return nodes.forEach(node => hasFoo(node)) will ALWAYS return undefined.
According to MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
forEach() executes the callbackFn function once for each array element; unlike map() or reduce() it always returns the value undefined and is not chainable.
Use a for loop instead
const hasFoo = (el) => {
const isSpan = el.tagName?.toLowerCase() === "span";
const isMatch = el.innerText?.toLowerCase() === "foo";
if (isSpan && isMatch) {
return true;
}
const nodes = [...el.children];
for (let i = 0; i < nodes.length; i++) {
if (hasFoo(nodes[i]))
return true;
}
return false;
}
Others have pointed out the problems with className and your forEach. But for better experience that an explicit loop, use Array.prototype.some, like this:
const hasFoo = (el) =>
(el .tagName ?.toLowerCase() === "span" && el .textContent ?.toLowerCase() === "foo") ||
[...el .children ?? []] .some (hasFoo)
const foos = [... document .querySelectorAll (".container > *")] .filter (hasFoo)
foos .forEach (div => div .classList .add ('hasFoo'))
div {border: 1px solid #ccc; margin: .125em; padding: .125em;}
span {border: 1px solid #000; padding: .125em;}
div.hasFoo {background-color: yellow; border: 2px dotted #00f;}
<div class="container"><div id="one" class="item"><div></div><div></div><div><span>foo</span></div></div><div id="two" class="item"><div></div><div></div><div><div>foo</div></div></div><div id="three" class="item"><span>foo</span><div></div><div><span></span></div></div><div id="four" class="item"><span></span><div></div><div><div><span>foo</span></div></div></div></div>
Here we mark all <div>s with a light border, all <span>s with a darker one and highlight all the children of .container which have a foo-span with a yellow background and a dotted border.
It's not recursive, but I prefer to return a filtered array of items based on the condition/requirement.
const items = Array.from(document.querySelectorAll('.item'));
const itemsWithFooSpans = items.filter(item => {
let hasFoo = false;
const spans = item.querySelectorAll('span');
spans.forEach(span => {
console.log('span: ', span);
if (span && span.textContent && span.textContent === 'foo') {
hasFoo = true;
}
});
return hasFoo;
});