2
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.

1

1 Answer 1

4

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).


Sign up to request clarification or add additional context in comments.

5 Comments

Hi Roko, thanks for the pointer to the new scope selector. I read about it but haven't used it yet and am grateful to see how you used it here. As for unique IDs on a page, I do a lot of work with userscripts and ever since Angular, React, Vue, et al... it has become common to see multiple elements sharing the same ID in different components within the same DOM tree. You would be surprised the companies that push code bases where this is a common practice. But you have pointed to the solution for that problem also.
Also another big thank you for the improved alias to document.querySelector. I used the $ alias above so the (rightfully labeled) spaghetti selector would not display the x-scroll bar. Although spaghetti, I am increasingly forced to be that specific with userscripts and browser extensions (but :scope probably solves that!). IDs are rarely seen in these days of Reactive frameworks, and duplicate IDs are common. Your improvement to this simple alias (which I mostly use to save typing) answers a question I've been intending to ask for a while now. May much good karma waft your way, sir.
BTW, if you're not using Tampermonkey yourself, take a look at this github example for inspiration.
@cssyphus you're very welcome. I've added some last paragraph to my answer you might eventually find interesting, where you can cache per-website root elements. The thing is that if you use a too long (spaghetto) selector, all it takes is for the website's design team to just slightly change the HTML markup and your script will fail. If you use the above suggestion your script survival rate is greater, since you're targeting directly a root element (of a speciic website) and then querying the descendants.
@cssyphus Somehow I just don't trust Teampermonkey and similar. I write my own extensions or simply just plug and write (i.e. in Chromium) a source → override.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.