Search code examples
javascriptdomselectors-api

custom querySelectorAll implemention


This was given to me as an interview question -- didn't get the job, but I still want to figure it out.

The objective is to write two querySelectorAll functions: one called qsa1 which works for selectors consisting of a single tag name (e.g. div or span) and another called qsa2 which accepts arbitrarily nested tag selectors (such as p span or ol li code).

I got the first one easily enough, but the second one is a bit trickier.

I suspect that, in order to handle a variable number of selectors, the proper solution might be recursive, but I figured I'd try to get something working that is iterative first. Here's what I've got so far:

  qsa2 = function(node, selector) {
    var selectors = selector.split(" ");
    var matches;
    var children;
    var child; 
    var parents = node.getElementsByTagName(selectors[0]);
    if (parents.length > 0) {
        for (var i = 0; i < parents.length; i++) {
            children = parents[i].getElementsByTagName(selectors[1]);
            if (children.length > 0) {
                for (var i = 0; i < parents.length; i++) {
                    child = children[i];
                    matches.push(child); // somehow store our result here
                }
            }
        }
    }
    return matches;
  }

The first problem with my code, aside from the fact that it doesn't work, is that it only handles two selectors (but it should be able to clear the first, second, and fourth cases).

The second problem is that I'm having trouble returning the correct result. I know that, just as in qsa1, I should be returning the same result as I'd get by calling the getElementsByTagName() function which "returns a live NodeList of elements with the given tag name". Creating an array and pushing or appending the Nodes to it isn't cutting it.

How do I compose the proper return result?

(For context, the full body of code can be found here)


Solution

  • Here's how I'd do it

    function qsa2(selector) {
        var next = document;
        selector.split(/\s+/g).forEach(function(sel) {
            var arr = [];
            (Array.isArray(next) ? next : [next]).forEach(function(el) {
                arr = arr.concat( [].slice.call(el.getElementsByTagName(sel) ));
            });
            next = arr;
        });
        return next;
    }
    

    Assume we always start with the document as context, then split the selector on spaces, like you're already doing, and iterate over the tagnames.
    On each iteration, just overwrite the outer next variable, and run the loop again.
    I've used an array and concat to store the results in the loop.

    This is somewhat similar to the code in the question, but it should be noted that you never create an array, in fact the matches variable is undefined, and can't be pushed to.