I have a situation where I need to use jQuery's $.fn.one() function for a click event, but I don't want it to apply to the next occurrence of the event (like it usually does), I want it to apply to the occurrence immediately after that, and then unbind itself (like .one() normally does).
The reason I don't want .one() to apply to the first occurrence is because I'm binding to the document from an event handler invoked earlier in the bubbling phase, so the first time it gets to document it'll be part of the same event. I want to know when the very next click event occurs.
Note: I do not want to use .stopPropagation() because it will potentially break other parts of my app.
Here are the two options I've come up with, though it seems like there must be a more elegant solution.
The double bind method:
$(document).one('click', function() {
$(document).one('click', callback);
});
The setTimeout method:
setTimeout(function() {
$(document).one('click', callback);
}, 1);
Both methods work just fine, but here's my question. I have no idea what the performance implications are for either setTimeout or frequent event binding and unbinding. If anyone knows, I'd love to hear it. But more importantly, I'd like some suggestions on how to measure this stuff myself for future situations like this.
I love sites like http://jsperf.com, but I don't know if it would really be helpful for measuring stuff like this.
And obviously, if someone sees a much better solution, I've love to hear it.
I find the double-bind method quite elegant - I think it accurately reflects your actual intent, and it only takes two lines of code.
But another approach is rather than using .one() you could use .on() and update the event object associated with the first event, adding a flag so that the callback will ignore the first time it is called:
function oneCallback(e) {
if (e.originalEvent.firstTimeIn)
return;
alert("This is the one after the current event");
$(document).off("click", oneCallback);
}
$("div.source").click(function(e) {
e.originalEvent.firstTimeIn = true;
$(document).on("click", oneCallback);
});
Demo: http://jsfiddle.net/q5LG4/
EDIT: To address your concerns about not modifying the event object (or any object you don't own) you could store a firstTime flag in a closure. Here's a rather dodgy .oneAfterThis() plugin that takes that approach:
jQuery.fn.oneAfterThis = function(eventName, callback) {
this.each(function() {
var first = true;
function cb() {
if(first){
first = false;
return;
}
callback.apply(this,[].slice.call(arguments));
$(this).off(eventName,cb);
}
$(this).on(eventName, cb);
});
};
$(someseletor).oneAfterThis("click", function() { ... });
I'm sure that could've done that more elegantly (perhaps I should've bothered to look at how jQuery implements .one()), but I just wanted to whip something up quickly as a proof of concept.
Demo: http://jsfiddle.net/q5LG4/1/
Related
UPDATE turns out the code is actually working see my answer below
I'm having some troubles here. I thought I found my answer in the .one method, but apparently, .one means ONLY ONCE PER PAGE PER ANYTHING EVER which isn't exactly what I was going for. Here's what my intention was:
$("#someID").one('mouseover', function() {
//do some stuff
});
$("#someOtherID").one('mouseover', function() {
//do some stuff
});
My expectation was that once that first one fired, that mouseover event would no longer fire for THAT ELEMENT.
The problem with this is that once the first one fires, the second one will not fire either. So the .one method appears to be disabling ALL mouseover events for ALL elements after that first one fires.
I did not expect this, I expected the .one to only apply to that first element. Is this just a flaw in my understanding of the .one method or am I coding wrong?
If it's just a flaw in my understanding, could someone point me in the right direction to correct my code?
Thank you in advance!
This is embarassing, I hope I don't get dinged for this and blocked again from stackoverflow (the easiest thing ever to get blocked from and the hardest to get unblocked).
First, #CertainPerformance, thanks for taking the time to look at my question. My real code didn't have the two mistakes you mentioned, I updated my post to reflect the correct syntax.
I'll be honest, my code is working now, and I have no idea why. I suspect I've been dealing with some crazy caching issues which frustrates me because I'm using inMotionHosting which has really great reviews, and I have caching disabled in cPanel.
If anything, maybe this thread will benefit somebody searching "how to make event fire only once in javascript".
You could make the callback run once like this:
// Extend the function prototype
Function.prototype.once = function() {
// Variables
var func = this, // Current function
result;
// Returns the function
return function() {
// If function is set
if(func) {
// Executes the function
result = func.apply(this, arguments);
// Unset the function, so it will not be called again
func = null;
}
// (:
return result;
};
};
// Bind the event to the function you will use as a callback
$("#someID").on('mouseover', function() {
console.log('just once');
}.once());
Is there any difference between the following: (Is there any reason to avoid example One?)
One:
$("#stuff").on("resize", function() { doThis(); });
$("#stuff").on("resize", function() { doThat(); });
Two:
$("#stuff").on("resize", function() {
doThis();
doThat();
});
Straightforwardly, there's no real difference.
In real-world code,
You may need to attach the two handlers in different places in your code, in which case you have to use version One (or similar).
You may want the ability to selectively detach handlers, in which case, the event can be namespaced as follows:
Attach handlers:
$("#stuff").on("resize.A", function() { doThis(); });
...
$("#stuff").on("resize.B", function() { doThat(); });
Detach one handler:
$("#stuff").off("resize.A");
The handler for resize.B remains attached (ie. doThis() will not be called but doThat() will be called) .
In your second example, if doThis() throws an exception then doThat() won't run. Not the case with the first example.
The only difference is that two distinct event handler functions are stored and executed wheres the second snippet is satisfied with one.
Do the math, second snippet is more elegant. I won't start talking about performance, but if we would bind like "hundreds" of methods that way, it becomes obvious that you don't want to bind multiple handlers.
The second will be a little more performant.
In the first example, you have the overhead of two function calls when the resize event fires (in addition to the calls to doThis and doThat). In the second example, you only have one event handler being called.
I'm in the process of authoring a completely client side web language reference site. A problem that I encountered today; I have a side panel that is a unordered list of terms and they have onmouseover event listeners. I decided it would be a good idea to add a delay prior to execution and cancel the event at run-time if the mouse was no longer over that element. This is what I've come up with but I feel there must be a better way.
var currentXCoordinate=0
var currentYCoordinate=0
var elementFromCurrentMousePosition=0
function trackCurrentMousePosition(event) {
if (document.elementFromPoint(event.clientX, event.clientY).nodeName=="SPAN") {
elementFromCurrentMousePosition=document.elementFromPoint(event.clientX, event.clientY).parentNode
}
else {
elementFromCurrentMousePosition=document.elementFromPoint(event.clientX, event.clientY)
}
return (currentXCoordinate=event.clientX, currentYCoordinate=event.clientY, elementFromCurrentMousePosition)
}
function initPreview(event, obj) {
arg1=event
arg2=obj
setTimeout("setPreviewDataFields(arg1, arg2)", 100)
}
function setPreviewDataFields(event, obj) {
if ('bubbles' in event) {
event.stopPropagation()
}
else {
event.cancelBubble=true
}
if (elementFromCurrentMousePosition!=obj) {
return 0;
}
The code goes on to do all the wonderful stuff I want it to do if execution wasn't cancelled by the previous if statement. The problem is this method is seeming to be really processor intensive.
To sum it up: on page load all my event listeners are registered, cursor position is being tracked by a onmousemove event. Applicable list items have a onmouseover event that calls the initPreview function which just waits a given period of time before calling the actual setPreviewDataFields function. If at run-time the cursor is no longer over the list element the function stops by return 0.
Sadly that's the best I could come up with. If anyone can offer up a better solution I would be very grateful.
Why not just use mouseout to tell when the mouse leaves an element? Running all of that code every time the mouse moves isn't ideal.
Also, you really shouldn't pass a string to setTimeout like that. Instead, pass a function. As a bonus, you can get rid of those evil global variables arg1 and arg2. With those being globals, I think you will run into issues if init gets called again before the timeout expires.
I read the following on the web this weekend and I wanted to know if most others consider this the right (better) way of doing things.
This is not the best (right) way to do things:
$(document).ready(function() {
$('#magic').click(function(e) {
$('#yayeffects').slideUp(function() {
// ...
});
});
$('#happiness').load(url + ' #unicorns', function() {
// ...
});
});
That this is better:
var PI = {
onReady : function() {
$('#magic').click(PI.candyMtn);
$('#happiness').load(PI.url + ' #unicorns', PI.unicornCb);
},
candyMtn : function(e) {
$('#yayeffects').slideUp(PI.slideCb);
},
slideCb : function() { ... },
unicornCb : function() { ... }
};
$(document).ready(PI.onReady);
Does one perform better than the next? Easier for debugging?
Thoughts? Comments?
If you have a stacktrace with lots of anonymous functions in it it takes significantly more time to find out where the error exactly has happened and from where it was called. so plus 1 for second.
The code inside an event handler has often not much to do with the code where the handler gets registered and should therefore be in a separate function/module. plus 1 for second.
Using anonymous functions for listeners is also bad because in case you have to remove this listeners (which most people don't care about) you can remove only them and you don't have to care about accidentally removing other listeners form other parts of code. plus 1 for the second.
Put the related functions into a single object is not necessarily the best. Mostly the onReady function is bad if you use the behaviour of the listeners for different dom objects.
don't care about performance. a listener is usually not called that often that it matters.If it does, there is most likely a problem somewhere else.
The second variant is reusable - you can reuse slideCb and other handlers for other events for other controls.
Maybe I'm totally missing something about even handling in jQuery, but here's my problem.
Let's assume there are some event binding, like
$(element).bind("mousemove", somefunc);
Now, I'd like to introduce a new mousemove binding that doesn't override the previous one, but temporarily exclude (unbind) it. In other words, when I bind my function, I must be sure that no other functions will ever execute for that event, until I restore them.
I'm looking for something like:
$(element).bind("mousemove", somefunc);
// Somefunc is used regularly
var savedBinding = $(element).getCurrentBinding("mousemove");
$(element).unbind("mousemove").bind("mousemove", myfunc);
// Use myfunc instead
$(element).unbind("mousemove", myfunc).bind("mousemove", savedBindings);
Of course, the somefunc is not under my control, or this would be useless :)
Is my understanding that is possible to bind multiple functions to the same event, and that the execution of those functions can't be pre-determined.
I'm aware of stopping event propagation and immediate event propagation, but I'm thinking that they are useless in my case, as the execution order can't be determined (but maybe I'm getting these wrong).
How can I do that?
EDIT: I need to highlight this: I need that the previously installed handler (somefunc) isn't executed. I am NOT defining that handler, it may be or may be not present, but its installed by a third-party user.
EDIT2: Ok, this is not feasible right now, I think I'm needing the eventListenerList, which is not implemented in most browsers yet. http://www.w3.org/TR/2002/WD-DOM-Level-3-Events-20020208/changes.html
Another way could be to use custom events, something along these lines:
var flag = 0;
$(element).bind("mousemove", function() {
if(flag) {
$(this).trigger("supermousemove");
} else {
$(this).trigger("magicmousemove");
}
}).bind("supermousemove", function() {
// do something super
}).bind("magicmousemove", function() {
// do something magical
});
$("#foo").click(function() {
flag = flag == 1 ? 0 : 1; // simple switch
});
Highly annoying demo here: http://jsfiddle.net/SkFvW/
Good if the event is bound to multiple elements:
$('.foo').click(function() {
if ( ! $(this).hasClass('flag')) {
do something
}
});
(add class 'flag' to sort of unbind, add it to 'bind')