1

I am trying to capture the id on each card from the root element. However, every time I click on a nested element I get empty string. However, I want the id from the wrapping card while listening on the root element cards. I want to handle both bubbling and capturing cases as this is part of a larger structure. I only want answers in vanilla js, and Javascript, no css please.

cards.addEventListener('click', evt => {
  if (evt.target !== evt.currentTarget) {
    var clickedItem = evt.target.id
    console.log(clickedItem);
  }
});
.card {
  /* Add shadows to create the "card" effect */
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  transition: 0.3s;
}


/* On mouse-over, add a deeper shadow */

.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}


/* Add some padding inside the card container */

.container {
  padding: 2px 16px;
}
<div id="cards" style="margin: auto; width: 50%;">
  <div class="card" id="1234567"><img src="img_avatar.png">
    <div class="container">
      <h4>1st Note</h4>
      <p>Note Body</p>
    </div>
  </div>
  <div class="card" id="1234547"><img src="img_avatar.png">
    <div class="container">
      <h4>2nd Note</h4>
      <p>Note Body2</p>
    </div>
  </div>
  <div class="card" id="721680"><img src="img_avatar.png">
    <div class="container">
      <h4>3Note Body</h4>
      <p>Note Body3</p>
    </div>
  </div>
</div>
Rick
  • 1,035
  • 10
  • 18

2 Answers2

3

Yes, one listener is a good requirement. As it happens you only need to change the line where you get your target:

cards.addEventListener('click', evt => {
  console.log( evt.target.closest( '.card' ).id );
});

Complete example:

cards.addEventListener('click', evt => {
  alert( evt.target.closest( '.card' ).id );
});
.card {
  /* Add shadows to create the "card" effect */
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  transition: 0.3s;
}


/* On mouse-over, add a deeper shadow */

.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}


/* Add some padding inside the card container */

.container {
  padding: 2px 16px;
}
<div id="cards" style="margin: auto; width: 50%;">
  <div class="card" id="1234567"><img src="img_avatar.png">
    <div class="container">
      <h4>1st Note</h4>
      <p>Note Body</p>
    </div>
  </div>
  <div class="card" id="1234547"><img src="img_avatar.png">
    <div class="container">
      <h4>2nd Note</h4>
      <p>Note Body2</p>
    </div>
  </div>
  <div class="card" id="721680"><img src="img_avatar.png">
    <div class="container">
      <h4>3Note Body</h4>
      <p>Note Body3</p>
    </div>
  </div>
</div>
Sheepy
  • 17,324
  • 4
  • 45
  • 69
  • 1
    Note this is not supported in IE – Mosho Jun 07 '17 at 03:56
  • 1
    @Mosho Polyfill support for `closest()` is available for IE and other old browsers. But I didn't mention it because the original code is ES6 (note the arrow function) which need a transpiler to work on IE. – Sheepy Jun 07 '17 at 04:00
  • Sure, just noting for posterity :) – Mosho Jun 07 '17 at 04:19
0

The issue is that depending on where you click, you may wind up clicking on <div class="container">, which has no id.

Let's examine your HTML structure:

<!-- You have your click handler attached to this parent element. It's true
     that any clicks on descendant elements will bubble up to this parent, 
     but when you handle it here, this parent becomes event.currentTarget 
     and while this element does have an id, it's not the id you want. -->
<div id="cards" style="margin: auto; width: 50%;">

  <!-- This is the element level that has the id that you want, so
       this is the element level that should have the event handlers. -->
  <div class="card" id="1234567">

    <!-- If the user clicks in the area of the following elements (which 
         they are most likely to because it takes up most of the space 
         on the card) one of these elements will become `event.target` 
         and none of these elements has an id to get. That's why you are
         getting an empty string in your console. -->
    <img src="img_avatar.png">

    <div class="container">
      <h4>1st Note</h4>
      <p>Note Body</p>
    </div>

  </div>

  ... inner pattern repeats ...

</div>

You need to assign the click event to each .card, not the parent .cards and then you can get the currentTarget.id

Array.prototype.slice.call(document.querySelectorAll(".card")).forEach(function(c){
  c.addEventListener('click', evt => {
    console.log(evt.currentTarget.id);
  });
});
.card {
  /* Add shadows to create the "card" effect */
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  transition: 0.3s;
}


/* On mouse-over, add a deeper shadow */

.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}


/* Add some padding inside the card container */

.container {
  padding: 2px 16px;
}
<div id="cards" style="margin: auto; width: 50%;">
  <div class="card" id="1234567"><img src="img_avatar.png">
    <div class="container">
      <h4>1st Note</h4>
      <p>Note Body</p>
    </div>
  </div>
  <div class="card" id="1234547"><img src="img_avatar.png">
    <div class="container">
      <h4>2nd Note</h4>
      <p>Note Body2</p>
    </div>
  </div>
  <div class="card" id="721680"><img src="img_avatar.png">
    <div class="container">
      <h4>3Note Body</h4>
      <p>Note Body3</p>
    </div>
  </div>
</div>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • I need to be able to do it with a single event listener on the parent. Are you saying that is not possible with current html structure? – Rick May 30 '17 at 19:17
  • Yes, because if you click on most of the area of the `.card`, you'll be clicking on something that has no `id`. You need to set the handler up on the `.card` elements and then whatever inside that `.card` you click, you will trap the event at that card, not a higher level. – Scott Marcus May 30 '17 at 19:19
  • Just to be clear, we can get a reference to the object that initially triggered the event (`event.target`) and we can get a reference to an object that is receiving the event (`event.currentTarget`), but we can't get references to objects that the event propagated through, unless we set an event handler on them. – Scott Marcus May 30 '17 at 19:21
  • Can you elaborate on this point in your answer, as this was the answer I was looking for, Thank you. – Rick May 30 '17 at 19:30
  • @Arrow An event is triggered by interaction with some element. That is the `evt.target`. From there, it bubbles (we're not going to get into the capture phase here), through its ancestor elements. It is only when the `evt.target` or one of its ancestors has an event listener (handler) set up for it that your code can get involved. We can place handlers at any (or every) element along the way. But, when we do, we can only get immediate references to either the object that triggered the event (`evt.target`) or the object where the handler intercepted the event (`evt.currentTarget`).... – Scott Marcus May 30 '17 at 19:34
  • @Arrow Your code tries to get the `id` of an element that is neither the `evt.target` or the `evt.currentTarget` because you are intercepting the event at the `.cards` level (so that one element becomes `evt.currentTarget`) when the user most likely initiated the event by cliicking on a `
    ` element (which would be `evt.target`) and you were trying to get the `id` of an element in between those two.
    – Scott Marcus May 30 '17 at 19:36
  • @Arrow I have updated the answer to better illustrate the issue. – Scott Marcus May 30 '17 at 19:43
  • that help a lot! One last thing, someone mentioned pointer-events to reference the parents in CSS.Also I saw an example where someone was walking up the tree until they saw the desired element and set that as the target element. If I changed the structure of my html would that be possible, and would it be preferred over setting up many event listeners over lots of similar elements? – Rick May 30 '17 at 20:08
  • @Arrow Setting up multiple event handlers is MUCH less expensive in terms of performance than walking the DOM (which is VERY expensive). And, given that we're doing it via a loop, it's minimal code as well. This really is the best way. As far as CSS `pointer-events` goes, what did you want to do with it? `pointer-events` is something that affects the element that you apply it to, it is not for targeting parent elements. – Scott Marcus May 30 '17 at 20:28
  • CSS pointer events isn't for event handling. It's for determining if mouse events should be generated for an element. – Scott Marcus May 30 '17 at 20:31
  • I was thinking maybe inside the addEventLIstener('click', evt=>{ we could do a while loop and check while there is still evt && evt.className !'==' ' we could then set the event to that event once that condition was reached, that condition being that we found a class or some other type of identifier that meets our condition – Rick May 30 '17 at 20:32
  • @Arrow To what end though? What are you hoping to accomplish that my answer doesn't already do? Loops and `if/then` tests are going to be much more expensive than assigning a few event handlers. – Scott Marcus May 30 '17 at 20:36
  • I think you answered the question perfectly!!!! It is a perfect answer!!. My understanding about what was taking place was muddled. I wanted to think about the problem from a scalability perspective as an afterthought, that was all. – Rick May 30 '17 at 20:41