Skip to the code
cf.

getElementsByClassName by walking the tree

EasyDOM

Write getElementsByClassName(root, classNames), a version of the built-in method that walks the tree itself. classNames is a string of one or more class names separated by spaces. Return an array of every element inside root (not root itself) whose class list has all of them, in document order.

Match whole class names, case-sensitively: item matches class="item active" but not class="item-active" or class="Item". Extra spaces don't matter, and a string with no class names gives []. root can be an element or document.

Walk the tree with children (or firstElementChild and nextElementSibling). Don't call querySelector, querySelectorAll, getElementsByClassName, getElementsByTagName, or matches.

// <ul id="menu"><li class="item active">One</li><li class="item">Two</li></ul>
getElementsByClassName(document.getElementById("menu"), "item");        // [li, li]
getElementsByClassName(document.getElementById("menu"), "active item"); // [the first li]
getElementsByClassName(document, "");                                    // []

What the tests check

  1. finds every descendant with the class, in document order
    const ids = (elements) => elements.map((element) => element.id);
    const root = document.getElementById("root");
    expect(ids(getElementsByClassName(root, "item"))).toEqual(["one", "three", "five", "six"]);
    
  2. returns a real array
    const result = getElementsByClassName(document.getElementById("root"), "item");
    expect(Array.isArray(result)).toBe(true);
    
  3. leaves out root itself, and searches everything from document
    const ids = (elements) => elements.map((element) => element.id);
    expect(ids(getElementsByClassName(document.getElementById("root"), "item"))).not.toContain("root");
    expect(ids(getElementsByClassName(document, "item"))).toEqual(["root", "one", "three", "five", "six", "outside"]);
    
  4. matches whole class names only
    const ids = (elements) => elements.map((element) => element.id);
    expect(ids(getElementsByClassName(document.getElementById("root"), "active"))).toEqual(["one", "three"]);
    
  5. needs every class in the list, in any order
    const ids = (elements) => elements.map((element) => element.id);
    const root = document.getElementById("root");
    expect(ids(getElementsByClassName(root, "active item"))).toEqual(["one", "three"]);
    expect(ids(getElementsByClassName(root, "extra item"))).toEqual(["three"]);
    expect(ids(getElementsByClassName(root, "item missing"))).toEqual([]);
    
  6. ignores extra spaces, and gives [] for no class names
    const ids = (elements) => elements.map((element) => element.id);
    const root = document.getElementById("root");
    expect(ids(getElementsByClassName(root, "  active   item "))).toEqual(["one", "three"]);
    expect(getElementsByClassName(root, "")).toEqual([]);
    expect(getElementsByClassName(root, "   ")).toEqual([]);
    
  7. is case-sensitive
    const ids = (elements) => elements.map((element) => element.id);
    expect(ids(getElementsByClassName(document.getElementById("root"), "Item"))).toEqual(["four"]);
    
  8. walks the tree without querySelectorAll, getElementsByClassName, getElementsByTagName, or matches
    const ids = (elements) => elements.map((element) => element.id);
    const root = document.getElementById("root");
    const banned = [Element.prototype, Document.prototype].flatMap((proto) =>
      ["querySelector", "querySelectorAll", "getElementsByClassName", "getElementsByTagName", "matches"]
        .filter((name) => name in proto)
        .map((name) => ({ proto, name, original: proto[name] })),
    );
    for (const { proto, name } of banned) {
      proto[name] = () => {
        throw new Error("Don't call " + name + ": walk the tree yourself with children");
      };
    }
    let found;
    try {
      found = getElementsByClassName(root, "item");
    } finally {
      for (const { proto, name, original } of banned) proto[name] = original;
    }
    expect(ids(found)).toEqual(["one", "three", "five", "six"]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).