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:
Related
I have defined this event handler:
document.addEventListener("load", function(){
alert("Called on page load");
}, false);
I noticed it does not get called when the boolean flag is set to false(fire at bubble phase). Could someone help me out on why this is the case.
When an event is being sent to an element, it descends the document tree in the capture phase until it reaches the target. Then, if it’s a bubbling event, it bubbles back up.
From 2.1 Introduction to “DOM Events” in the DOM standard:
When an event is dispatched to an object that participates in a tree (e.g. an element), it can reach event listeners on that object’s ancestors too. First all object’s ancestor event listeners whose capture variable is set to true are invoked, in tree order. Second, object’s own event listeners are invoked. And finally, and only if event’s bubbles attribute value is true, object’s ancestor event listeners are invoked again, but now in reverse tree order.
load isn’t a bubbling event, and – here’s the important part – it doesn’t target document. When you add a capture listener, you’re really getting load events from parts of the document’s content that normally receive the event, like scripts or images. On a page with only the script, you won’t see the listener get called at all:
<iframe srcdoc="<script>document.addEventListener('load', () => { alert('loaded'); }, true);</script>"></iframe>
And on a page with load events that fire after the listener is attached, like this Stack Snippet that includes <style>s, you’ll see it more than once:
let i = 0;
document.addEventListener('load', e => {
console.log(`loaded ${++i}: ${e.target.nodeName}`);
}, true);
You probably meant to add a non-capturing listener to window instead of document, because window is something that receives a load event, unlike document. (Or you might have meant something else. There’s a lot of ways to interpret “page load”. See Window: load event on MDN for details on what the load event means on window and alternatives if it wasn’t what you intended.)
window.addEventListener("load", function() {
alert("Called on page load");
}, false);
I want to do something on all clicks except on a certain element.
I've created a very simple example which demonstrates the issue: http://jsfiddle.net/nhe6wk77/.
My code:
$('body').on('click', ':not(a)', function () {
// do stuff
});
I'd expect all click to on <a> to be ignored, but this is not the case.
Am I doing something wrong or is this a bug on jQuery's side?
There's a lot going on in that code that's not obvious. Most importantly, the click event is actually attached to the body element. Since that element isn't an anchor, you'll always get the alert. (Event delegation works because the click event bubbles up from the a through all its ancestors, including body, until it reaches document.)
What you want to do is check the event.target. That will tell you the element that was actually clicked on, but the actual click event is still bound to the body element:
$('body').on('click', function (e) { // e = event object
if ($(e.target).is(':not(a)')) {
alert('got a click');
}
});
http://jsfiddle.net/y3kx19z7/
No this is not a bug but rather intended behaviour.
The event bubbles all the way up. By clicking the a node, you are still triggering it's parents event from the div node.
Read more about event bubbling in the W3C DOM Specification. Just search for "bubble".
You need to stop the event propagation of the a nodes. i.e.:
$('body').on('click', ':not(a)', function () {
// do something effectively
alert('you should not see me when clicking a link');
});
$("a").click(function( event ) {
// do nothing effectively, but stop event bubbling
event.stopPropagation();
});
JSFiddle: http://jsfiddle.net/nhe6wk77/6/
It's working as intended, here's why!
Use of the :not() selector is honored in delegated events, but it's an uncommon practice because of how events bubble up the DOM tree potentially triggering the handler multiple times along the way.
The jQuery API Documentation states that:
jQuery bubbles the event from the event target up to the element where the handler is attached (i.e., innermost to outermost element) and runs the handler for any elements along that path matching the selector.
Notice the phrase "and runs the handler for any elements along that path matching the selector".
In your example, jQuery is accurately not running the handler on the a element, but as the event bubbles up the tree, it runs the handler for any element that matches :not(a), which is every other element in the path.
Here is a clear example showing how this works: http://jsfiddle.net/gfullam/5mug7p2m/
$('body').on('click', ':not(a)', function (e) {
alert($(this).text());
});
<div class="outer">
<div class="inner">
Click once, trigger twice
</div>
</div>
<div class="outer">
<div class="inner">
<button type="button">Click once, trigger thrice</button>
</div>
</div>
Clicking on the link in the first block of nested divs, will start the event bubbling, but the clicked a element — a.k.a. the event target — doesn't trigger the handler because it doesn't match the :not(a) selector.
But as the event bubbles up through the DOM, each of its parents — a.k.a the event currentTarget — triggers the handler because they do match the :not(a) selector, causing the handler to run twice. Multiple triggering is something to be aware of since it may not be a desired result.
Likewise, clicking on the button in the second block of nested divs, will start the event bubbling, but this time the event target does match the :not(a) selector, so it triggers the handler immediately. Then as the event bubbles up, each of its parents matching the selector triggers the handler, too, causing the handler to run three times.
As others have suggested, you need to either bind an alternate handler that stops propagation on a click events or check the event target against the :not(a) selector inside your handler instead of the delegated selector.
$("body").click(function(e) {
if($(e.target).is('a')){
e.preventDefault();
return;
}
alert("woohoo!");
});
check the target of the click. this way you dont need to bind another event.
updated fiddle
I frequently use jQuery's on function to handle future events. Eg:
$(document).on('click', '.my-div', myHandler);
But, I'm not able to get this to work with the play or playing events on my video tag:
$(document).on('playing', 'video', function() {
console.log('video playing');
});
The following works just fine, though does not bind to future elements:
$('video').on('playing', function() {
console.log('video playing');
});
Does jQuery's future binding syntax of the on function only work with certain events, or does it lack suppoert for HTMLMediaElement object events? I'm using jQuery 1.11.2.
It lacks support for events that doesn't bubble, and media events don't bubble because they don't make sense on any other elements except media elements, and media elements, like <audio> and <video> can't be nested, so there's no need to let the event bubble up the chain.
The way it works is that events bubble, on a click event it starts on the element you click, which is the event.target and continues all the way up to the document level.
This is so a click on an image inside an anchor for instance, also triggers the anchor.
The way event delegation works, is that it attaches the event handler to elements higher up the event chain, and then checks the event.target
Quick jQuery example
$('#parent').on('click', function(e) {
if ( $(e.target).hasClass('child') ) func();
});
// is (somewhat) the same as
$('#parent').on('click', '.child', func);
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).
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
});