paul.kim1901
paul.kim1901

Reputation: 461

trying to recreate getElementsByClassName

function getElementsByClassName(cls) {
  const result = [];
  const checkClass = (element) => {
    const children = element.children;
    for (let i = 0; i < children.length; i++) {
      if()
        if (children[i].contains(cls)) {
          result.push(children[i]); 
        }
      if (children[i].hasChildNodes()) {
     
        checkClass(children[i]);      }
    }
  };
  checkClass(document.body); 
  return result;
}

Hello, all.

From what I understand childNodes has undefined nodes like "text" that can't go through .contain method.

So I switched from childNodes to children and then I am still getting the same ERROR message as below.

getElementsByClassName("targetClassName")

VM1349:8 Uncaught TypeError: Failed to execute 'contains' on 'Node': parameter 1 is not of type 'Node'.
    at checkClass (<anonymous>:8:23)
    at getElementsByClassName (<anonymous>:18:3)
    at <anonymous>:1:1

I understand issue has to do with while iterating through the node list and then trying to apply contains, but I just don't get why it won't execute it after switching it to elements rather than nodes.

Please advise.

Upvotes: 1

Views: 302

Answers (2)

Peter Seliger
Peter Seliger

Reputation: 13417

// ... non live collection approach ...
//
// - does not "know" `String.prototype.trim`,
// - is unaware of `Array.prototype.includes` ...
// - ... as well as of `Element.prototype.classList`
//
function getElementsByClassName(node, className) {

  const regXSplitWS = (/\s+/);
  const regXTrimLeft = (/^\s+/);
  const regXTrimRight = (/\s+$/);

  function trim(str) {
    return str.replace(regXTrimLeft, '').replace(regXTrimRight, '');
  }
  function getClassList(str) {
    str = trim(String(str || ''));
    return (
      (!str && []) ||
      str.split(regXSplitWS)
    );
  }
  const classList = getClassList(className);

  function doesMatchClassNameQuery(elmClassList) {
    return (
      !!elmClassList.length &&
      !!classList.length &&
      classList.every(function (queryName) {
        return (elmClassList.indexOf(queryName) >= 0);
      })
    );   
  }

  function query(collector, elm/*, idx, collection*/) {
    if (doesMatchClassNameQuery(getClassList(elm.className))) {

      collector.push(elm);
    }
    // query recursively.
    return collector.concat(Array.from(elm.children).reduce(query, []));
  }
  // start querying.
  return Array.from((node && node.children) || []).reduce(query, []);
}


console.log(
  document.body.getElementsByClassName('baz bizz')
);
console.log(
  getElementsByClassName(document.body, 'baz bizz')
);

console.log(
  document.body.getElementsByClassName('foo')
);
console.log(
  getElementsByClassName(document.body, 'foo')
);

console.log(getElementsByClassName());
console.log(getElementsByClassName(null, 'bar'));

console.log(getElementsByClassName(document.body));
console.log(document.body.getElementsByClassName(''));
.as-console-wrapper { min-height: 100%!important; top: 0; }
<div class="level-1 foo">
  <div class="level-2a bar">
    <div class="level-3a baz bizz" />
    <div class="level-3b buzz " />
  <div>
  <div class="level-2b baz">
    <div class="level-3c bizz buzz" />
    <div class="level-3d foo bar" />
  <div>
  <div class="level-2c bizz buzz">
    <div class="level-3e foo bar" />
    <div class="level-3f baz bizz" />
  <div>
<div>

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1075875

From what I understand childNodes has undefined nodes like "text" that can't go through .contain method.

I don't know what you mean by "undefined nodes," but Text nodes are perfectly valid arguments for contains:

const div = document.getElementById("x");
const text = div.firstChild;
console.log(text.nodeName);      // #text
console.log(div.contains(text)); // true
<div id="x">foo</div>

That said, using children in your function is reasonable, since only Elements can have classes and your code doesn't need to work on document fragments (e.g., checking their contents), so you only need to look at Elements, not other kinds of nodes.

I assume it's this code:

if (children[i].contains(cls)) {

that gives you

parameter 1 is not of type 'Node'

cls in your code is a string, not a Node. As the error says, contains accepts Nodes. It doesn't accept strings.


A couple of side notes:

  1. Properly recreating getElementsByClassName in your own code is fairly complicated, because it returns a live HTMLCollection, not a snapshot NodeList like querySelectorAll does. That means properly recreating it would require using a MutationObserver to track changes over time and update the list you return as things change. But to recreate it without that "live" feature, you'll probably want a recursive function.

  2. getElementsByClassName accepts multiple class names, not just one, in a space-delimited string.

  3. It works on the entire document, not just body (elements in head can have classes).

FWIW, a non-live solution's general form might look something like this:

function getElementsByClassName(cls) {
    const classes = cls.split(" ");
    return worker(document.documentElement, classes, []);
}

function worker(element, classes, result) {
    if (/*element has all the classes*/) {
        result.push(element);
    }
    for /*...loop through `children`...*/ {
        worker(child, classes, result);
    }
    return result;
}

Or, as Peter Seliger points out, you could start with the HTMLCollection from getElementsByTagName and just filter it. I assume this is a learning exercise, so it depends on the purpose of the learning exercise.

Upvotes: 3

Related Questions