jQuery one() handler executed twice - javascript

I have a bug in my jQuery/animate.css code. The problem is an event handler that is called twice but it should be executed only one. You can find the code here, the important line is below:
JSFiddle
$('#tiles').addClass(tiles_in).one(animation_end, function() {
// called twice
log('tiles in');
});
In order to reproduce the bug, follow these steps:
Wait for the animation to end, which will output these logs
change_tiles
tiles out
tiles in
Click on the yellow div, here are the new logs
tile out // normal
tiles in // why ?
Tiles in should not be called again as the event is one(). And the documentation says "The .replaceWith() method removes all data and event handlers associated with the removed nodes." So I don't know.

why ?
A call to .one() is within first .one() event handler where the the event is attached to the same selector #tiles

I have found it myself after logging the event that was fired.
https://jsfiddle.net/uhz6Ly0s/8/
$('#tiles').addClass(tiles_in).one(animation_end, function(event) {
log('tiles in, event=' + event.type);
});
The first event is animationend, and the second is webkitAnimationEnd. For some reasons, the second one is fired at the end of the click event.

Related

JavaScript onMouseDown and onClick events versus event queue

I have following simple JS code (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js):
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here? As I understand it, the moment script is executed, handler mousedown is added to even queue. When I actually click button, this handler is run, so it's taken from event queue, added to call stack and it is executed. When it is executed, handler "click" is added to event queue.
How actually event onClick is triggered after onMouseDown? How is that related to event queue? Why onMouseDown handler is run before click event happens? I'm asking because I have a lot more complex code where result is different in different scenarios.
When user navigates to page in SPA which contains similiar script, and then clicks button 'baton' order is:
mousedown event -> handler mousedown -> handler click -> click event
And when user reloads page, so SPA is loaded right on that page, and clicks button 'baton' order is:
mousedown event -> click event -> handler mousedown
I am seeking answer and truth. Any help will be greatly appreciated.
Ps. Unfortunately I'm not able to reproduce this error in example repository - it happens in quite complex web app which production code I can't share here for obvious reasons.
Ps2. Just to clarify, because probably it isn't stated clearly enough: I'm not asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event". This is NOT obvious, because handlers are not run immediately. In order of handler to be run, it first have to wait to call stack to be empty, so event queue can be processed by JS engine.
The browser tracks the element you clicked the moused down on. Then it tracks the element you lifted the mouse button on. If the element you lifted the mouse button on is the same element or a child element of the target element. Then a click event is dispatched to the last element you lifted the mouse on. The event then propagates up the element chain to every parent element unless the event is told to stop propagating.
If you click down on element A and mouse up on element B. Then A gets mouse down event, and B gets mouse up event, but neither get a click event. Same thing if you navigate the browser to another page in between the mouse down and mouse up.
From MDN Web Docs
An element receives a click event when a pointing device button (such as a mouse's primary mouse button) is both pressed and released while the pointer is located inside the element.
So there is a mouseup event and then the click event.
EDIT after question edit:
"why mousedown HANDLER is run before click event?"
Your already executing mousedown handler registers the click handler so how should the click handler run before it?
All click handlers registered in all previous mousedown handlers will run after the mousedown and mouseup events too.
Perhaps we should start by clarifying a few things.
Events in the browser, are modeled more like a "nesting hierarchy", then a queue -- How it works is referred to as Event Bubbling -- [Wikipedia][1]
But, essentially what you are doing, when adding an EventListener, is hooking into one or more points of the DOM, and saying hey, when X Event passes through here, use function Y to handle it, before passing it along up the stack.
Once an EventListener has been "added" it remains active waiting to be given an event. What exactly it does is defined in its handler function.
let myYFunction = function( e ) { ... }
let myXListener = baton.addEventListern('X event', myYFunction );
// at this point, anytime 'X event' happens to baton, myYFunction will
// be called to handle it...
Now let's take a look at your examples, lets break things down a little,
const baton = document.querySelector('button');
This first line, is simply querying the DOM, to find the first element of type 'button' in the page. Right... This is "where" we want to insert our event handler. We could add them to any element, anywhere in the DOM, we could even hook into the 'body' element if we wanted to.
Ok, then you have this bit,
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
Which is "nesting" the creation of the 'click' Event Listener, but only after a 'mousedown' event has been "handled". There is no real reason the 'click' event had to be registered within the function body of the mousedown handler.
If we re-write it a bit, it may be clearer what is actually going on.
baton.addEventListener('mousedown', (e) => {
console.log('baton mousedown');
}
baton.addEventListener('click', (e) => {
console.log('baton click');
});
Additionally I would also point out, that how it is being done currently "works" -- but it is actually hiding a tiny bit of sloppy coding... you see every time the 'mousedown' event is triggered a new 'click' eventListener is being registered... so eventually you may end up with many, many, many click handlers responding to a single 'click' event... Check out MDN to learn more about [this][2]
I hope this answers your initial questions as to what is going on.
To your question "When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here?" -- To me, it would look something like this:
a 'mousedown' eventListener is added, however nothing "executes"
a 'mousedown' event takes place, now your 'mousedown' listener executes its function, which in turn logs out to the console, and registers a new 'click' handler -- but again, does not execute.
Moving forward, steps 1 and 2 are repeated for every 'mousedown' seen by baton. Additionally, for any 'click' event passed through baton --- which happens after every 'mousedown' on baton:
A 'click' event occurs, your 'click' handler is then executed and logs out to the console.
SPA event handling strategies
When working with SPAs, where multiple "pages" are displayed, in the same page load... it can get messy, all these event listeners hanging around piling up on one another. If you are going to employ eventListeners between "Pages" of your SPA, you might want to look into how to "remove" them too. - [MDN][3]
That way, you only have eventListeners active for the current "Page" of your SPA.
Also, consider "generalizing" your handlers, and attaching them higher up in the DOM... This would allow you to have only a few event listeners which "route" events to their "logical" handlers.
Random/Different Behaviors
With the steps outlines above, 1, 2 and 3 and how they don't all happen at the same time. You will see what appears to be random output to the console... try and run something like this, to get a proper sense of things:
let cCount = 0;
let mCount = 0;
let tCount = 0;
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('mousedown # ' + (mCount++) + ' order:' + tCount++);
baton.addEventListener('click', (e) => {
console.log('click # ' + (cCount++) + ' order:' + tCount++);
});
});
[1]: https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event%20bubbling%20is%20a%20type,Provided%20the%20handler%20is%20initialized).
[2]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[3]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener

how to add custom events to svg.js

I'm testing svg.js library, and have found problems declaring custom events. Here is the fiddle. Clicking on the first circle should change the color and it works:
circleOne.click(function() {
this.fill({ color: '#f06' })
})
Clicking the second circle should fire the custom event, but it doesn't:
var circleTwo = SVG.select('circle.circle-01');
circleTwo.on('myevent', function() {
alert('ta-da!')
})
function testMe() {
circleTwo.fire('myevent')
}
Changing .fire to .event doesn't help either. Any suggestions? Thanks in advance.
You never use testMe function to fire your event :)
Browser defined events are already fired when an event happens, for the custom event you have correctly defined what happens (alert) when it fires but if you intend it to go off at some point you have to fire it. You also made trigger function but you never used it.
You can fire it on browser defined triggers BUT then simply rather use those events, don't define custom.
Custom events are intended for different use cases. For example, you detected an object is untouched for 10 sec and you wanna notify some other part of the code to react to it. That event is not defined by default, you define it and have custom code checking that fire the event when the condition is met.
Try firing your event on for example click event or simply for testing put this at the end of the scrypt:
testMe();
Now you have fired your custom event when the script loads to that point and executes trigger function.

How to fix problems with mouseup and mousedown?

I have the following code that returns 'haha' when the user clicks up and down. I am having trouble with the amount of times this gets retuned though. The first time I click up/down, 'haha' only gets returned once. The second time I go through this, it gets printed twice. The third time, 'haha' gets printed 3 times and so on. Any advice on why this is happening? I only want 'haha' to be printed once for each up/down.
$(".test").mousedown(function(){
$(".test").mouseup(function(){
console.log('haha');
});
});
You could use one, so that the event handling is unbound after the mouseup :
$(".test").mousedown(function(){
$(".test").one('mouseup', function(){
console.log('haha');
});
});
But it seems simpler to simply use click :
$(".test").click(function(){ console.log('haha') });
You're adding a new mouseup event handler every time you click mouse down. You may want to separate the two, or just use the mouseup handler.
This is because each time the mousedown event is triggered, a new mouseup event is added to the target element.
Remove the mouseup event immediately again on the firing of mouseup event and your problem will be solved.
The problem is that in your code you are adding the "mouseup" handler every time the user clicks. You don't need to wait for the mousedown event in order to add this handler; you should just be able to add it from scratch. If you really want to make sure the mousedown has been triggered first, use a boolean, like so:
var mouseClicked = false;
$(".test").mousedown(function(){
mouseClicked = truel
}).mouseup(function(){
if (mouseClicked ==true){
console.log('haha');
}
});

Order of jQuery On Events and Triggers

See the following:
$('body').on('whyhellothere', function(){
console.log('done...');
});
$('body').triggerHandler('whyhellothere');
This snippet returns:
done...
While if we reverse the order:
$('body').triggerHandler('whyhellothere');
$('body').on('whyhellothere', function(){
console.log('done...');
});
This snippet returns nothing. Why is this the case?
If you shout in the forest, and then I come along, I won't hear anything, would I?
You're registering the event handler after the event was triggered. A registered handler can only listen to events that are triggered after they start listening.
It's simple physics :P
Javascript gets processed top down. It doesn't recognize your on call as some top-level definition that should be processed before something else. So you're calling one function that triggers a handler, that function call looks for all currently registered handlers for that event, of which there are none, and then calls them all (of which there are none). Then you add a new handler to that event listener. And any calls thereafter will iterate the list of handlers (now 1) and call them.

In jQuery, is there any way to only bind a click once?

I have an ajax app that will run functions on every interaction. I'd like to be able to run my setup function each time so all my setup code for that function remains encapsulated. However, binding elements more than once means that the handler will run more than once, which is obviously undesirable. Is there an elegant way in jQuery to call bind on an element more than once without the handler being called more than once?
User jQuery one function like Tom said, but unbind the handler each time before binding again. It helps to have the event handler assigned to a variable than using an anonymous function.
var handler = function(e) { // stuff };
$('#element').unbind('click', handler).one('click', handler);
//elsewhere
$('#element').unbind('click', handler).one('click', handler);
You can also do .unbind('click') to remove all click handlers attached to an element.
You could attach the event to document with the one() function:
$(document).one('click', function(e) {
// initialization here
});
Once run, this event handler is removed again so that it will not run again. However, if you need the initialization to run before the click event of some other element, we will have to think of something else. Using mousedown instead of click might work then, as the mousedown event is fired before the click event.
You can also use .off() if unbind doesn't do the trick. Make sure the selector and event given to .off exactly match the ones initially provided to .on():
$("div.selector").off("click", "a.another_selector");
$("div.selector").on("click", "a.another_selector", function(e){
This is what worked for me in resolving the same ajax reloading problem.
The answer from Chetan Sastry is what you want. Basically just call a $(element).unbind(event); before every event.
So if you have a function like loadAllButtonClicks() that contains all the
$(element).on("click", function (){});
methods for each button on your page, and you run that every time a button is clicked, this will obviously produce more than one event for each button. To solve this just add
$(element).unbind(event);
before every
$(element).on("click", function (){});
and it will unbind all events to that element, then add the one click event.

Categories