Option to limit or scope the result of getElementsBy*() functions

I’d like to propose an option to all DOM functions that select multiple elements (i.e. getElementsBy*, maybe also querySelectorAll?) to limit which elements they can find.

As a simple example:

<div id="context">
    <li id="A"></li>
    <li id="B"></li>
    <li id="C">
        <li id="CA"></li>
        <li id="CB"></li>
    <li id="D"></li>

I’d like to select all <li> elements inside <div id="context"> but not <li> inside other <li>. Maybe like so:

const contextElement = document.getElementById('context')
contextElement.getElementsByTagName('li', { flat: true })

A more flexible variation could allow you to specify elements whose descendants should be ignored, if those elements are very different:

contextElement.getElementsByTagName('li', { ignoreDescendantsOf: 'li' })

It would be nice if ignoreDescendantsOf was a CSS selector. Otherwise just another tag name.

This is useful if one wants to select all conceptual children of a recursive structure even if the elements aren’t children in a technical sense.

One good example (and also one of my potential use-cases) would be the following XML Schema:

  <xs:element name="element-a">
        <xs:element name="element-a-a">
              <xs:element name="element-a-a-a" />
              <xs:element name="element-a-a-b" />
          <xs:element name="element-a-b" />
          <xs:element name="element-a-c" />
        <xs:element name="element-a-d"></xs:element>
        <xs:element name="element-a-e"></xs:element>

When I parse this using the DOMParser and get a reference to “element-a” I could select all element definitions that define immediate children of “element-a” like this:

elementA.getElementsByTagNameNS(ns, 'element', { flat: true })

The result would only include element-a-a to element-a-e, but not element-a-a-a and element-a-a-b because they are located inside another <xs:element>.

Using the alternative implementation you could select all <xs:element> elements that are not somewhere inside a <xs:choice> element:

elementA.getElementsByTagNameNS(ns, 'element', { ignoreDescendantsOf: 'choice' })

Although it seems CSS selectors don’t support namespaces. So, I see at least a problem with this particular combination.

1 Like

You can do what you want with CSS selectors already. E.g. for your first example:

document.querySelector("#context li:not(#context li li)");

Hm… I was about to say: this only works if the context element has an id. But after thinking this through I think this could be flexible enough for more use-cases. Although, I don’t know if it’s still faster to use the getElementsBy* functions and filter the results. It’s often said that the getElementsBy* functions are faster than querySelector(All). But thanks for this tipp nonetheless.

It works with any selector, nothing specific to ids. The performance difference of more specialized functions vs selector queries is negligible today, and is likely offset by the fact that getElementsByTagName() and friends return a Live NodeList.

Your first example (and thus, your proposed flat option) can also be achieved via:

document.getElementById('context').querySelector(":scope li:not(:scope li li)");

Sorry, the part with the id was poorly described by me. But never mind that, because :scope solves so many problems I’ve had in my mind. I’ve never seen this before. Thank you a lot for pointing that out.

I also forgot that querySelectorAll and getElementsBy* actually return different results (live vs. not-live). But wouldn’t that actually be a reason to add this feature? It would be the only way to get a live and “scoped” collection of elements. Or would a live version of querySelectorAll be a viable alternative?