Reverse event bubbling in JavaScript - javascript

As you know, events usually bubble up in JavaScript, so the event handler of the element that fires the event is executed first, then the event handler of the parent element is called and so on. This behaviour causes some problems on a project I'm currently working on, I would rather have the execution order reversed.
I figured out a solution that is using timeouts:
$(element).mouseover(function(){
var that = this;
setTimeout(function() {
//actual event handler, but references to "this" are replaced with "that"
},$(this).parents().length)
});
So basically, the event handlers are executed after a short timeout, the waiting time depends on the the depth of the element in the DOM-tree:
The event handler of the the html-element is executed right away, the event handler of the body element is executed after waiting 1ms and so on. So the execution order of the events is reversed.
The results of my first tests are positive, but I'm still not sure if there are any problems or drawbacks with this solution. What do you think of this solution? Other ideas on how to solve this problems are also highly appreciated.

Reverse event bubbling is called capture phase.
See the DOM event architecture
Pass true as the 3rd argument to Event.addEventListener to have it trigger on capture phase
el.addEventListener("click", function () {
console.log("i run in capture");
}, true);
Of course it won't work in legacy platforms. You'll need a DOM3 events shim to emulate the event system. Feel free to contribute to the DOM-shim

You could try to emulate it, but probably it could make lots of troubles.
Here is very very simple example (and on jsfiddle).
The idea is to aggregate callbacks from each event, and calling them in reverse order in document event handler (we also clear our list, to ensure new queue on next click).
<div id="a">
<div id="b">
Click Me!
</div>
</div>
var cb = [];
$(document).click(function(evt){
cb.unshift(function(){
console.log('document');
});
var i;
for(i=0; i<cb.length; ++i){
cb[i].call(null, evt);
}
cb = [];
});
$("#a").click(function(){
cb.unshift(function(){
console.log('div#a');
});
});
$("#b").click(function(){
cb.unshift(function(){
console.log('div#b');
});
});
$('a').click(function(evt){
cb.unshift(function(evt){
evt.preventDefault();
console.log('a');
});
});
Maybe you need to change your design? Could you post more information, why you need event capturing?

I can see one huge drawback of your solution is fact, that for every handled event you have to traverse DOM upwards starting with this to establish number of parents.
Traversing in such manner in every event handler = low performance.

Related

Change the Attr and do new function not working in jQuery [duplicate]

I have a site that uses AJAX to navigate. I have two pages that I use a click and drag feature using:
$(".myDragArea").mousedown(function(){
do stuff...
mouseDrag = true; // mouseDrag is global.
});
$("body").mousemove(function(){
if (mouseDrag) {
do stuff...
}
});
$("body").mouseup(function(){
if (mouseDrag) {
do stuff...
mouseDrag = false;
}
});
I just type that out, so excuse any incidental syntax errors. Two parts of the site use almost identical code, with the only difference being what is inside the $("body").mouseup() function. However, if I access the first part, then navigate to the second part, the code that runs on mouseup doesn't change. I have stepped through the code with Firebug, and no errors or thrown when $("body").mouseup() is run when the second part loads.
So, why doesn't the event handler change when I run $("body").mouseup() the second time?
Using $("body").mouseup( ... ) will add an event handler for the body that is triggered at mouseup.
If you want to add another event handler that would conflict with current event handler(s) then you must first remove the current conflicting event handler(s).
You have 4 options to do this with .unbind(). I'll list them from the least precise to the most precise options:
Nuclear option - Remove all event handlers from the body
$("body").unbind();
This is pretty crude. Let's try to improve.
The elephant gun - Remove all mouseup event handlers from the body
$("body").unbind('mouseup');
This is a little better, but we can still be more precise.
The surgeon's scalpel - Remove one specific event handler from the body
$("body").unbind('mouseup', myMouseUpV1);
Of course for this version you must set a variable to your event handler. In your case this would look something like:
myMouseUpV1 = function(){
if (mouseDrag) {
do stuff...
mouseDrag = false;
}
}
$("body").mouseup(myMouseUpV1);
$("body").unbind('mouseup', myMouseUpV1);
$("body").mouseup(myMouseUpV2); // where you've defined V2 somewhere
Scalpel with anesthesia (ok, the analogy's wearing thin) - You can create namespaces for the event handlers you bind and unbind. You can use this technique to bind and unbind either anonymous functions or references to functions. For namespaces, you have to use the .bind() method directly instead of one of the shortcuts ( like .mouseover() ).
To create a namespace:
$("body").bind('mouseup.mySpace', function() { ... });
or
$("body").bind('mouseup.mySpace', myHandler);
Then to unbind either of the previous examples, you would use:
$("body").unbind('mouseup.mySpace');
You can unbind multiple namespaced handlers at once by chaining them:
$("body").unbind('mouseup.mySpace1.mySpace2.yourSpace');
Finally, you can unbind all event handlers in a namespace irrespective of the event type!
$("body").unbind('.mySpace')
You cannot do this with a simple reference to a handler. $("body").unbind(myHandler) will not work, since with a simple reference to a handler you must specify the event type ( $("body").unbind('mouseup', myHandler) )!
PS: You can also unbind an event from within itself using .unbind(event). This could be useful if you want to trigger an event handler only a limited number of times.
var timesClicked = 0;
$('input').bind('click', function(event) {
alert('Moar Cheezburgerz!');
timesClicked++;
if (timesClicked >= 2) {
$('input').unbind(event);
$('input').val("NO MOAR!");
}
});​
Calling $("body").mouseup(function) will add an event handler.
You need to remove the existing handler by writing $("body").unbind('mouseup');.
jQUery doesn't "replace" event handlers when you wire up handlers.
If you're using Ajax to navigate, and not refreshing the overall DOM (i.e. not creating an entirely new body element on each request), then executing a new line like:
$("body").mouseup(function(){
is just going to add an additional handler. Your first handler will still exist.
You'll need to specifically remove any handlers by calling
$("body").unbind("mouseUp");

Javascript pause event propagation

I need to be able to chain click events, and temporarily pause event propagation between them.
So, for a given element, it runs three different click events, but the second one needs user input, so it pauses propagation while the user fills in the form and then continues.
clickAction2 = ->
#Pause event propagation
somehow.pauseEventPropegationRightHere()
#Go and handle the dialogs, user input, JS requests
goDoSomething().thenDoCallback( ->
#User is now authenticated completely.
somehow.continueEventPropegationAsIfNothingHappened()
)
In an effort to allow single responsibility, and events chained higher/lower shouldn't have knowledge that the event propagation was paused and nothing should be called twice.
No, the three click events can't be called sequentially from another function or any similar primitive solution.
This is in relation to AngularJS directives, but the solution does not need to rely on it.
There is a similar question but none of the answers are satisfactory: How to continue event propagation after cancelling?.
Edit:
What I need is a cleaner way to call e.stopImmediatePropagation(), and then continue from that point. As of right now, my best option is by manually entering the jQuery "private' data[1] and calling the functions manually.
$._data( $(element)[0], 'events' ).click[].handler()
I had a similar issue myself and I hope my solution is satisfactory because I realize it may not directly apply to your situation. However hopefully the principle I show here helps you find a solution of your own.
The problem (if I understand correctly) is that you need a way to prevent a child element from triggering an event if a parent element has triggered the same event at essentially the same time. Whereas e.stopImmediatePropagation() prevents event bubbling, effectively halting any parent elements from triggering the event. I believe the solution is using a setTimeout() with a zero millisecond delay to perform the desired function and if the triggering event ever occurs again while the setTimeout() is in use, use clearTimeout() to stop the previous event from occuring. The best way I can demonstrate this approach is by creating a parent and child element and watching the mouseleave event.
Here is the JS/jQuery:
var timerActive = false;
$('#div1, #div2').mouseleave(function(e) {
var trigger = $(this).attr('id'); // for monitoring which element triggered the event
if (timerActive) {
clearTimeout(announce); // stops the previous event from performing the function
}
window.announce = setTimeout(function() {
alert('mouse exited'+trigger); // trigger could be use in an if/else to perform unique tasks
timerActive = false;
},0);
timerActive = true;
});
Here is a JSFiddle demo: http://jsfiddle.net/XsLWE/
Since the events are triggered sequentially from child to parent, clearing each previous timeout effectively waits for the last or top-most element to trigger the event. At which point you could use an IF/ELSE statement to perform desired tasks. Or perform one function then a callback on complete.
In the demo, the effect is that on the left and bottom edges of the div elements, each div element is allowed to trigger the event individually. However on the top and right edges of div2, the only element allowed to trigger the event is div1.
Again, from your original post, I take it you are after something slightly different. But perhaps this approach will help you in some way.
Try something like
elem.on('event',function(e) {
e.stopPropagation();
goDoSomething(function(){
elem.parent().trigger(e);
});
});
goDoSomething() could do an AJAX call or something else async, and then for whatever reason call the callback to have the event propagation continue

How can the children listen/capture to the parent's event

How can the parent fire the custom event to notify its children/siblings?
For example:
<div id="div1">
<div id="div2"></div>
</div>
div2 had addEventListener('customEvent2', doSth), and then div1 will fire a custom event (customEvnet2), but this will never trigger div2's doSth function.
Sample code: http://jsfiddle.net/r4tcT/2/
The "div 1 trigger customEvent 2" button never works
so when a parent fire a custom event (dispatchEvent[IE9]/fireEvent[IE9-]/trigger[jQuery]), the children can not capture the event.
Is there any workaround?
The difference you are talking about is either between the 'Capturing' event model or the 'Bubbling' event model. jQuery's trigger operates on the Bubble model probably because this is the more supported event model -- mainly thanks to Internet Explorer. The Bubble model only travels backwards up through an elements parents... this is the reason why your events don't trigger on div2 when fired from div1, as it is always bubbling up and not down.
I've not tried custom events before with native functions but most modern browsers allow for you to decide which type of model you use when you set the event listener:
addEventListener (type, listener[, useCapture])
https://developer.mozilla.org/en-US/docs/DOM/element.addEventListener
Basically if you use true as the final argument the event listener should trigger in the Capture phase (which is when the event is travelling down the dom tree). If set to false the event will trigger in the bubbling phase which occurs when travelling back up the dom tree.
This has been discussed here:
Event Capturing vs Event Bubbling
As I've said whether this will work for bespoke events I'm not sure. I am pretty certain you can not do this with jQuery (as of yet) probably due to the lack of support in older browsers.
Correction
It appears what I guessed at above doesn't work. I thought as much due to the term 'Capturing' kind of makes you think about capturing user input -- and when bespoke events are involved there is no way to define a new kind of user input. So with that in mind I put together this quick jQuery plugin... it's only been roughly tested, but the logic should be sound - hope it's useful:
/**
* unbubble v0.2
*
* trigger an event down through the children of a collection,
* rather than up through it's parents
*
* #update 2013/03/18 - fixed the problem of triggering bubble phase each
* step down the element tree as pointed out by #vine.
*/
$.fn.unbubble = function( eventNames ){
var names = eventNames.split(' '),
non = names.length,
args = Array.prototype.slice.call(arguments);
/// our own trigger function designed to bubble down... not up!
var trigger = function(){
var i, events, elm = $(this);
/// make sure we can read the events array
if ( $._data ) {
/// make sure events is defined
if ( (events = $._data(this, 'events')) ) {
/// do a quick check, saves firing trigger on every element found
for ( i=0; i<non; i++ ) {
/// make sure our eventName appears in the event list
if ( names[i] && ( names[i] in events ) ) {
/// trigger the standard jQuery trigger function
elm.triggerHandler.apply(elm, args);
/// escape as trigger should fire for multiple names
break;
}
}
}
}
/// if we can't access the events array, just trigger and hope
else {
/// trigger the standard jQuery trigger function
elm.triggerHandler.apply(elm, args);
}
/// trigger for all the children, and on, and on...
elm.children().each(trigger);
};
/// foreach element trigger now...
this.each(trigger);
}
/**
* Example usage
*/
$(function(){
/// bind our event as usual
$('.div2').bind('customEvent', function(){
alert('I should trigger!');
});
/// rather than use trigger, fire with unbubble
$('#div1').unbubble( 'customEvent' );
});
The answer of pebbl is good but it has flaw. The capturing phase is somehow simulated by the normal triggering of events from document down to the concerned element. But the issue will be, calling the standard jQuery trigger function on any element will immediately followed by a bubbling phase starting from that element and up. So i believe he can stick with accessing the events data directly from the collection of elements and calling it directly not using the standard trigger function , something like this
var JQ_LT_17 = parseFloat($.fn.jquery) < 1.7;
function getEventsData(element) {
return JQ_LT_17 ? $(element).data('events') : $._data(element).events;
}
Code snippet borrowed form jQuery.bind-first library v0.1 Vladimir Zhuravlev
customEvent2 is only bound to div2. When you try to trigger it on div1, nothing happens because that event doesn't exist for div1.
If you want to fire customEvent2, it has to be triggered on an element (or child of one) it is actually bound to.
I have played a little bit with the code and here is what I now have. It is a hack, but maybe it could help you to solve your problem?

Memory leak with unused event handler in javascript

I'm in a situation where I make a lot of ajax calls to change the same portion of html. This html represent a grid. After changing the html in the ajax call, I attach a event handler to an event of the grid. When the user click on a refresh button, I execute the same ajax call that set new html code and also add again an event handler to listen of event of the grid.
I want to know if each time I refresh my grid and add a new event handler if the previous event handler is still in memory and if yes, what are the bests practices in this situation? (e.g. unbind the event handler if exist before putting new html)
Here is an example of what I do:
$.get(this.config.actionLoggingUserListUrl, viewModel, function (data) {
MyNamespace.ui.hideGridAnimation($("#LoggingActionUsersList"));
if (data.success) {
$("#validationSummary").html("");
$("#usersList").html(data.result);
$("#LoggingActionUsersList").click(function() {
console.log("Here is my event handler attached multiple times!");
});
}
else {
$("#validationSummary").html(data.result);
$("#usersList").html("");
}
});
Note that the event handler I'm talking in this case is:
$("#LoggingActionUsersList").click(function() {
console.log("Here is my event handler attached multiple times!");
});
Event handlers stack, so yeah, this is a memork leak. Probably a fairly insignificant one, but its more the principle than the effect. Unless for some reason you really do need to have dynamic event handlers (something that is pretty rarely used as there aren't very many realistic uses for it), I'd strongly suggest pulling the event handler assignment out of the ajax call.
If you do need the event handler to change, the clever way to do it would be to make your event handler smart enough to know a little bit about the object to which it is assigned. That way, instead of adding a new event each time, you can just have logic in the event handler do different things based on the current identity of the object.
why are you binding it every time you make the call?
You are adding onto the stack every time. You are not replacing it. Best solution is to use on and do it once.
Other solution is to unbind the click event, before you add click event. The problem with this solution is if anything else added the click event, you just removed it.

jQuery: more than one handler for same event

What happens if I bind two event handlers to the same event for the same element?
For example:
var elem = $("...")
elem.click(...);
elem.click(...);
Does the last handler "win", or will both handlers be run?
Both handlers will run, the jQuery event model allows multiple handlers on one element, therefore a later handler does not override an older handler.
The handlers will execute in the order in which they were bound.
Suppose that you have two handlers, f and g, and want to make sure that they are executed in a known and fixed order, then just encapsulate them:
$("...").click(function(event){
f(event);
g(event);
});
In this way there is (from the perspective of jQuery) only one handler, which calls f and g in the specified order.
jQuery's .bind() fires in the order it was bound:
When an event reaches an element, all handlers bound to that event
type for the element are fired. If there are multiple handlers
registered, they will always execute in the order in which they were
bound. After all handlers have executed, the event continues along the
normal event propagation path.
Source: http://api.jquery.com/bind/
Because jQuery's other functions (ex. .click()) are shortcuts for .bind('click', handler), I would guess that they are also triggered in the order they are bound.
You should be able to use chaining to execute the events in sequence, e.g.:
$('#target')
.bind('click',function(event) {
alert('Hello!');
})
.bind('click',function(event) {
alert('Hello again!');
})
.bind('click',function(event) {
alert('Hello yet again!');
});
I guess the below code is doing the same
$('#target')
.click(function(event) {
alert('Hello!');
})
.click(function(event) {
alert('Hello again!');
})
.click(function(event) {
alert('Hello yet again!');
});
Source: http://www.peachpit.com/articles/article.aspx?p=1371947&seqNum=3
TFM also says:
When an event reaches an element, all handlers bound to that event
type for the element are fired. If there are multiple handlers
registered, they will always execute in the order in which they were
bound. After all handlers have executed, the event continues along the
normal event propagation path.
Both handlers get called.
You may be thinking of inline event binding (eg "onclick=..."), where a big drawback is only one handler may be set for an event.
jQuery conforms to the DOM Level 2 event registration model:
The DOM Event Model allows
registration of multiple event
listeners on a single EventTarget. To
achieve this, event listeners are no
longer stored as attribute values
Made it work successfully using the 2 methods: Stephan202's encapsulation and multiple event listeners. I have 3 search tabs, let's define their input text id's in an Array:
var ids = new Array("searchtab1", "searchtab2", "searchtab3");
When the content of searchtab1 changes, I want to update searchtab2 and searchtab3. Did it this way for encapsulation:
for (var i in ids) {
$("#" + ids[i]).change(function() {
for (var j in ids) {
if (this != ids[j]) {
$("#" + ids[j]).val($(this).val());
}
}
});
}
Multiple event listeners:
for (var i in ids) {
for (var j in ids) {
if (ids[i] != ids[j]) {
$("#" + ids[i]).change(function() {
$("#" + ids[j]).val($(this).val());
});
}
}
}
I like both methods, but the programmer chose encapsulation, however multiple event listeners worked also. We used Chrome to test it.
There is a workaround to guarantee that one handler happens after another: attach the second handler to a containing element and let the event bubble up. In the handler attached to the container, you can look at event.target and do something if it's the one you're interested in.
Crude, maybe, but it definitely should work.
jquery will execute both handler since it allows multiple event handlers.
I have created sample code. You can try it
demo

Categories

Resources