javascriptcss-selectorstampermonkeyuserscriptsselectors-api

Use previously cached selectors with querySelector()


How can I use previously cached selectors with certain aspects of querySelector() ?

For example, I have this HTML / JavaScript:

let L1, L2, L3;
L1 = document.querySelector('#L1');

L2 = L1.querySelector('div:nth-child(1)');
L2.classList.add('L2');

L3 = L1.querySelector('div:nth-child(2)');
L3.classList.add('L3');

console.log(L2);
console.log(L3);
<div id="L1">
  <div id="L2">
    <div id="L2a"></div>
    <div id="L2b"></div>
  </div>
  <div id="L3"></div>
</div>

Notice that div#L2b gets the className 'L3' instead of div#L3 getting it.

What I really want to do is something like:

L3 = L1.querySelector('> div:nth-child(2)');

to force querySelector() to choose the correct nth-child(2). For example, as in this demo that does NOT cache the selectors:

let L1, L2, L3;
const $ = document.querySelector.bind(document);
L1 = document.querySelector('#L1');

L2 = L1.querySelector('div:nth-child(1)');
L2.classList.add('L2');

L3 = document.querySelector('div#L1 > div:nth-child(2)');
L3.classList.add('L3');

console.log(L2);
console.log(L3);
<div id="L1">
  <div id="L2">
    <div id="L2a"></div>
    <div id="L2b"></div>
  </div>
  <div id="L3"></div>
</div>

Is there any way this can be done with the L1 cached selector?

In my real-world use case, the L1 selector looks more like:

const $ = document.querySelector.bind(document);
$('body > div > div#main > div:nth-child(3) > div:nth-child(1) > div > div#L1');

I cringe at typing that string before each of 15 selectors I must cache.


Solution

  • I believe you want to use the :scope selector

    const L1 = document.querySelector('#L1');
    const L3 = L1.querySelector(':scope > div:nth-child(2)');
    console.log(L3)
    <div id="L1">
      <div id="L2">
        <div id="L2a"></div>
        <div id="L2b"></div>
      </div>
      <div id="L3"></div>
    </div>

    Regarding your spaghetti selector:

    $('body > div > div#main > div:nth-child(3) > div:nth-child(1) > div > div#L1');
    

    I'm not sure if it's a demo to prove a point, but since IDs must be unique all you need is to target directly that ID (the tag is also unnecessary):

    $("#L1")
    

    I see where you're going with $, is to emulate a quasi-jQuery DOM query helper function.
    I would suggest instead a better variant in where you can also pass the desired parent:

    const el = (sel, par = document) => par.querySelector(sel);
    

    which can be used like:

    // Target an element from document:
    const elL1 = el("#L1");
    // Target an element from a specific parent:
    const elL3 = el(":scope > div:nth-child(2)", elL1); // << notice the second argument
    

    If for some reason you want to make just sure that the #L1 you're trying to target is from a specific website — you can use:

    // Example making sure to cache `<html>` of Stack Overflow specifically
    const elRoot = el(":root:has(meta[content='Stack Overflow'])");
    // Cache L1 of specifically "Stack Overflow" website
    const elL1 = el("#L1", elRoot);
    

    which has a greater survival rate than waiting for a website's design team to just slightly modify the HTML markup and see your script fail (since the too-specific spaghetto selector).