Using loops in Javascript event delegation - javascript

I have the following piece of code:
var page = document.getElementById("contentWrapper");
page.addEventListener("click", function (e) {
var target, clickTarget, propagationFlag;
target = e.target || e.srcElement;
while (target !== page) {
clickTarget = target.getAttribute("data-clickTarget");
if (clickTarget) {
clickHandler[clickTarget](e);
propagationFlag = target.getAttribute("data-propagationFlag");
}
if (propagationFlag === "true") {
break;
}
target = target.parentNode;
}
});
I'm using a single event handler in my whole project (single page application). Event handlers are identified using attribute "data-clickTarget" and to prevent event propagation "data-propagationFlag" is used.
If the DOM tree is large, should I go with looping approach or conventional event handlers?

Delegated event handlers can be slow if the document is large and if the selected element is far from the element that triggers the event... from JQuery documentation:
Attaching many delegated event handlers near the top of the document
tree can degrade performance. Each time the event occurs, jQuery must
compare all selectors of all attached events of that type to every
element in the path from the event target up to the top of the
document. For best performance, attach delegated events at a document
location as close as possible to the target elements. Avoid excessive
use of document or document.body for delegated events on large
documents.
As the documentation says for a "click" event probably this is not going to be a serious issue (because users won't click like crazy on a page) but for events like mouse motion or scroll slow response can become quite annoying.
The specific feature of delegated handlers is that even new elements added later to the DOM will use the handler, but do you really need this? If you are not writing a library but just an application then you control when new elements are added and thus you can factor out the even handler attachment into the element creation (in other words instead of having a function that just creates the new element, make it so that it creates the element and also automatically registers the standard event handler).

Related

Add on load event for img elements now and in future?

I want to add a load event on an image that would affect that image even when that image is added to the document after the page's initial load. For a click event I would do someting like this:
$(document).on('click', '.elem', function(e) {
// do stuff
});
When I try something similar with the load event, however, it does not to work. This is what I have tried:
$(document).on('load', '.image', function() {
// do stuff
});
This event is simply never triggered. Does anyone know what I may be doing wrong, or how to achieve this?
This answer is incorrect. It's possible to do this using the capture phase, see Dhia Louhichi's answer. I'll delete this answer when I can (i.e.., once it's no longer the accepted answer).
By their nature, delegated handlers only work for events that bubble. Not all do, and load is one of the ones that doesn't. The jQuery documentation even highlights this:
In all browsers, the load, scroll, and error events (e.g., on an element) do not bubble.
You'll have to add the load handlers to the images when you add them.
What I mean by "delegated handlers only work for events that bubble":
Events that bubble work like this (in the "bubbling" phase, which is the phase you normally work with): The event is fired on the element where it originates, and then on that element's parent, then that element's parent, etc. until it gets to the document element (html). This diagram from the DOM3 events spec may help make this clearer:
Using a delegated handler (the kind you're using in your question) relies on bubbling. Consider this HTML:
<div id="container">
<div class="content">xxxx</div>
<div class="content">xxxx</div>
<div class="content">xxxx</div>
</div>
If you do $("#container").on("click", ".content", ...) you're not hooking the event on the "content" divs, even though jQuery will make it seem a bit like you are. You're hooking the event on the "container" div. When the event bubbles down to the container div, jQuery looks at where it started and sees whether it passed through any "content" divs during its bubbling. If it did, jQuery calls your handler as though (mostly) you'd hooked the event on the "content" div. That's why delegated handlers work when you add elements later; the event isn't hooked on the element, but on the container.
So that's why it won't work for load: load doesn't bubble, so even though it fires on the img elements you add, it doesn't bubble to the parent and so on, and so you never see it. To see it, you have to hook it on the specific element, not an ancestor of it.
This code shows handling the load event for img elements created in the future, without explicitly adding a listener/handler to them, by using the capture phase of the event process on document.body (also works when attached to document, but not window because of backward compatibility issues):
document.body.addEventListener(
"load",
function (event) {
var elm = event.target;
if (elm.nodeName.toLowerCase() === 'img') {
console.log("Loaded: " + event.target.src);
}
},
true // Capture phase
);
Live Example:
document.body.addEventListener(
"load",
function (event) {
var elm = event.target;
if (elm.nodeName.toLowerCase() === 'img') {
console.log("Loaded: " + event.target.src);
}
},
true // Capture phase
);
// Brief wait, then add an image
setTimeout(function() {
document.body.insertAdjacentHTML(
"beforeend",
"<img src='https://via.placeholder.com/150/202080?text=Some+Image'>"
);
}, 400);
This is tested and works in at least the following:
IE9+
Chrome and other Chromium-based browsers (Opera, Edge, Vivaldi, ...)
Firefox
iOS Safari
The behavior is also documented. In fact, coincidentally the specification gives this example mentioning load by name (scroll down slightly from that link):
EXAMPLE 5
The following is one way to interpret the above tables: the load event will trigger event listeners attached on Element nodes for that event and on the capture and target phases. This event is not cancelable. If an event listener for the load event is attached to a node other than Window, Document, or Element nodes, or if it is attached to the bubbling phase only, this event listener would not be triggered.
That's saying load will be fired in the capture and target phases, but not the bubbling phase (since the event doesn't bubble).
By default, when you use addEventListener, the handler is attached for the target phase of the element you call addEventListener on and the bubbling phase for any element within that element. If you add the third argument with the value true, though, it attaches the handler for the target phase of the element you call addEventListener on (as before) and the capture phase for any element within that element. So the code above will handle load for document.body during the target phase (except document.body doesn't fire load) and also handle load for the capture phase of any element within document.body.
More about event flows in the specification, including this handy diagram:

Javascript event listener refresh

I have some javascript that I inherited for my job. In this javascript we have a side bar that is constantly updated(every 1 - 10 or so minutes). In the script we parse and process the AJAX from the server and then we call an interesting function.
function renewClicks(){
$('.classElem').unbind('click');
$('.classElem2').unbind('click');
$('.classElem3').unbind('click');
$('.classElem').click(elm1funct);
$('.classElem2').clikc(elm2funct);
$('.classElem3').click(elm3funct);
}
Where .classElem is a css class selector that is appended to each image that is added to the page. And elmfunct is a function that is written to handle the click. This runs on each update (deauthorizing valid already added elements and then re adding them all). I want to know if there is a way I can possibly attach a listener on the body element in the DOM so that all of the image elements added to the page and that inherit the css class will already be handled and therefore not unregistered and re-registered on each update.
Thank you for any info you can provide.
You could try this:
$('body').on('click','.classElem',elm1funct)
.on('click','.classElem2',elm2funct)
.on('click','.classElem3', elm3funct);
From jQuery's docs:
Delegated events have the advantage that they can process events from
descendant elements that are added to the document at a later time. By
picking an element that is guaranteed to be present at the time the
delegated event handler is attached, you can use delegated events to
avoid the need to frequently attach and remove event handlers.
As #crush mentioned, use an event delegated approach to avoid unbinding and re-binding events:
$(document).on('click', '.classElem', elm1funct);

Attach click handler to dynamically generated anchor tag

I dynamically generate some markup and inject it into the DOM like this:
content+='<td><a class="reportLink" onclick="showReport();return false;" href="'+layerResults.features[i].attributes['Information_External']+'">Info</a></td>';
I know it would be better to use jQuery to attach the click handler instead of using an inline handler.
The problems are, even using an inline handler and a function like this:
function showReport() {
console.log('stopped');
}
Still doesn't prevent the link from navigating away from my page.
The second problem is, when I try using
jQuery('.reportLink'.on('click', function(e) {
e.preventDefault();
console.log('clicked');
});
The event never gets attached. I'm using jQuery 1.7.2.
This is driving me a bit insane as it's a simple task I've done about a zillion times in jQuery <= 1.5.
Delegate the event handler to a parent element that exists at the time the dom is loaded. You can replace body with that parent.
jQuery('body').on('click','.reportLink', function(e){
e.preventDefault();
console.log('clicked');
});
from jquery docs .on()
Event handlers are bound only to the currently selected elements; they must exist on the page at the time your code makes the call to .on(). To ensure the elements are present and can be selected, perform event binding inside a document ready handler for elements that are in the HTML markup on the page. If new HTML is being injected into the page, select the elements and attach event handlers after the new HTML is placed into the page. Or, use delegated events to attach an event handler, as described next.
Delegated events have the advantage that they can process events from descendant elements that are added to the document at a later time. By picking an element that is guaranteed to be present at the time the delegated event handler is attached, you can use delegated events to avoid the need to frequently attach and remove event handlers. This element could be the container element of a view in a Model-View-Controller design, for example, or document if the event handler wants to monitor all bubbling events in the document. The document element is available in the head of the document before loading any other HTML, so it is safe to attach events there without waiting for the document to be ready.
In addition to their ability to handle events on descendant elements not yet created, another advantage of delegated events is their potential for much lower overhead when many elements must be monitored. On a data table with 1,000 rows in its tbody, this example attaches a handler to 1,000 elements:
to prevent it from navigating away, enter this right after "console.log('stopped');
return false;
for the second one, i usually use this syntax, maybe it'll help:
jQuery(".reportLink").click(function() {
//do something
});

Override all JavaScript events bound to an element with a single new event

Assuming that there are a large number of elements throughout the site that have an unknown number and type of events bound to them.
If I need to override all of these events with one single bound event, and only that event will fire, what are some recommendations?
I would be binding the event to a click event handler, and I am using jQuery.
Thanks in advance.
You’re looking for jQuery#unbind.
To remove all event handlers on an element or a set of elements, just do:
$('.some-selector').unbind();
To unbind only click handlers, use unbind('click'):
$('.some-selector').unbind('click');
To unbind all click handlers and immediately bind your own handler after that, you can do something like this:
$('.some-selector').unbind('click').click(function(event) {
// Your code goes here
});
Note that this will only work for events bound using jQuery (using .bind or any jQuery method that uses .bind internally). If you want to remove all possible onclick events from a given set of elements, you could use:
$('.some-selector')
.unbind('click') // takes care of jQuery-bound click events
.attr('onclick', '') // clears `onclick` attributes in the HTML
.each(function() { // reset `onclick` event handlers
this.onclick = null;
});
I would like to provide a thought without removing all events all together (just override them).
If your new one single bound event (we call it "click" here) is specific to the element it binds to, then I believe you can ignore any other events simply by stopPropagation() function. Like this
$("specific-selector").on("click", ".specific-class", function (e) {
e.stopPropagation()
// e.stopImmediatePropagation()
/* your code continues ... */
});
It will stop events bubbles up, so your other events won't fire. use stopImmediatePropagation() to prevent other events attached onto the same elements as "click" does.
For example, if "mouseleave" event is also bind to $("specific-selector .specific-class") element, it won't fire, too.
At last, all other events won't fire on this element but your new "click" element.
The unsolved question is, what if other events also use stopPropagation()? ... Then I think the one with best specification wins, so try to avoid complex, too many events is final suggestion.
You can see "Direct and delegated events" on jQuery site for more information.
Looks like this is pretty simple actually:
$('#foo').unbind('click');
$('#foo').bind('click', myNewFunction);
Thanks for your responses though.
Try to use live instead of bind. Then you can easily remove live binding with die from selector which is fast operation and set another live equally fast.
$('selection here').live('..', .....); // multiple invocations
$('selection here').die();
$('selection here').live('click',.....);
DOM is not touched at all. Event condition is evaluated on event occurrence.
But generally if you just want to swap handler functions why not to do it this way:
var ahandler = function(evt) { /* first implementation */ }
$('.selector').bind('click', function(evt) { ahandler(evt); });
//and then if you want to change handlers
ahandler = function(evt) { /* new implementation */ };
This gives absolutely no cost of any changes, rebinding etc.

A bookmarklet that adds an onclick event to every element on the page?

I want to make bookmarklet, and when you press it all the elements on the page gets an onclick event that calls a function with the element as a parameter or at least the elements id/class/something, is that possible? In that case, how would I do it?
click bubbles. So just bind to the <body> and check what the target of the event element is (you'll need to do a bit of event normalization to get around IE being different from everyone else.)
event.target is the actual DOM element, so you can filter on anything that you want to filter on at that point.
This has the advantage that it doesn't matter how many elements are on the page, you only need one event handler for all of them. (Which means performance will be better on pages with hundreds or thousands of elements.)
javascript:void( document.body.onclick = function (event) { event = event || window.event; var element = event.target || event.srcElement; alert(element.textContent) } )

Categories

Resources