Tagged: javascript

  • Selector scoping in element.querySelector() and element.querySelectorAll()

    I was reading the MDN docs for element.querySelector() and learnt something totally unexpected about selector scope when this method is called on an element.

    Let’s say I have the following HTML:

    <div>
    	<p>This is a paragraph and it has a <span>span inside</span> it.</p>
    </div>
    
    <script>
    	const baseElement = document.querySelector('p');
    	// Note: We are querying relative to the <p> element, not the document
    	const matchedSpan = baseElement.querySelector('div span');
    	console.log(matchedSpan);
    </script>

    Once the above JavaScript is executed, what do you expect to happen?

    I expected it to log null to the console. My reasoning was simple: I am calling the method on baseElement (the <p> tag), and that paragraph clearly does not contain a <div> inside it.

    But here’s what actually gets logged to the console:

    <span>span inside</span>

    This surprisingly returns the <span> from inside the <p>.

    Why does this happen? MDN explains it like this:

    […] selectors is first applied to the whole document, not the baseElement, to generate an initial list of potential elements. The resulting elements are then examined to see if they are descendants of baseElement.

    Note: element.querySelectorAll() also applies selector scoping in the same manner.

    If we want to restrict the selector to the element on which it is called then we should use the :scope pseudo-class.

    const scopedQuery = baseElement.querySelector(':scope div span');
    console.log(scopedQuery); // Returns null, as expected

    As mentioned in the MDN docs for :scope, this works because:

    When used within a DOM API call — such as querySelector(), querySelectorAll(), matches(), or Element.closest():scope matches the element on which the method was called.

  • Natural sorting of strings that contain numbers in JavaScript

    In JavaScript, things get interesting when you need to sort strings that contain numbers in a way that matches human expectations.

    const files = ['IMG_2.png', 'IMG_1.png', 'IMG_10.png', 'IMG_20.png'];
    files.sort();
    // Output: ['IMG_1.png', 'IMG_10.png', 'IMG_2.png', 'IMG_20.png']

    This happens because, by default, when no compare function is provided, JavaScript’s array sort() method converts the elements into strings, then compares their sequences of UTF-16 code unit values.

    This behaviour is well documented on MDN.

    So, how do we sort arrays like the ones above in a more accurate, human-friendly order?

    This is where the compare() method of the Intl.Collator API with the numeric: true option shines. It provides the natural sorting behaviour that correctly handles numbers alongside other characters.

    When numeric: true is set, the collator detects numeric substrings inside the strings and parses those substrings as actual numbers, not as sequences of digits. And then it compares the numbers numerically, not character by character.

    const naturalOrder = new Intl.Collator(undefined, {
    	numeric: true,
    }).compare;
    
    const files = ['IMG_2.png', 'IMG_1.png', 'IMG_10.png', 'IMG_20.png'];
    files.sort((a, b) => naturalOrder(a, b));
    // Output: ['IMG_1.png', 'IMG_2.png', 'IMG_10.png', 'IMG_20.png']

    Hat tip to Jan Miksovsky, whose zero dependencies SSG project led me to discover this.