1

I have bound click eventListeners to an up and down vote button.

Problem: When I click on different parts of the button I get the corresponding element I clicked on and not the parent element which contains relevant information for further processing.

What I already tried: I already tried ev.stopPropagation(); but the behaviour remained the same.

Question: How can I solve this problem?

My example code

const commentVotes = document.querySelectorAll('.comment-votes');

commentVotes.forEach((row) => {
  const up = row.querySelector('.comment-vote-up');
  const down = row.querySelector('.comment-vote-down');            

  up.addEventListener('click', async (ev) => {    
    // ev.stopPropagation();
    const id = ev.target.getAttribute('data-item-id');
    console.log({"target": ev.target, "ID": id})
  });
  
  down.addEventListener('click', async (ev) => {
    // same as up
  })
});
.comment .comment-vote-box {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}

.spacer {
  margin-right:10px;
}
<div class="comment">
  <div class="comment-vote-box comment-votes mt-10">
    
    <div class="vote-up">
      <button class="comment-vote-up"
              data-item-id="11">
        <span class="spacer">Like</span>
        <span>0</span>
      </button>
    </div>
    
    <div class="vote-down">
      <button class="comment-vote-down"
              data-item-id="12">
        <span class="spacer">Dislike</span>
        <span>1</span>
      </button>
    </div>
    
  </div>
</div><!-- comment -->
Max Pattern
  • 1,430
  • 7
  • 18
  • either use your `up` / `down` variables which reference your elements & which you saved previously instead of `ev.target` or as tenbits suggests `currentTarget` – zangab Jan 19 '23 at 08:09
  • 1
    @zangab Thank you for your comment. Unfortunately, I didn't quite understand your approach / suggestion. Can you please describe it to me again in more detail. Gladly as an answer, so that I can thank you. Is there something not "state of the art" about my approach? – Max Pattern Jan 19 '23 at 08:13
  • As everyone mentioned the correct approach is using currentTarget instead of target. But to get the ID you can also use ev.currentTarget.dataset.itemId instead of ev.target.getAttribute('data-item-id'). https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset – Armin Ayari Jan 19 '23 at 08:17

3 Answers3

1

Use the Event.currentTarget to get the attribute values from.

tenbits
  • 7,568
  • 5
  • 34
  • 53
  • Thank tennbits for the fast answer. It's a pity that there are no two Accept buttons, because your and Mike's answers helped me understand and solve my problem..., – Max Pattern Jan 19 '23 at 08:50
1

ev.target: is the element within the bubbling that triggered the event. So exactly what you are describing.

ev.currentTarget: is the element to which you have bound the listener.

* ev = event

https://medium.com/@etherealm/currenttarget-vs-target-in-js-2f3fd3a543e5

const commentVotes = document.querySelectorAll('.comment-votes');

commentVotes.forEach((row) => {
  const up = row.querySelector('.comment-vote-up');
  const down = row.querySelector('.comment-vote-down');            

  up.addEventListener('click', async (ev) => {    
    // ev.stopPropagation();
    const id = ev.currentTarget.getAttribute('data-item-id');
    console.log({"target": ev.target, "currentTarget": ev.currentTarget, "ID": id})
  });
  
  down.addEventListener('click', async (ev) => {
    // same as up
  })
});
.comment .comment-vote-box {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}

.spacer {
  margin-right:10px;
}
<div class="comment">
  <div class="comment-vote-box comment-votes mt-10">
    
    <div class="vote-up">
      <button class="comment-vote-up"
              data-item-id="11">
        <span class="spacer">Like</span>
        <span>0</span>
      </button>
    </div>
    
    <div class="vote-down">
      <button class="comment-vote-down"
              data-item-id="12">
        <span class="spacer">Dislike</span>
        <span>1</span>
      </button>
    </div>
    
  </div>
</div><!-- comment -->
Maik Lowrey
  • 15,957
  • 6
  • 40
  • 79
  • Thank you Mike for the good answer. tennbits answer was quite faster then yours and that the reason why tennbits get the accept sign. Both answers helps me! – Max Pattern Jan 19 '23 at 08:51
1

You probably meant to use event.currentTarget instead of event.target:

  • event.currentTarget is the target of the current listener (further: current target).
  • event.target is the target to which the event was dispatched (further: dispatching target).

Alternatively you can just reference the specific target directly since you use distinct listeners with distinct references (up, down) as a closure.

Instead of using distinct listeners for each element, you could also make use of event delegation (see below).

Also, see below for an explanation of event propagation and stopPropagation().


Event propagation

A single dispatched event may invoke multiple listeners.

The order of invocation for the listeners is specified to happen in phases:

  1. Capturing phase:
    Capturing listeners are invoked in tree-order; from root to dispatching target.
  2. Target phase:
    Non-capturing listeners of the dispatching target are invoked.
  3. Bubbling phase:
    Non-capturing listeners are invoked in reverse tree-order; from (excluding) dispatching target to root.

Additionally, listeners of a target are invoked in the order in which they are added.

This sequence (of listener invocations) is called event propagation.

At any point may a listener stop this propagation from reaching the next listener, e.g. via event.stopPropagation():

Example of stopping propagation early:

const outer = document.getElementById("outer");
const inner = document.getElementById("inner");

// A non-capturing listener
inner.addEventListener("click", evt => logId(evt.currentTarget));

outer.addEventListener("click", evt => {
  evt.stopPropagation();
  logId(evt.currentTarget);
}, { capture: true }); // Attach a capturing listener

function logId(element) {
  console.log(element.id);
}
#outer {background-color: blue}
#inner {background-color: red}

div {
  padding: .2rem;
  padding-block-start: 3.8rem;
}
<div id="outer">
  <div id="inner"></div>
</div>

Event delegation

Listeners of common ancestors of two distinct elements will be invoked due to event propagation (see above).

And events hold references to the dispatching target and the current target. This allows listeners to be more abstract than otherwise possible.

Together, these aspects allow events to be handled by one listener on a (but usually the first) common ancestor (further: delegator) for multiple distinct elements. This is called event delegation.

Sidenote: This is most easily realized if the relevant elements are siblings, but this is not a requirement.

(Any descendant of) the delegator may be the dispatching target. This also means that no relevant element may be a target. For example, if the common ancestor itself is the dispatching target, then no relevant element is targeted.

We need to assert that the event happened in a relevant element. Otherwise the event should not be handled.

In most cases that assertion can be done by querying for the relevant element with calling closest() on event.target.

Advantages of using event delegation:

  • "It allows us to attach a single event listener for elements that exist now or in the future":
    • Less memory usage.
    • Less mental overhead and simpler code when adding/removing elements.
  • Allows "behaviour pattern": Elements with e.g. certain attributes will automatically inherit some behaviour.
  • Allows separation of design (relevant elements + CSS) and application (delegator).
  • Less likely to cause significant memory leaks:
    • One listener means one closure at maximum. As opposed to potentially infinite listeners and therefore closures, this one closure is less likely to have a significant effect on memory usage.

Note that not all events bubble, meaning you cannot use event delegation to handle them.

Example

A typical listener for event delegation...

  1. Finds the first common ancestor to be used as a delegator.
  2. Attaches an abstract listener to the common ancestor:
    1. Assert that event happened in a relevant element; otherwise abort.
    2. Handles the event.

Let's say we have a table of products, and want to log the product row that was clicked on as an object. An implementation may look like this:

const tbody = document.querySelector("tbody"); // First common ancestor

tbody.addEventListener("click", evt => {
  const tr = evt.target.closest("tr"); // Find reference to relevant element
  if (tr === null) return; // Abort if event not in relevant element
  
  // Usecase-specific code
  logRow(tr);
});

function logRow(tr) {
  const [idCell, nameCell, amountCell] = tr.children;
  
  const row = {
    id: idCell.textContent,
    name: nameCell.textContent,
    amount: Number(amountCell.textContent)
  };
  
  console.log(row);
}
<table>
  <caption>Table of products</caption>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Amount in stock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>Spaghetti</td>
      <td>34</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Peanuts</td>
      <td>21</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Crackers</td>
      <td>67</td>
    </tr>
  </tbody>
</table>

Without event delegation (i.e. with distinct listeners), an implementation could look like this:

const tableRows = document.querySelectorAll("tbody>tr");

tableRows.forEach(tr => {
  tr.addEventListener("click", () => {
    // `event.currentTarget` will always be `tr`, so let's use direct reference
    logRow(tr);
  });
})

function logRow(tr) {
  const [idCell, nameCell, amountCell] = tr.children;
  
  const row = {
    id: idCell.textContent,
    name: nameCell.textContent,
    amount: Number(amountCell.textContent)
  };
  
  console.log(row);
}
<table>
  <caption>Table of products</caption>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Amount in stock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>Spaghetti</td>
      <td>34</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Peanuts</td>
      <td>21</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Crackers</td>
      <td>67</td>
    </tr>
  </tbody>
</table>

If you were to add or remove rows, then...

  • ... the first example would just work.
  • ... the second example would have to consider adding listeners to the new elements. If they are added e.g. via innerHTML or cloneNode(), then this may become complicated.
Oskar Grosser
  • 2,804
  • 1
  • 7
  • 18