javascriptnode.jsjestjsselectorjsdom

Selector excluding nested values


nodejs: 24.7

jsdom: 26.1.0

Basically, I want to create a selector, that would exclude nested elements - it should find only elements at the first level of nesting. I have this:

it('Test selector non-nested', () => {
  const mainAttribute = 'data-main';
  const itemAttribute = 'data-item';
  const sideAttribute = 'data-side';
  document.body.innerHTML = `
<div>
    <div ${mainAttribute}>
        <div ${itemAttribute}>
            <div ${sideAttribute}>
                <div ${mainAttribute}>
                    <div ${itemAttribute}>
                        <div ${sideAttribute}></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
`;

  const result = [];
  const foundElements = document.querySelectorAll(`[${mainAttribute}]`);
  for (const element of foundElements) {
    result.push(element);
  }

  for (const element of result) {
    const selector = `:scope [${sideAttribute}]:not(:scope [${itemAttribute}] [${itemAttribute}] [${sideAttribute}])`;
    element.innerHTML = element.innerHTML; // It makes it work!
    const results = element.querySelectorAll(selector);
    expect(results.length).toEqual(1);
  }
});

As you can see, I want to find elements having sideAttribute, but only in the top element having the itemAttribute. It means that in this case I want to have 1 result for both iterations of the loop.

It doesn't work UNLESS I will throw element.innerHTML = element.innerHTML; in there, then it magically starts working. What's going on here?

The problem happens in jsdom, but not in a browser. I created an issue ticket here: https://github.com/jsdom/jsdom/issues/3924 but maybe someone will find a workaround for that.


Solution

  • You could use a filter to check if there are any parents with that attribute:

    const divs = Array.from(document.querySelectorAll('[data-main]'));
    const topLevelDivs = divs.filter(d =>  d.parentNode.closest('[data-main]') === null); // you would need to use parentNode here so it doesn't return itself
    
    console.log(topLevelDivs.length)
    <div data-main="main">
      <div data-item="item">
        <div data-side="side">
          <div data-main="main">
            <div data-item="item">
              <div data-side="side"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div data-main="main">
      <div data-item="item">
        <div data-side="side">
          <div data-main="main">
            <div data-item="item">
              <div data-side="side"></div>
            </div>
          </div>
        </div>
      </div>
    </div>