olo
olo

Reputation: 5271

pure Javascript get parents

I created a snippet based on .parents() without jquery - or querySelectorAll for parents

  function getParents (el, _class) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    return parents[parents.map(x => x.className).indexOf(_class)]
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log(getParents(e, 'child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4 hello">  <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

if <div class="child4 hello"> without hello I can get all child4, if hello or anything else is added, I am unable to get child4. I use indexOf() I think shouldn't be -1 or undefined. Could someone please correct me where is the error? Thanks

Upvotes: 2

Views: 841

Answers (4)

User863
User863

Reputation: 20039

Using Element.matches() to add support to query the parents

function getParents(el, query) {
  let parents = []
  while (el.parentNode !== document.body) {
    el.matches(query) && parents.push(el)
    el = el.parentNode
  }
  return parents
}

document.querySelectorAll('.child1').forEach((e, i) => {
  console.log(getParents(e, '.child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


<div class="parent">
  <div class="child5">
    <div class="child4 hello">
      <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Upvotes: 1

VLAZ
VLAZ

Reputation: 28969

The problem is that when you do parents.map(x => x.className) you will get an array with the full class string for each element. So the result will look like (simplified) [ "child4 hello" ] and you try to do indexOf("child4") on that array. Since there is no member that is simply "child4", that fails.

Here is the problematic code illustrated:

function getParents (el, _class) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    let mappedClassName = parents.map(x => x.className)
    console.log("mappedClassName", mappedClassName)
    let indexOf = mappedClassName.indexOf(_class);
    console.log("indexOf", indexOf)
    return parents[indexOf]
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log(getParents(e, 'child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4 hello">  <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

This code would work with a string:

let indexOf = "child4 hello".indexOf("child4");
console.log(indexOf);

However, it doesn't necessarily work correctly:

//the class is NOT child4
let indexOf = "child42 hello".indexOf("child4");
console.log(indexOf);

You should use Element.classList for a more accurate class check:

let div1 = document.getElementById("one");
let div2 = document.getElementById("two");

let _class = "child4";
console.log(`div1 has ${_class}`, div1.classList.contains(_class))
console.log(`div2 has ${_class}`, div2.classList.contains(_class))
<div id="one" class="child4 hello"></div>
<div id="two" class="child42 hello"></div>

If you combine this with Array#findIndex to achieve what you want:

function getParents (el, _class) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    return parents[parents.map(x => x.classList).findIndex(cl => cl.contains(_class))]
    //                                ^^^^^^^^^  ^^^^^^^^^
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log(getParents(e, 'child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4 hello">  <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

You can make the code slightly shorter by dropping .map and running the logic in .findIndex:

parents.findIndex(x => x.classList.contains(_class))

However, with all that said, your algorithm only checks classes. You can very easily extend it to work with any selector by using Element.matches:

function getParents (el, selector) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    return parents[parents.findIndex(x => x instanceof Element && x.matches(selector))]
    // only check Elements -------------> ^^^^^^^^^^^^^^^^^^^^              
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log('.child4', getParents(e, '.child4'))
})


document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log('#parentTwo.child4', getParents(e, '#parentTwo.child4'))
})
<div class="parent">
  <div class="child5">
    <div id="parentOne" class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div id="parentTwo" class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div id="parentThree" class="child4 hello">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Upvotes: 2

shrys
shrys

Reputation: 5940

You could use findIndex of Array prototype like so : return parents[parents.map(x => x.className).findIndex(x => x.indexOf(_class) > -1)]

function getParents (el, _class) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    return parents[parents.map(x => x.className).findIndex(x => x.indexOf(_class) > -1)]
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log(getParents(e, 'child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4 hello">  <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Upvotes: 2

Titus
Titus

Reputation: 22474

The problem is that you're getting a list of elements and you're mapping those to the elements' className, when the target element have more then just the child4 class, the value in the array will be child4 hello and when you're looking for the index of the child4 value in the array, the child4 hello won't match. Instead of Array.indexOf you can use Array.find.

  function getParents (el, _class) {
    let doc = document
    let parents = []
    let p = el.parentNode

    while (p !== doc) {
      let o = p
      parents.push(o)
      p = o.parentNode
    }
    parents.push(doc)
    return parents.find(e => e.className.includes(_class))
  }

document.querySelectorAll('.child1').forEach((e,i)=>{
  console.log(getParents(e, 'child4'))
})
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


  <div class="parent">
  <div class="child5">
    <div class="child4 hello">  <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Instead of the getParents function you could use Element.closest

Here is an example:

const parents = Array.from(document.querySelectorAll('.child1')).map(e => e.closest('.child4'));
console.log(parents);
<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            1
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


<div class="parent">
  <div class="child5">
    <div class="child4">
      <div class="child3">
        <div class="child2">
          <div class="child1">
            2
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


<div class="parent">
  <div class="child5">
    <div class="child4 hello">
      <!-- problem here -->
      <div class="child3">
        <div class="child2">
          <div class="child1">
            3
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

As @Khauri mentioned, in the first example, instead of using the className property and using string functions to check if that value is a match, it will be better to use the classList property because it has a contains function.

Upvotes: 3

Related Questions