In the following code:
<div onClick={() => console.log('outer div')}>
<div onClick={() => console.log('middle div')}>
<div onClick={() => console.log('innermost div')}>
Click me!
</div>
</div>
</div>
I understand that the event object possesses a target property that contains a reference to the DOM node on which the event occurred. My question is: in the capturing (when capture set to true) and bubbling phases, will the event always fire on an HTML element if that element contains the same HTML attribute (ex. onClick, onHover) that started the propogation cycle? Thanks.
YES.
The event always fires on an HTML element if that element contains the same HTML attribute.
In the code below, you can see which phase of the event fires on a specific node.
Event.eventPhase interface indicates which phase of the event flow is currently being evaluated.
0: none, 1: capturing, 2: target, 3: bubbling
// true: capturing / false: bubbling
const useCature = true;
const phase = (e) => {
const phases = ["capturing", "target", "bubbling"];
const node = `${e.currentTarget.nodeName} .${e.currentTarget.className}`;
console.log(node, phases[e.eventPhase - 1]);
};
document.querySelector(".outer").addEventListener("click", phase, useCature);
document.querySelector(".middle").addEventListener("click", phase, useCature);
document.querySelector(".inner").addEventListener("click", phase, useCature);
<div class="outer">
<div class="middle">
<div class="inner">
Click me!
</div>
</div>
</div>
Related
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 -->
Use the Event.currentTarget to get the attribute values from.
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 -->
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:
Capturing phase:
Capturing listeners are invoked in tree-order; from root to dispatching target.
Target phase:
Non-capturing listeners of the dispatching target are invoked.
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...
Finds the first common ancestor to be used as a delegator.
Attaches an abstract listener to the common ancestor:
Assert that event happened in a relevant element; otherwise abort.
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.
as I said in title I have problem with HTML elements created with Element.insertAdjacentHTML() method, I'm trying about an hour to solve this but can't. I have button that create new HTML elements, couple of that elements is new buttons with same class or id, it's no matter, that I need to catch in some variable and than again use for event listener, for some reason the class or id for these new created button doesn't exist, is there any way to catch it and use it later, I need Vanila Javascript?
There is over 500 lines of code, this is only for mentioned method
btnClaim.addEventListener("click", () => {
rewardCurrent.style.display = "none";
claimedRewards.push(currentReward);
rewardsList.innerHTML = ``;
claimedRewards.forEach(function (rew, i) {
const html = `
<div class="reward" id="${i}">
<div class="img-text-cont">
<img src="${rew.imgUrl}" alt="">
<div class="text-cont">
<p class="claimed-reward-title">${rew.title}</p>
<p class="claimed-reward-price">$${rew.price}</p>
</div>
</div>
<div class="claimed-rewards-action">
<button id="btn-sell2">Sell</button>
<button id="btn-ship">Ship</button>
</div>
</div>
`;
rewardsList.insertAdjacentHTML("afterbegin", html);
I need that btn-sell2 and btn-ship buttons in variables.
your element is going to be created and doesn't exist at the time page loads, so js addeventlistener will throw an error. to solve you have 2 ways.
1- use parent node that element will be created inside.
addevenlistener to parent and use
parent.addeventlistener( event, function (event){
if(event.target.classList.contains("childClass") {}
}
2- give addeventlistener when creating the element :
function createElement () {
const elem = -craete elemnt-
elem.addeventlistener(event, function);
}
I'm making a Rock Paper Scissors Game to practice DOM manipulation and I have to select a button in JavaScript to know user's choice.
I created an event listener to each button but somehow when the event fires it's target is one of the button's children nodes (in my search I discovered that this is related to event bubbling I think).
I tried to use capture, but it's not working too.
So I'm trying to use a recursive function to grab the parent node until it reaches the button itself. But in the console, when it actually reaches the button node, it returns undefined and doesn't attribute to buttonChecked.
let buttons = document.querySelectorAll('.game__button')
buttons.forEach(button => button.addEventListener('mousedown', rockPaperScissors, {capture:true}));
function checkValidOption(element){
if(element.hasAttribute('data-option')){
console.log(element.nodeName);
return element;
}
else {
console.log("Not Found it")
element = element.parentNode;
checkValidOption(element);
}
}
function rockPaperScissors(event) {
let buttonPressed = event.target;
let buttonChecked = checkValidOption(buttonPressed);
console.log(buttonChecked);
// A lot of comments below
}
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="images/icons8-rock-80.png" alt="">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
Try event.currentTarget to get the component the listener was added to, rather than an internal element.
See: Event.currentTarget
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.
The event.target could be the button, or any child within the button. If we only want the button, no matter where it is clicked, reference the event's current target (the owner of the event).
let buttonPressed = event.currentTarget; // Only need to change this line
let buttons = document.querySelectorAll('.game__button')
buttons.forEach(button =>
button.addEventListener('mousedown', rockPaperScissors, { capture: true }));
function checkValidOption(element) {
if (element.hasAttribute('data-option')) {
console.log(element.nodeName);
return element;
} else {
console.log("Not Found it")
element = element.parentNode;
checkValidOption(element);
}
}
function rockPaperScissors(event) {
let buttonPressed = event.currentTarget;
let buttonChecked = checkValidOption(buttonPressed);
console.log(buttonChecked);
// A lot of comments below
}
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="images/icons8-rock-80.png" alt="">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
All you need to do is set the event up on the button elements and then use this to refer to the button that gets clicked which triggers the handler. From there, you can access anything about that object (including its nested content).
// The click event is more appropriate than mousedown for buttons.
document.querySelectorAll('.game__button').forEach(button => button.addEventListener('click', rockPaperScissors));
function rockPaperScissors(event) {
// In a DOM event handler, "this" will reference the
// DOM object that is handling the event, not necessarially
// the object that triggered the event (which is event.target).
console.log(this);
console.log("You clicked: " + this.querySelector("figcaption").textContent);
}
img { height: 50px; }
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://images.thdstatic.com/productImages/94af8836-0338-4802-914e-04cc71e562ad/svn/backyard-x-scapes-fake-rocks-hdd-rof-rocsb-64_1000.jpg" alt="rock">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
<button class="game__button" data-option="2">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://upload.wikimedia.org/wikipedia/commons/5/52/Continuous_form_paper_%2814p875_x_11%29.jpg" alt="paper">
</div>
<figcaption class="game__figure--figcaption">Paper</figcaption>
</figure>
</button>
<button class="game__button" data-option="3">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://cdn.thewirecutter.com/wp-content/uploads/2015/03/scissors-kitchen-shears-lowres.jpg" alt="scissors">
</div>
<figcaption class="game__figure--figcaption">Scissors</figcaption>
</figure>
</button>
Well you just need to store the value of recursion in a variable. As simple as that.
Like this in else block -:
console.log("Not Found it");
element = element.parentNode;
let buttonElement = checkValidOption(element);
return buttonElement;
Full function -:
function checkValidOption(element) {
if (element.hasAttribute("data-option")) {
console.log(element.nodeName);
return element;
} else {
console.log("Not Found it");
element = element.parentNode;
let buttonElement = checkValidOption(element);
return buttonElement;
}
}
This question already has answers here:
onClick to get the ID of the clicked button
(18 answers)
Closed 3 years ago.
How can I get the ID of a child node of an element when the parent node has many child nodes?
<div id="parentID">
<div id="id_1"></div>
<div id="id_2"></div>
<div id="id_3"></div>
...
<div id="id_n"></div>
</div>
In the above code example, how can I get the ID of any child node when the user clicks it?
Assign an eventListener to the parent element and when clicking on the child
element retrieve their id attribute. Here is a code sample:
document.getElementById('parentID').addEventListener('click', (e) => {
console.log(e.target.id)
})
<div id="parentID">
<div id="id_1">id1</div>
<div id="id_2">id2</div>
<div id="id_3">id3</div>
<div id="id_n">idn</div>
</div>
Sure, events in the dom propagate and move from child to parent evnetually.
You can add a single event listener and check its target property. This technique of adding a single event listener and routing based on children is called event delegation.
document.querySelector("#parentID").addEventListener('click', (e) => {
// e is an event object
// e.target; is whomever was actually clicked
if (e.target.id === 'id_1') {
// do something specific.
}
});
Note that in general having sequential IDs in your code is a code smell indicating bad separation of concerns. So make sure you know it's the right thing to do before doing it.
you can try this.
function byid( e ){ return document.getElementById( e ); }
var id1 = byid( "id_1" ),
id2 = byid( "id_2" ),
id3 = byid( "id_3" ),
id4 = byid( "id_4" );
[id1,id2,id3,id4].forEach( e => {
e.addEventListener( "click",getId );
function getid( e ){
alert( e.getAttribute( "id" ) );
}
} );
I'm trying to add an event listener on some repeating innerHTML. I.E for every lot of HTML added by innerHTML, I'll also need to add a click event onto it.
To complicate things I'm also importing a data set from another JS file imported under the name data. As you can see in the code I need the data inside the event listener to be specific to the for loop iteration of the innerHTML so that when I fire the event listener I can see the correct, corresponding data.
This is my attempt:
JS:
import data from './data.js';
import img from './images.js';
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
for (let i = 0; i < data.lists.length; i++) {
let obj = eval(data.lists[i]);
let totalReminders = getTotalReminders(obj);
lists.innerHTML += `
<div class="flex-between row list">
<h4>${obj.name}</h4>
<div class="flex-between sections">
<h4 class="number-width flex-c">${obj.items.length}</h4>
<h4 class="number-width flex-c">${totalReminders}</h4>
<div class="text-width flex-end">
<h4 class="date">${obj.created}</h4>
<img class="img-radius" src="${img.symbols[3]}" alt="Delete Bin">
</div>
</div>
</div>
`;
const list = document.querySelector('.list');
list.addEventListener('click', () => { // click event
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
};
};
const getTotalReminders = passed => { // find total reminders
let total = 0;
for (let i = 0; i < passed.items.length; i++) {
total += passed.items[i].reminders;
};
return total;
};
At the moment ONLY the first iteration of innerHTML += has an event listener attached and when I click on it I see the data that should be corresponding the last iteration.
What am I doing wrong here?
You need to move the code that sets up the event handlers so that it is outside of your for loop and runs after that loop is finished. Then, instead of .querySelector(), which only returns the first matching element, you need .querySelectorAll() to return all matching elements. After that, you'll loop through all those elements and set up the handler.
You'll also need to change how your obj variable is declared so that it will be in scope outside of the for loop. Do this by declaring it just before the loop, but assigning it inside the loop:
let obj = null; // Now, obj is scoped so it can be accessed outside of the loop
for (let i = 0; i < data.lists.length; i++) {
obj = eval(data.lists[i]);
And, put the following just after the for loop finishes:
// Get all the .list elements into an Array
const list = Array.prototype.slice.call(document.querySelectorAll('.list'));
// Loop over the array and assign an event handler to each array item:
list.forEach(function(item){
item.addEventListener('click', () => {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
});
With all this said, your approach here is really not very good. There is almost always another option than to use eval() for anything and using .innerHTML is usually something to avoid due to its security and performance implications. Using it in a loop is almost always a bad idea. You really should be using the DOM API to create new elements, configure them and inject them into the DOM. If you must use .innerHTML, then build up a string in your loop and after the loop, inject the string into the DOM via .innerHTML, just once.
One options is to look at event delegation/bubbling. The basic principle here is you add the event handler to a parent object, in this case <div id="lists"></div>. Then when the event is fired you query the target of that event to see if it matches your element.
Using this technique you don't have to re-bind event handlers when new items are added, particularly useful if the items are added by user interaction.
In your case it would look something like:
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
//Now that the parent element is added to the DOM
//Add the event handler
lists.addEventListener("click",function(e) {
// e.target was the clicked element
if (e.target && e.target.matches(".list")) {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
}
//Add Items etc
});
NOTE Scots comments re eval and innerHTML apply equally to this answer.