JS event bubbling - How can I avoid the changing targets? - javascript

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.

Related

In Event Propogation, how does the event know when to fire?

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>

Debouncing event handlers in AlpineJS x-init directive

I'm trying to add scroll event listeners to x-refs (I don't want to add them in the DOM with #scroll.debounce, because I want it to be as portable as possible).
Codepen here: https://codepen.io/skttl/pen/vYXowBY?editors=1111
<div x-data="xOverflow()" x-init="init()" style="width:50%;border:1px solid red;position:relative;overflow:hidden;">
<div x-ref="wrapper" style="overflow-x:auto;">
<div x-ref="content" style="width:1000px;border:1px solid blue; height:500px;">
<div>wrapper.clientWidth: <span x-text="$refs.wrapper.clientWidth"></span></div>
<div>content.clientWidth: <span x-text="$refs.content.clientWidth"></span></div>
<div>overflow: <span x-text="overflow"></span>
</div>
</div>
<div x-show="overflow" style="position:absolute;top:0;left:90%;right:0;bottom:0;background:rgba(0,0,0,.15);"></div>
</div>
function xOverflow() {
return {
overflow:false,
// methods
setFromResize() {
this.overflow = this.$refs?.wrapper?.clientWidth < this.$refs?.content?.clientWidth
console.log(`resize: overflow is ${this.overflow}`);
},
setFromScroll(e) {
this.overflow = !(e.target.scrollLeft == this.$refs?.content?.clientWidth - this.$refs?.wrapper?.clientWidth);
console.log(`scroll: overflow is ${this.overflow}`);
},
init() {
window.addEventListener("resize", event => _.debounce(this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", event => _.debounce(this.setFromScroll(event)));
}
}
}
What I am trying to detect if the clientWidth of $refs.content is larger than $refs.wrapper. When the window resizes, this should be detected again.
In addition to that, I want to show an overlay (if content is overflowing), that should be removed when scrolled to the end.
I could do it by just slapping #resize.window.debounce on the root element, and #scroll.debounce on the wrapper element. But I want this component to be portable, by just adding x-data, x-init and x-refs, without worrying about attaching event listeners.
I tried adding lodash debounce to the eventlisteners, but the functions gets called for each event, and not debounced. Check the console.log for proof.
Can anyone help me get this right?
So turns out I am an idiot, and I just needed to move things around a bit :)
From
window.addEventListener("resize", event => _.debounce(this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", event => _.debounce(this.setFromScroll(event)));
to
window.addEventListener("resize", _.debounce(event => this.setFromResize(), 250));
this.$refs?.wrapper.addEventListener("scroll", _.debounce(event => this.setFromScroll(event)));

React- Pass event to inner child

I have an element and a child element, both have an event handler, however, the parent handler seems to block the child handler.
I have tried to use https://reactjs.org/docs/events.html#supported-events.
sadly its didn't work or I didn't use it correctly.
Where is some of my code:
classes:
Matrix and Tile
the Matrix render function contains:
<element onMouseDown={this._onPressDown} onMouseUp={this._onPressUp}><Matrix/></element>
Matrix include the Tile object as NxN matrix as following:
[[<Tile/>,<Tile/>],[<Tile/>,<Tile/>]]
the Tile render functioncontains:
<div onClick={this._onClick} onMouseEnter={this._onEnter}><Matrix/></div>
The child (tile) function _onClicked is not beening called.
Edit:
buildMatrix(){
let [tileWidth,tileHeight] = this.calculateTileSize();
this.reBuildMatrix = !this.reBuildMatrix;
return (<element onMouseDown={this._onPressDown} onMouseUp={this._onPressUp} stopPropagation>
<table className='matrix' key={this.reBuildMatrix}>{
this.state.tileMatrixMap.map((rowItem,row)=>(
<tr>
{
rowItem.map((tile,col)=>(
<th><Tile
width={tileWidth}
height={tileHeight}
isWall={tile.isWall}
isPath={tile.isPath}
onClick={tile.onClick}
position= {[row,col]}
backgroundColor={tile.isWall?this.wallColor:(tile.isPath? this.pathColor:this.backgroundColor)}
/></th>
))
}
</tr>
))}
</table>
</element>);
When this line is disabled this.reBuildMatrix = !this.reBuildMatrix;
the code work as expexted, how ever if I enable the line the function is not called

change this style attribute from class element on click

const expandIt = document.querySelectorAll('.k-grid')
.forEach(el => el.addEventListener('click', () => {
el.classList.add('pressed') // This wont work getting HTML obj etc in console
console.log('pressed' + el)
$(this).css({'white-space':'normal'}); // this works in console
}))
Above is what I have; I am simply trying to set white space to normal on current clicked element. I think using this would work - classList doesn't seem to be working I guess from how/when dynamic elems are injected. I am looking for plain JS, or ideally ES6.
Add event as a function parameter and set el = event.target
const expandIt = document.querySelectorAll('.k-grid')
.forEach(el => el.addEventListener('click', (event) => {
const el = event.target;
el.classList.add('pressed') // This will work getting HTML obj etc in console
console.log('pressed' + el);
}))
.k-grid {padding: 1em; float: left;}
.pressed {font-weight: bold;}
<div class="k-grid">Hello</div>
<div class="k-grid">Hello</div>
<div class="k-grid">Hello</div>
HTML DOM addEventListener() Method
function
Required. Specifies the function to run when the event occurs.
When the event occurs, an event object is passed to the function as
the first parameter. The type of the event object depends on the
specified event. For example, the "click" event belongs to the
MouseEvent object.
Bold emphasis mine

Event listener outside of Shadow DOM won't bind to elements inside of Shadow DOM

I have an Angular web component installed on my site. It uses Shadow DOM so it's super fast (which it has to be in my case).
On my site I also have a shortcut on h which opens up a popup that displays some helpful information. It's a must that this h keybinding stays as it is. Example code of how it was implemented can be seen here: https://jsfiddle.net/js1edv37/
It's a simple event listener that listens on document:
$(document).on("keyup", function(e) {
}
However, this also gets triggered when my web component has focused textarea or input elements. This happens because it uses Shadow DOM, which a script from the outside cannot access.
You can test it by pressing h on the keyboard inside and outside of the input and textarea elements.
Is there a way to let my script from outside of the Shadow DOM web component, still listen for the keyup event, but make it listen for all elements on the page? Even the ones inside the Shadow DOM.
In the Web Component, get the input element with a querySelector() call on the shadowRoot property:
let textareainshadow = div.shadowRoot.querySelector( 'textarea' )
Then listen to the keyup event and stop its propagation with the help of the stopImmediatePropagation() method.
textareainshadow.addEventListener( 'keyup' , ev => {
console.log( 'caught', ev.type )
ev.stopImmediatePropagation()
})
https://jsfiddle.net/7mkrxh25/1/
If you save the reference to the shadow root you can always access it's children as search on those
$(document).on("keyup", function(e) {
let focusedInputs = $("input:focus, textarea:focus").length + $(shadow).children("input:focus, textarea:focus").length;
if (focusedInputs > 0) {
return true;
}
if (e.keyCode === 72) {
trigger();
}
});
function trigger() {
alert("If this was triggered, everything is perfectly fine");
}
let div = document.querySelector("div");
let shadow = div.createShadowRoot();
shadow.innerHTML = "<textarea>This shouldn't fail</textarea>";
textarea {
width: 500px;
height: 100px;
}
input {
width: 250px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<textarea>Some stuff here</textarea>
<br />
<input type="text" value="Some more text here" />
<br />
<br />
<h1>Shadow DOM element WON'T fail now :)</h1>
<div></div>
Fiddle

Categories

Resources