Home > OS >  Search DOM nodes and return the parent if any descendants contain a match
Search DOM nodes and return the parent if any descendants contain a match

Time:05-12

Let's say I have the following:

<div className="container">

  <div id="one" >
    <div></div>
    <div></div>
    <div>
      <span>foo</span>
    </div>
  </div>

  <div id="two" >
    <div></div>
    <div></div>
    <div>
      <div>foo</div>
    </div>
  </div>

  <div id="three" >
    <span>foo</span>
    <div></div>
    <div>
      <span></span>
    </div>
  </div>

  <div id="four" >
    <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));

CodePudding user response:

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;
}

CodePudding user response:

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;
});

CodePudding user response:

You can first target all the spans that are inside your container with a simple container.querySelectorAll("span") and then filter in only the ones with the proper text content.
Once you have this list you only have to retrieve each of these spans ancestor matching the .item selector. This can be achieved with the Element.closest() method.
In case you may have multiple such spans per item, you may want to deduplicate the resulting Array:

const container = document.querySelector(".container");
const spans = [...container.querySelectorAll("span")]
  .filter(el => el.textContent.toLowerCase() === "foo");
const items = Array.from(new Set( // avoid duplicates
  spans.map(el => el.closest(".item"))
)).filter(Boolean); // avoid null

console.log(items.map(el => el.id));
<div >

  <div id="one" >
    <div></div>
    <div></div>
    <div>
      <span>foo</span>
    </div>
  </div>

  <div id="two" >
    <div></div>
    <div></div>
    <div>
      <div>foo</div>
    </div>
  </div>

  <div id="three" >
    <span>foo</span>
    <div></div>
    <div>
      <span></span>
    </div>
  </div>

  <div id="four" >
    <span></span>
    <div></div>
    <div>
      <div>
        <span>foo</span>
      </div>
    </div>
  </div>
  
</div>

CodePudding user response:

var elem = document.querySelector(".container");
var spans = elem.querySelectorAll("span");
var found = [];
spans.forEach(item => {
  if (/foo/i.test(item.textContent || "")) {
    found.push({
      span: item,
      parent: item.closest("div.item")
    });
  }
})
console.log("found", found)
<div >

  <div id="one" >
    <div></div>
    <div></div>
    <div>
      <span>foo</span>
    </div>
  </div>

  <div id="two" >
    <div></div>
    <div></div>
    <div>
      <div>foo</div>
    </div>
  </div>

  <div id="three" >
    <span>foo</span>
    <div></div>
    <div>
      <span></span>
    </div>
  </div>

  <div id="four" >
    <span></span>
    <div></div>
    <div>
      <div>
        <span>foo</span>
      </div>
    </div>
  </div>

</div>

  • Related